Tem algo estranho acontecendo…

Nunca me preocupei muito em usar o recurso de performance counting, presente nos processadores Intel desde o Pentium P6. O motivo? A instrução de leitura desses contadores só funciona no ring 0 — ou, pelo menos eu sempre confiei que a especificação da Intel estivesse correta — e os próprios contadores são dependentes de arquitetura. De modelo em modelo eles mudam de lugar e de semântica.

Mas, eis que me interessei por eles. O motivo é simples: Eles são mais precisos do que usar o timestamp counter (TSC). Então, mesmo sabendo que eu obteria uma exceção do tipo “segmentation fault” na cara (ou “General Protection Fault”, se preferirem), fiz um pequeno teste:

inline unsigned long long rdpmc_actual_cycles(void)
{
  unsigned int a, d, c;

  c = (1U << 30) + 1;
  __asm__ __volatile__ (
    "rdpmc"
    : "=a" (a), "=d" (d)
    : "c" (c)
  );

  return (unsigned long long)a | 
         (((unsigned long long)d) << 32);
}

Como disse, ao executar RDPMC (PMC=Performance Monitoring Counter) eu esperava um “segmentation fault”, mas não é isso o que obtenho. Eis o código de teste:

#include <stdio.h>

inline unsigned long long rdpmc_actual_cycles(void)
{
  unsigned int a, d, c;

  c = (1U << 30) + 1;
  __asm__ __volatile__ (
    "rdpmc"
    : "=a" (a), "=d" (d)
    : "c" (c)
  );

  return (unsigned long long)a |
          (((unsigned long long)d) << 32);
}

inline unsigned long long rdtsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ (
    "rdtsc"
    : "=a" (a), "=d" (d)
  );

  return (unsigned long long)a |
         (((unsigned long long)d) << 32);
}

extern int f(int); /* Uma função simples. */
/* definida em f.c como:
     int f(int x) { return 2*x; } */

void main(void)
{
  unsigned long long c1, c2, c3, c4;
  unsigned short cs;
  int v1, v2;

  __asm__ __volatile__ (
    "movw %%cs,%0"
    : "=a" (cs)
  );

  printf("CS Requestor Privilege Level = %hu\n",
    cs & 3);

  c1 = rdtsc();
  v1 = f(10);
  c2 = rdtsc();

  c3 = rdpmc_actual_cycles();
  v2 = f(20);
  c4 = rdpmc_actual_cycles();

  printf("c1=%lu, c2=%llu, rdtsc count=%llu\n"
         "c3=%lu, c4=%llu, rdpmc count=%llu\n"
         "v1=%d, v2=%d\n",
         c1, c2, (c2 - c1),
         c3, c4, (c4 - c3),
         v1, v2);
}

Qual não foi a minha surpresa ao saber que RDPMC não causa GPF nesse caso! E, ainda mais, o contador de performance é, de fato, mais preciso:

$ gcc -O2 -o rdpmc rdpmc.c f.c
$ ./rdpmc
CS Requestor Privilege Level (RPL) = 3

c1=56484229725606, c2=56484229725776, rdtsc count=170
c3=281462128258839, c4=281462128258891, rdpmc count=52
v1=20, v2=40

$ ./rdpmc
CS Requestor Privilege Level (RPL) = 3

c1=56486155175262, c2=56486155175398, rdtsc count=136
c3=281446297640345, c4=281446297640397, rdpmc count=52
v1=20, v2=40

$ ./rdpmc
CS Requestor Privilege Level (RPL) = 3

c1=56487756713152, c2=56487756713309, rdtsc count=157
c3=281472235489050, c4=281472235489102, rdpmc count=52
v1=20, v2=40

Note a variação da contagem usando TSC. Ele não leva em conta os efeitos do cache, da paginação, das interrupções, do chaveamento de tarefas. O TSC simplesmente lê o contador de clocks… Já os performance counters não contam os ciclos enquanto o processador está num estado de “halt” ou “idle”.

Vale o aviso de que o jeito com que estou lendo os contadores está errado. A documentação diz que o correto é ler o contador 1 (actual cycles) e depois o contador 2 (reference cycles) para obter a diferença real… Acontece que ao inicializar ECX com 0x80000002, obtenho como contagem o valor zero! E isso ai, acima, parece funcionar bem…

Mas, é claro, é muito estranho! É contra a especificação da própria Intel que diz, explícita e claramente que o CPL (Current Privilege Level) tem que ser 0. Do manual Intel 64® and IA-32 Architecture Software Development’s Manual Volume 2:

#GP(0)  If the current privilege level is not 0 and the PCE flag 
        in the CR4 register is clear.

O flag PCE em CR4 habilita ou não (PCE=Performance Counter monitoring Enable) o recurso de monitoramento. De qualquer maneira a especificação é clara… PCE tem que estar habilitado e CPL (e, por consequência, o RPL de CS) tem que ser 0…

Note que o programinha anterior está executando no CPL 3 (o Requestor Privilege Level no seletor CS nos diz isso!), ou seja no usersapce! E, só para confirmar, o processador onde testei esse código é um i5 e o sistema operacional é Debian 8.3:

$ cat /proc/cpuinfo | grep '^model name' | uniq
model name	: Intel(R) Core(TM) i5-3570 CPU @ 3.40GHz

$ lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 8.3 (jessie)
Release:	8.3
Codename:	jessie

Só posso presumir que isso é um bug do processador… Preciso testar em outros…

UPDATE:

Isso também funcionou no Ubuntu 14.04 com um processador i7:

$ cat /proc/cpuinfo | grep '^model name' | uniq
model name    : Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz

$ lsb_release -a
No LSB modules are available.
Distributor ID:    Ubuntu
Description:    Ubuntu 14.04.4 LTS
Release:    14.04
Codename:    trusty

UPDATE 2:

Alterei os tipos de unsigned long para unsigned long long caso você queira testar as rotinas no Windows. Lembre-se que Linux usa o padrão I32LP64 e o Windows usa IL32P64. Em minha residência eu não tenho uma VM com o Windows instalado, daí não posso fazer o teste (uma preguiça dos diabos para instalar essa coisa inútil da Microsoft aqui!)… Vou pedir para alguém testar pra mim e depois atualizo o artigo.

Anúncios