Uma pequena correção no código do contador de ciclos

Tenho postado a macro para contar ciclos de máquina aqui faz um tempo. É uma “macro” porque, em teoria, o código fica inline com o código a ser testado. Acontece que é mais fácil ainda usar um código completamente em assembly e, acredito, que eu o tenha publicado por aqui. Se o fiz (não me lembro), o código em assembly tem um errinho na rotina que coleta a contagem inicial. Eis o código correto:

; Código para o NASM.
bits 64
section .text

; Retorna a contagem de ciclos inicial,
; depois de limpar o cache.
global start_cycle_count:function
start_cycle_count:
  ; RDTSCP obtém o Time Stamp Counter e lê
  ; o registrdor IA32_TSC_AUX em RCX, serializando
  ; o processador.</span>
  times 16 rdtscp
  shl rdx,32
  or  rax,rdx
  ret

; Retorna a contagem final.
global end_cycle_count:function
end_cycle_count:
  rdtsc
  shl rdx,32
  or  rax,rdx
  ret

Note que a função start_cycle_count() contém 16 instruções RDTSCP. Em teoria não seriam necessárias 16 instruções, uma só bastava… mas quero ter certeza que o cache L1 esteja vazio antes de ler o timestamp. Em contrapartida, a função end_cycle_count() usa a instrução RDTSC, que não serializa o processador…

Essa diferença serve para obter sempre o pior caso possível, quando o cache está vazio!

Note também que, seundo a especificação POSIX com relação à convenção de chamadas, apenas os registradores RBP, RBX e de R12 até R15 precisam ser preservados pela rotina chamada. Como nenhuma das duas funções sequer usam esses registradores, nada precisou ser “salvo”.

O header file é este:

#ifndef __CYCLE_COUNTER_INCLUDED__
#define __CYCLE_COUNTER_INCLUDED__

extern unsigned long long start_cycle_count(void);
extern unsigned long long end_cycle_count(void);

#endif

Forma de usar:

#include "cycle_counter.h"

...
unsigned long long t1;
int i;

i = 1000;
t1 = start_cycle_count();
while (i--) do_something();
t1 = end_cycle_count() - t1;

A variável ‘t1’ conterá, aproximadamente, a quantidade de ciclos gastos por 1000 execuções da função do_something(), 1000 vezes.

Possível makefile para testes:

test: test.o cycle_counter.o
  gcc -o $@ $^

cycle_counter.o: cycle_counter.asm
  nasm -f elf64 -o $@ $<

test.o: test.c cycle_counter.h
  gcc -O3 -mtune=native -c -o $@ $<
Anúncios

6 comentários sobre “Uma pequena correção no código do contador de ciclos

  1. Muito bom esse post, parabéns. Gostaria de provocar um pequeno pensamento: O fato da função end_cycle_count utilizar a instrução rdtsc, que não serializa o processador, poderia permitir que alguma instrução termine depois de rdtsc, influenciando negativamente na contagem.

    1. Note que todos os “testes” têm que estar contidos entre start_cycle_count() e end_cycle_count(). Assim, tanto faz que end_cycle_count() não serialize. Além do mais, o problema de ciclos adicionais sendo contados não é, necessariamente, culpa de end_cycle_count(), mas de start_cycle_count()… Já que depois de RDTSCP temos um shift, um OR e um retorno.

      A “poluição” na contagem não é lá muito significativa, já que existem muitos outros fatores não considerados… E também, já que é prudente usar esse tipo de contagem “do homem pobre” em múltiplas chamadas do código que se quer testar… Costumo fazer 1000 chamadas a uma função sob teste e dividir o valor por 1000… Mesmo assim, faço isso sob multiplas chamadas ao mesmo código de teste para obter uma média…

      Anyway, thanks pelo comentário, Mr. Pl4nkt0n! :)

      1. Você está correto Frederico, andei lendo alguns manuais da Intel e gostaria de reformular minha ideia. Corroboro que esse tipo de contagem apesar de não ser extremamente “limpa” traz uma ótima ideia sobre o desempenho mas gostaria de sugerir uma alternativa. Grosseiramente seria:

        CPUID
        RDTSC
        //function to benchmark
        RDTSCP
        CPUID

        1) O primeiro CPUID serializa as instruções acima de RDTSC, não permitindo a ocorrencia de alguma out-of-order execution depois do RDTSC, “poluindo” nossa contagem
        RDTSCP apesar de ler o timestamping register, logo em seguida lê a CPU id, poluindo a contagem, além disso sua “pseudo” serialização não impede que uma instrução abaixo dele seja executada antes dele (resultado de uma out-of-order execution)[1]

        2) O RDTSC lê o timestamping register

        3) O RDTSCP lê o timestamping e serializa as instruções que estão acima dele

        4) Por fim o CPUID impede out-of-order para as instruções que estão abaixo dele (corrigindo a caracteristica falha do RDTSCP).

        Será necessário salvar os registradores EAX EDX antes do ultimo CPUID, já que ele irá sobrescrever os valores.

        Ainda assim, teremos algumas “sejeiras” no cálculo, mas tentamos fixar elas, impedindo que out-of-order executions compliquem mais nossa vida.

        É isso…

        Forte abraço e parabéns pelo ótimo blog :D

        Referencia:
        [1] ia-32-ia-64-benchmark-code-execution-paperout

      2. Ahhh… o uso de RDTSCP no início da contagem, está em conformidade com as especificações da própria Intel, no uso do TimeStamp Counter Register para medição de performance…
        Não tenho mais o link, mas quando achá-lo, coloco por aqui…

      3. Que maneiro, gostaria de ver :D
        Quando eu tiver algum PoC dessa ideia doida ai eu te mando tbm
        Quem sabe conseguimos melhorar um pouco a abordagem

        Obrigado pela atenção Frederico

        Cheers

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s