Retratação: RDRAND é lerdo pacas!

Anteriormente fiz uma medição de ciclos do RDRAND com relação à chamada a função rand() – que usa o Linear Congruential Generator. O detalhe é que com pouquíssimas execuções da instrução ela é, de fato, bem rápida, mas com muitas execuções, parece, há algum problema e o processador leva quase 10 ms para executar a instrução (e isso está de acordo com a documentação da Intel).

Assim… se você quer uma garantia de aleatoriedade, use RDRAND, mas não espere grandes performances. Me desculpem pela informação meio errada.

Associatividade em caches

Confesso pra vocês que, por alguns anos, fiz uma confusão dos diabos sobre o que significa termos como “4 way set associative“. Sempre pensei nisso como se uma linha de cache fosse dividida em 4 pedaços e estava totalmente errado. O “set” do termo deveria ter chamado minha atenção.

Caches são memórias estáticas (hoje em dia, a maioria, pelo menos, contidas dentro do processador) que são bem mais rápidas que as memórias dinâmicas que estão contidas no “pentes” de memória instalados na sua placa mãe. O processador usa esses caches para acelerar o acesso à memória (evitando o acesso direto!).

Os caches são divididos em linhas. Nos processadores x86, desde o Pentium 4, pelo menos, cada linha de cache tem 64 bytes de tamanho. Não é possível lidar com menos de uma linha por vez, no que concerne os caches. Se você pedir para ler um único byte da memória, uma linha inteira (64 bytes) será carregada para o cache. Se você pedir para escrever um único byte, uma linha inteira de cache será, eventualmente, despejada para a memória, todos os 64 bytes, de uma só vez.

Cada linha é identificada por uma “marca” (uma tag) que corresponde aos bits superiores do endereço de memória usado. Os 6 bits inferiores do endereço compõem o offset dentro de uma linha. O que o processador faz quando você tentar ler/escrever num endereço é carregar uma linha inteira para o cache (L1) e marcá-la como pertencendo ao range de endereços onde os 6 bits inferiores vão de 0 até 0x3F… Daí toda leitura e escrita é feita com o processador procurando pela tag no cache e, se encontrar, o dado é escrito ou lido na linha.

Tipicamente o cache L1 (que é o cache mais próximo do processador lógico e o que mais nos interessa) tem 64 KiB de tamanho (em alguns processadores, 128 KiB ou 256 KiB). Vou assumir aqui 128 KiB… Com 128 KiB e 64 bytes por linha temos um total possível de armazenamento de 2048 linhas com tags diferentes. Quando você acessa a memória, o processador percorre todas as 2048 entradas procurando por uma tag idêntica à fornecida no endereço linear. Se achar, o acesso é feito no cache, senão, a linha é “carregada” (do cache L2).

Onde entra a associatividade de conjunto (set associativity)? Para melhorar essa pesquisa da tag alguns caches juntam mais de uma linha num conjunto de linhas, ligados (associados) à mesma tag, Ao invés de termos tag+offset, agora temos tag+way+offset. Onde way é uma indicação (um “caminho”) de uma linha em um conjunto (set) de linhas associadas a uma tag. Daí, “4 way set associative” significa que uma tag, nesse tipo de associativivdae possui 2 bits que indicam o caminho (way) para uma das 4 linhas associadas a uma tag.

O cache L1 está ligado a um processsador lógico individual e é dividido em 2 “tipos”: L1I (cache de instruções) e L1D (cache de dados). O cache L1I, em muitos processadores Intel modernos, é 4 way set assiciative, o que significa que cada tag comporta 4 linhas adjacentes ou 256 bytes. Cada linha é lidada de forma individual, mas ao alocar espaço para uma tag no cache, 4 linhas sempre serão alocadas. O cache L1D, por outro lado, é 8 way set associative. Alocação de 8 linhas por tag ou 512 bytes.

O cache L2 também costuma ser 8 way set associative e o cache L3 geralmente é chamado de full associative (ou seja, não tem o campo binário way no endereço linear – 1 tag, 1 linha).

Note que, e vou frisar isso, não é porque o cache L1D é 8 way set associative teremos o preenchimento de 8 linhas sempre que tentarmos ler/escrever um dado que não tenha uma tag no cache L1D… Apenas UMA linha é lida… mas 8 são sempre reservadas. Isso diminui, no nosso cache L1D de 128 KiB, a quantidade de tags de 2048 para 256, embora ainda tenhamos 2048 linhas disponíveis no cache.

Deu para esclarecer o que é “associatividade de conjunto” em caches?

Usando OracleDB em C (Oracle Call Interface)

Para usar o banco de dados Oracle em programas em C ou C++ é necessário linká-los contra a Oracle Call Interface (OCI) ou a OCCI (para C++)  . A versão mais atual pode ser baixada, gratuitamente, aqui. Recomendo baixar apenas a versão Basic Light e o SQL*Plus (para testar duas queries e algumas configs). As bibliotecas são fornecidas em forma de shared objects e é necessário criar links simbólicos em /usr/local/lib/ e configurar o linker com ldconfig, depois desses links criados. Isso consta na página sobre a instalação dos pacotes. Suponhamos que você resolveu descompactar os pacotes em /usr/local/lib/oracle/instantclient. A primeira coisa a fazer é criar a variável de ambiente $ORACLE_HOME apontando para esse diretório:

# echo "ORACLE_HOME='/usr/local/lib/oracle/instantclient'"\
 >> /etc/environment

Ou coloque isso no profile do usuário ou em outro lugar qualquer onde a variável seja exportada para os demais processos.

Dando uma olhada no diretório você encontrará vários arquivos com extensão .so*:

# find $ORACLE_HOME/ -type f -name '*.so*'
/usr/local/lib/oracle/instantclient/libclntshcore.so.19.1
/usr/local/lib/oracle/instantclient/libocci.so.19.1
/usr/local/lib/oracle/instantclient/liboramysql19.so
/usr/local/lib/oracle/instantclient/libsqlplus.so
/usr/local/lib/oracle/instantclient/libclntsh.so.19.1
/usr/local/lib/oracle/instantclient/libsqlplusic.so
/usr/local/lib/oracle/instantclient/libociei.so
/usr/local/lib/oracle/instantclient/libnnz19.so
/usr/local/lib/oracle/instantclient/libocijdbc19.so
/usr/local/lib/oracle/instantclient/libmql1.so
/usr/local/lib/oracle/instantclient/libipc1.so

Claro, esses arquivos terão links simbólicos em /usr/local/lib/, assim que você criá-los. Para que seu código funcione, é necessário linká-lo contra libclntsh.so e libclntshcore.so, outras libs podem ser necessárias, de acordo com as funções que você usar.

Num programa, a OCI funciona com base em uma hierarquia de objetos. O primeiro deles, que deve ser instanciado, é o environment ou OCIEnv. Isso é feito com a função OCIEnvCreate():

OCIEnv *envp;

if ( OCIEnvCreate( &envp,
                   OCI_THREADED,  // existe outras opções.
                   NULL, NULL, NULL, NULL, // Deixa a OCI
                                           // lidar com o
                                           // gerenciamento
                                           // de memória.
                   // nenhum dado extra.
                   0, NULL ) != OCI_SUCCESS )
{ ... Trata erro aqui... }

Todos os demais objetos serão, de uma forma ou de outra, atrelados a esse ambiente.

O Oracle tem várias camadas. Daí temos que instanciar um “servidor”, um “serviço”, e uma “sessão”, antes de pensarmos em enviar comandos (statements). Ou existem funções especializadas para criar/obter os ponteiros desses objetos ou usamos a função genérica OCIHandleAlloc(). No caso do objeto servidor:

OCISrv *srvp;
OCIError *errp;

if ( OCIHandleAlloc( envp, 
                     &srvp, 
                     OCI_HTYPE_SERVER ) != OCI_SUCCESS )
{ ... Trata erro aqui ... }

if ( OCIHandleAlloc( envp, 
                     &errp,
                     OCI_HTYPE_ERROR ) != OCI_SUCCESS )
{ ... Trata erro aqui ... }

if ( OCIServerAttach( srvp, 
                      errp,

                      // string de conexão (o "serviço" no
                      // tnsnames.ora).
                      connstr, strlen( connstr ),
                      OCI_DEFAULT ) != OCI_SUCCESS )
{ ... Trata erro aqui ... }

Note que precisamos criar, também, um objeto de erro. Todos os erros repostados na OCI são colocados nesse objeto. O retorno das funções devolve o status da mesma (não o “erro”). OCI_SUCCESS (0) é apenas um deles.

Uma vez que temos o server “atachado” ao ambiente, temos que criar o objeto de serviço. É ele quem conecta ao banco:

OCISvc *svcp;

if ( OCILogon2( envp, errp, &svcp,
                user, strlen( user ),
                password, strlen( password ),
                dbname, strlen( dbname ),
                OCI_DEFAULT ) != OCI_SUCCESS )
{ ... trata erro aqui... }

Note que o nome do banco é diferente do nome do serviço, anteriormente citado ao atacharmos o servidor. E a função OCILogon2() cria a instância do serviço, atrelada am ambiente, para nós. Não é necesário o uso de OCIHandleAlloc().

Nesse ponto, se tudo correu bem, já podemos enviar comandos para o banco de dados. E, surpresa das surpresas, isso é feito através de um outro objeto: OCIStmt, mas esse depende de outros objetos (OCIDescriptor e OCIBind, por exemplo). E, dependendo do tipo do campo contido no comando, teremos que usar ainda outros objetos, como o OCILobLocator, por exemplo, para o caso de queremos mexer com BLOBs.

Os objetos OCIDescriptor descrevem cada campo de retorno do comando e os atrela a duas variáveis: A que receberá o valor do tipo correspondente e um “indicador”. Esse indicador nos dirá se o campo é NULL, por exemplo. Isso tem que ser assim porque os tipos primimitivos de C não suportam NULL (a não ser que seja um ponteiro). Assim, a Oracle preferiu informar o estado do campo através desse “indicador”, à parte. Um exemplo simples. Suponha que queiramos executar um “SELECT 1 FROM DUAL” (comando comum para determinar se o banco está respondendo ou enviar “keep alives” para a sessão). Podemos fazer assim:

OCIStmt *stmtp;
OCIDefine *defp;
int value;
short int value_ind;
static const char sql[] = "select 1 "
                            "from dual";

// Prepara o comando.
if ( OCIStmtPrepare2( svcp,
                      &stmtp,
                      errp,
                      sql, sizeof sql - 1,
                      NULL, NULL,
                      OCI_NTV_SYNTAX ) != OCI_SUCCESS )
{ ... trata erro... }

// Define o retorno.
if ( OCIDefineByPos( stmtp,
                     &defp,
                     errp,
                     0,   // começa em 0.
                     &value, sizeof( value ),
                     SQLT_INT,
                     &value_ind,
                     NULL, NULL,
                     OCI_DEFAULT ) != OCI_SUCCESS )
{ ... trata erro aqui ... }

// Executa o comando.
if ( OCIStmtExecute( svcp,
                     stmtp,
                     errp,
                     0, 0, NULL, NULL,
                     OCI_DEFAULT ) != OCI_SUCCESS )
{ ... trata erro... }

printf( "$d\n", value );

// Libera o statement... Não precisaremos liberar defp
// porque ele está atrelado ao statement.
if ( OCIStmtRelease( stmtp, errp, 
                     NULL, 0, 
                     OCI_DEFAULT ) != OCI_SUCCESS )
{ ... trata erro aqui... }

Aqui não fiz questão de testar o indicador por um valor NULL no campo porque a ordem foi devolver 1.

É bom notar que a execução precisa de uma séria de parâmetros dependente do tipo de comando enviado ao banco de dados. Por exemplo, o primeiro 0 (zero) da sequência 0, 0, NULL, NULL à partir do 4º argumento de OCIStmtExecute nos diz quantas linhas deversão ser afetadas. Para “SELECTs” ele pode ser 0. Consulte a documentação da OCI.

Outra possibilidade é usarmos queries parametrizadas. Isso é feito colocando “variáveis” precedidas por : na query. Existem dois tipos, as nomeadas e as numeradas. Por exemplo: “SELECT * FROM ID=:id“. Aqui, retiramos o : do :id e usamos esse nome de parâmetro para criar objetos do tipo OCIBind, mais ou menos do mesmo jeito que fizemos com OCIDefine. Mas existem duas funções: OCIBindByPos() e OCIBindByName(). A função OCIBindByPos() funciona igualzinha a OCIDefineByPos(), onde criamos vários objetos começanco do índice 0. Mas, há um problema… Se o parâmetro for repetido no comando, qual “posição” usar? A primeira encontradda, mas para evitar essa ambiguidade, usamos OCIBindByName(), onde ao invés do índice, informamos o nome do parâmetro.

Antes de chamar OCIStmtExecute() as variáveis dos binds e seus indicadores devem ser inicializados e voilà!

Esse é o básico do uso da OCI. A documentação é extensa, cobrindo tópicos mais avançados como transações, connection pooling, etc… O documento tem mais de 1500 páginas e pode ser encontrado aqui.

PS: Para obter a próxima linha de um resultado, caso o SELECT devolva mais de uma, use a função OCIStmtFetch2().

Um contador mais “preciso”?

Vocês já viram meu contador de ciclos do homem pobre antes. Mas os processadores Intel/AMD têm dentro de si, desde o antigo Pentium, contadores de perforamnce bem mais precisos. O problema é que para lidar com eles temos que executar instruções que só estão disponíveis para o kernel (no ring 0). Como fazer?

Bem… O kernel disponibiliza uma syscall chamada perf_event_open() que nos dá, justamente, acesso aos performance counters de qualquer arquitetura de processadores que os implementem. Agora, o problema é que para usá-los precisamos obter um descritor de arquivo e usar syscalls como ioctl() e read(), E isso adicionaria overhead à medição, se a própria syscall perf_event_open não permitisse excluir esses ciclos extras. Consultando a documentação da syscall nas manpages, vemos que os membros exclude_kernel e exclude_hv, da estrutura da qual passamos o ponteiro, excluem da contagem final os ciclos gastos pelo kernel e hypervisor, respectivamente (se você estiver usando o código numa VM!).

Outra coisa é que perf_event_open não tem um wrapper ou protótipo em header files, então temos que chamar a syscall diretamente, seja usando o wrapper syscall(), seja em assembly. Eis o exemplo, pouco modificado, que acompanha a manpage:

/*
    perf.c

    Compilar com:
      cc -O2 -mtune=native -march=native -o perf perf.c
 */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/perf_event.h>
#include <asm/unistd.h>

// Necessária porque não há implementação dessa syscall
// na glibc.
static int perf_event_open( struct perf_event_attr *hw_event, 
                            pid_t pid,
                            int cpu, 
                            int group_fd, 
                            unsigned long flags )
{
  int r;
  register long g asm ("r10") = group_fd;
  register long f asm ("r8") = flags;

  __asm__ __volatile__ (
    "syscall"
      : "=a" (r) 
      : "a" (__NR_perf_event_open),
        "D" (hw_event),
        "S" (pid),
        "d" (cpu),
        "r" (g),
        "r" (f)
      : "memory" );

  return r;
}

int main( int argc, char **argv )
{
  unsigned long count;
  int fd;
  struct perf_event_attr pe = { 
    .type = PERF_TYPE_HARDWARE,
    .size = sizeof pe,
    .config = PERF_COUNT_HW_CPU_CYCLES,
    .disabled = 1,       // disabilita PMC.
    .exclude_kernel = 1, // exclui kernel.
    .exclude_hv = 1 };   // exclui hypervisor.

  // Infelizmente só pode rodar como root.
  if ( getuid() )
  {
    fputs( "ERROR: Need root priviledge.\n", stderr );
    return EXIT_FAILURE;
  }

  // Obtém o file descriptor, veja descrição
  // dos argumentos na manpage.
  if ( ( fd = perf_event_open( &pe, 0, -1, -1, 0 ) ) < 0 )
  {
    fputs( "Error opening PMC.\n", stderr );
    return EXIT_FAILURE;
  }

  // Reset e habilita o performance counter.
  ioctl( fd, PERF_EVENT_IOC_RESET, 0 );
  ioctl( fd, PERF_EVENT_IOC_ENABLE, 0 );

  // Rotina sendo medida.
  puts( "Measuring instruction count for this puts()." );

  // Desabilita, lê o performance counter 
  // e fecha o descritor.
  ioctl( fd, PERF_EVENT_IOC_DISABLE, 0 );
  read( fd, &count, sizeof count );
  close( fd );

  printf( "Used %lu cpu cycles.\n", count );

  return EXIT_SUCCESS;
}