Calcular a frequência da CPU em C com RDTSC sempre retorna 0
-
26-09-2019 - |
Pergunta
A seguinte parte do código nos foi dada de nosso instrutor para que pudéssemos medir alguns algoritmos de desempenho:
#include <stdio.h>
#include <unistd.h>
static unsigned cyc_hi = 0, cyc_lo = 0;
static void access_counter(unsigned *hi, unsigned *lo) {
asm("rdtsc; movl %%edx,%0; movl %%eax,%1"
: "=r" (*hi), "=r" (*lo)
: /* No input */
: "%edx", "%eax");
}
void start_counter() {
access_counter(&cyc_hi, &cyc_lo);
}
double get_counter() {
unsigned ncyc_hi, ncyc_lo, hi, lo, borrow;
double result;
access_counter(&ncyc_hi, &ncyc_lo);
lo = ncyc_lo - cyc_lo;
borrow = lo > ncyc_lo;
hi = ncyc_hi - cyc_hi - borrow;
result = (double) hi * (1 << 30) * 4 + lo;
return result;
}
No entanto, preciso que esse código seja portátil para máquinas com diferentes frequências da CPU. Para isso, estou tentando calcular a frequência da CPU da máquina onde o código está sendo executado assim:
int main(void)
{
double c1, c2;
start_counter();
c1 = get_counter();
sleep(1);
c2 = get_counter();
printf("CPU Frequency: %.1f MHz\n", (c2-c1)/1E6);
printf("CPU Frequency: %.1f GHz\n", (c2-c1)/1E9);
return 0;
}
O problema é que o resultado é sempre 0 e não consigo entender o porquê. Estou executando o Linux (Arch) como convidado no VMware.
Na máquina de um amigo (MacBook), está trabalhando até certo ponto; Quero dizer, o resultado é maior que 0, mas é variável porque a frequência da CPU não é fixa (tentamos corrigi -lo, mas por algum motivo não somos capazes de fazê -lo). Ele tem uma máquina diferente que está executando o Linux (Ubuntu) como host e também relata 0. Isso descarta o problema na máquina virtual, que eu pensei que era o problema no início.
Alguma idéia de por que isso está acontecendo e como posso consertar isso?
Solução
Ok, como a outra resposta não foi útil, tentarei explicar mais detalhes. O problema é que uma CPU moderna pode executar instruções fora de ordem. Seu código começa como algo como:
rdtsc
push 1
call sleep
rdtsc
CPUs modernas fazem não necessariamente executar instruções em sua ordem original. Apesar do seu pedido original, a CPU é (principalmente) livre para executar isso como:
rdtsc
rdtsc
push 1
call sleep
Nesse caso, fica claro por que a diferença entre os dois rdtsc
S estaria (pelo menos muito próximo de) 0. Para evitar isso, você precisa executar uma instrução de que a CPU irá Nunca reorganize para executar fora de ordem. A instrução mais comum a ser usada para isso é CPUID
. A outra resposta que vinculei deve (se a memória servir) começar aproximadamente a partir daí, sobre as etapas necessárias para usar CPUID
corretamente/efetivamente para esta tarefa.
Claro, é possível que Tim Post estivesse certo e você está também vendo problemas por causa de uma máquina virtual. No entanto, como está agora, não há garantia de que seu código funcione corretamente, mesmo em hardware real.
Editar: sobre por que o código gostaria Trabalho: Bem, antes de tudo, o fato de que as instruções posso ser executado fora de ordem não garante que eles vai ser. Segundo, é possível que (pelo menos algumas implementações de) sleep
contêm instruções serializadas que evitam rdtsc
de serem reorganizados em torno dele, enquanto outros não (ou podem conter -os, mas apenas os executam em circunstâncias específicas (mas não especificadas)).
O que você resta é o comportamento que pode mudar com quase qualquer recompilação, ou mesmo apenas entre uma corrida e a próxima. Pode produzir resultados extremamente precisos dezenas de vezes seguidas e depois falhar por algum (quase) motivo completamente inexplicável (por exemplo, algo que aconteceu em algum outro processo completamente).
Outras dicas
Não posso dizer ao certo o que exatamente há de errado com seu código, mas você está fazendo um trabalho desnecessário para uma instrução tão simples. Eu recomendo que você simplifique o seu rdtsc
código substancialmente. Você não precisa fazer matemática de 64 bits, e não precisa armazenar o resultado dessa operação como dupla. Você não precisa usar saídas separadas no seu ASM embutido, pode dizer ao GCC para usar o EAX e o EDX.
Aqui está uma versão muito simplificada deste código:
#include <stdint.h>
uint64_t rdtsc() {
uint64_t ret;
# if __WORDSIZE == 64
asm ("rdtsc; shl $32, %%rdx; or %%rdx, %%rax;"
: "=A"(ret)
: /* no input */
: "%edx"
);
#else
asm ("rdtsc"
: "=A"(ret)
);
#endif
return ret;
}
Além disso, considere imprimir os valores que está saindo disso para poder ver se está saindo de 0s ou outra coisa.
Quanto ao VMware, dê uma olhada o tempo mantendo as especificações (Link pdf), bem como este tópico. As instruções do TSC são (dependendo do sistema operacional convidado):
- Passado diretamente para o hardware real (convidado PV)
- Ciclos de contagem enquanto A VM está executando no processador host (Windows / etc)
Nota, no #2 o enquanto A VM está executando no processador host. O mesmo fenômeno também iria para Xen, se bem me lembro. Em essência, você pode esperar que o código funcione conforme o esperado em um hóspede paravartualizado. Se imitada, é totalmente irracional esperar hardware como consistência.
Você esqueceu de usar volatile
Na sua declaração ASM, então você está dizendo ao compilador que o asm
A declaração produz a mesma saída sempre, como uma função pura. (volatile
só está implícito para asm
declarações sem saídas.)
Isso explica por que você está recebendo exatamente Zero: o compilador otimizado end-start
para 0
No momento da compilação, através da CSE (eliminação de comuns-subexpressão).
Veja minha resposta em Obter contagem do ciclo da CPU? para o __rdtsc()
A resposta intrínseca, e @ @mysticial, tem funcionando GNU c inline ASM, que vou citar aqui:
// prefer using the __rdtsc() intrinsic instead of inline asm at all. uint64_t rdtsc(){ unsigned int lo,hi; __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi)); return ((uint64_t)hi << 32) | lo; }
Isso funciona de maneira correta e eficiente para o código de 32 e 64 bits.
Hmmm, não tenho certeza, mas suspeito que o problema possa estar dentro desta linha:
resultado = (duplo) oi * (1 << 30) * 4 + lo;
Suspeito se você pode realizar com segurança uma multiplicações tão grandes em um "não assinado" ... Isso geralmente não é um número de 32 bits? ... apenas o fato de você não poder se multiplicar com segurança por 2^32 e ter que anexá -lo como um "* 4" adicionado ao 2^30 no final já sugere essa possibilidade ... você pode precisar Converta cada subcomponente Hi e Lo em um duplo (em vez de um único no final) e faça a multiplicação usando os dois duplos