Já acabei com o minha auto-flagelação (é assim que se escreve?!) e resolvi fazer umas medidas com relação ao uso da função rdtsc(). Como só faz sentido fazer testes com uma função destinada a testes do ponto de vista estatístico, então aqui vamos nós usar médias, modas e desvios-padrão. Aliás, por este último, entenda como sendo a média dos desvios da média (huh! ou seja: a maioria das amostras desviam-se de uma certa quantidade da média simples das mesmas amostras, ok?).
O desvio padrão nos dará uma idéia da precisão das medidas via rdtsc(). Vou usar o coeficiente de variação para expressar isso. Esse coeficiente é o a relação entre a o desvio-padrão (expressado pela letra grega σ) e a média (expresada pela letrea grega μ). A relação é dada em porcentagem. A interpretação é que a maioria das amostras desviam-se x porcento da média…
Antes de começarmos quero que você tenha em mente algumas informações sobre o seu hardware:
- Sua memória é particionada em blocos de 4 kB chamados páginas. Essas páginas podem ou não existir fisicamente. No caso de elas ainda não existirem o sistema operacional vai criá-la e, para isso, pode simplesmente alocar o bloco de memória e colocá-lo num diretório de páginas ou, se este diretório estiver cheio ou prestes a encher, pode decidir guardar uma ou mais das páginas pré-existentes em disco e atribuí-las ao seu processo (ou seja, fazer swap). Esses processos são demorados e implicam numa parada total do seu processo (tempo dedicado 100% para o kernel);
- Todos os processadores modernos possuem caches. Hoje é muito comum termos 2 caches. Um deles é o chamado cache L1 e localiza-se no interior do processador. Também hoje é comum termos caches L1 com 64 kB de tamanho. O outro cache é o L2 e também é muito comum que este cache esteja dentro do processador também. Em bons processadores o cache L2 pode ter 4 MB de tamanho. Caches aumentam a velocidade do acesso à memória porque, com eles, a quantidade de acessos à memória é diminuída. O problema é quando uma linha (64 bytes), correspondente a um bloco da memória do sistema, não se encontra no cache L1. Neste caso o processador pára tudo e tenta ler a linha do cache L2. Se a linha também não está no cache L2 então este tentará ler um bloco de linhas da memória do sistema e depois repassa a linha lida para o cache L1;
- Existem muitas coisas feitas pelo kernel do seu sistema operacional que o seu processo não tem controle. A maioria deles interrompe a execução de seu processo.
Esses três fatores afetam muito a medição dos ciclos de máquina de um fragmento do seu processo. E estes são os motivos que causam imprecisão no uso da função rdtsc().
O processo que usei para colher dados estatísticos do uso de rdtsc() compreende a leitura de n diferenças de tempo entre chamadas à rdtsc() e o armazenamento dessas diferenças em um buffer com n “quad words” (ou seja, cada entrada é do tipo “uint64_t”). Este tamanho n vai ser de 8 elementos (64 bytes), 32 elementos (256 bytes), 256 elementos (4 kB) e 1024 elementos (8 kB). Em essência, o fragmento de código que armazena as diferenças é assim:
int i; uint64_t buffer[BUFFER_SIZE], *p; for (p = buffer, i = 0; i < BUFFER_SIZE; i++, p++) { *p = rdtsc(); *p = rdtsc() - *p; }
Eis as estatísticas para 8 e 32 elementos:
$ ./test8 Tamanho do buffer: 64 bytes. Média: 314.12 Desvio padrão: 9.49 Coeficiente de variação: 3.02% Valor mínimo: 308 Valor máximo: 336 $ ./test32 Tamanho do buffer: 256 bytes. Média: 308.22 Desvio padrão: 1.24 Coeficiente de variação: 0.40% Valor mínimo: 308 Valor máximo: 315
Uma vez que o buffer está dentro do cache L1, a variação na medição de tempo não é lá grandes coisas. Chega a ser menor o igual a 5%. O problema é quando cruzamos a fronteira de uma página:
$ # Ainda não cruzamos a fronteira de 4096 bytes aqui! $ ./test256 Tamanho do buffer: 2048 bytes. Média: 309.94 Desvio padrão: 3.49 Coeficiente de variação: 1.13% Valor mínimo: 308 Valor máximo: 336 $ # Agora sim, cruzamos a fronteira! $ ./test1024 Tamanho do buffer: 8192 bytes. Média: 322.03 Desvio padrão: 290.35 Coeficiente de variação: 90.16% Valor mínimo: 308 Valor máximo: 7434
É interessante observar que, no último gráfico, os picos ocorrem aproximadamente aos 4 kB e aos 8 kB, justamente a fronteira de uma página!
Observem que, no geral, mesmo com os efeitos da paginação e do cache, RDTSC não foge muito de seu valor esperado, exceto em alguns pontos. Se fizermos os mesmos testes com um buffer de 1 milhão de elementos, obteríamos um gráfico mais ou menos como o último, com mais picos graças ao número quase 1000 vezes maior de amostras.
Meu veredito? rdtsc() possui problemas de precisão, mas é muito confiável. Mas, acho que eu já havia dito isso aqui, não?