Surpresa! Contar ciclos no ring0 não é tão bom assim!

Estive aqui testando um módulo para o kernel para medir os ciclos de clock gastos por uma função qualquer e comparando isso com a minha rotina para fazer o mesmo, no userspace. A rotina sob teste é simples:

/* test.c */

// Não vamos usar esse array fora daqui,
// por enquanto...
int a[100] = { 0 };

void f(void)
{
  for (int i = 1; i < 100; i++)
    a[i] = a[i-1] + 1;
}

No userspace basta usar as funções inline begin_tsc e end_tsc, como mostradas abaixo:

/* cycle_counting.h */

#ifdef __x86_64__
# define REGS1 "rbx", "rcx"
# define REGS2 "rax, "rbx", "rcx", "rdx"
#else
# ifdef __i386__
# define REGS1 "ebx", "ecx"
# define REGS2 "eax, "ebx", "ecx", "edx"
# else
#  error "Need x86-64 or i386 platforms to use cycle counting!"
# endif
#endif

unsigned long long __local_tsc;

inline void begin_tsc(void)
{
  unsigned int a, d;

  __asm__ __volatile__ ("xorl %%eax,%%eax\n"
                        "cpuid\n"
                        "rdtsc"
                        : "=a" (a), "=d" (d)
                        :: REGS1);

  __local_tsc = ((unsigned long long)d << 32) | a;
}

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

  __asm__ __volatile__ ("rdtscp\n"
                        "movl %%eax,%0\n"
                        "movl %%edx,%1\n"
                        "xorl %%eax,%%eax\n"
                        "cpuid"
                        : "=m" (a), "=m" (d)
                        :: REGS2);

  // Retorna a contagem de ciclos.
  return ((unsigned long long)d << 32) + a - __local_tsc;
}

Já para usá-las no kernelspace, temos que criar um módulo:

/* cyclemod.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/hardirq.h>
#include <linux/preempt.h>
#include "cycle_counting.h"

//extern int a[];
extern void f(void);

static int __init cyclecnt_start ( void )
{
  int i;
  unsigned long flags;
  unsigned long long c;

  printk ( KERN_INFO "Loading Cycle-Counting module...\n" );

  preempt_disable();
  raw_local_irq_save(flags);

  begin_tsc();
  f();
  c = end_tsc();

  raw_local_irq_restore(flags);
  preempt_enable();

  printk ( KERN_INFO "\tCycles: %llu\n", c);

  return 0;
}

static void __exit cyclecnt_end ( void )
{
  printk ( KERN_INFO "Goodbye Cycle-Counter.\n" );
}

module_init ( cyclecnt_start );
module_exit ( cyclecnt_end );

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Frederico L. Pissarra");
MODULE_DESCRIPTION("Cycle Counting Module");

Para compilar esse bicho, crie o makefile abaixo e simplesmente execute make. Você verá diversos arquivos, mas o módulo é o com extensão .ko (kernel object).

ccflags-y := -Ofast
obj-m := cyclescount.o
cyclescounter-objs := cyclemod.o test.o

KVERSION = $(shell uname -r)

all:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) modules

clean:
  make -C /lib/modules/$(KVERSION)/build M=$(PWD) clean

A diferença na rotina acima é que desabilitamos as interrupções para o processador local (provavelmente o kernel usa apenas a instrução CLI) e a preempção (não haverá chaveamento de tarefa para essa thread)… Isso deveria eliminar algum custo que existe no userspace e nos dar uma contagem mais “precisa”, mas veja só… Não é isso o que acontece:

$ ./cnttest    # userspace app
Cycles: 552
$ sudo insmod cyclecounter.ko
$ sudo rmmod cyclecounter
$ dmesg | sed -n '/Load.\+Cycle-Count/,+2p'
[17630.739459] Loading Cycle-Counting module...
[17630.739464] 	Cycles: 748
[17630.747001] Goodbye Cycle-Counter.

Como assim, no userspace obtive uma contagem menor de ciclos do que no kernspace?

E agora, Zé?!

Anúncios