Adendo à estrutura e comportamento dos caches

Espero que o artigo anterior tenha te dado uma compreensão conceitual a respeito do funcionamento dos caches do processador. Digo “conceitual” porque as entranhas do processador estão devidamente escondidas de olhos curiosos, graças a proteção de propriedade industrial. Só a Intel e AMD sabem como funcionam, de fato, seus processadores por dentro.

Isso não significa que não podemos apreender alguma coisa na prática…

Se você já teve curiosidade de ver o tempo de latência e throughput das instruções executadas por seu processador, deve ter visto que instruções como MOV “gastam”, no mínimo, 0.5 clock para serem executadas. Esse mínimo ocorre justamente quando a instrução não efetua acessos à memória. Se MOV ler um DWORD, ela poderá gastar 1, 2, ou mais ciclos de clock adicionais:

mov eax,ebx   ; Gasta 0.5 ciclo (não acessa memória).
mov eax,10    ; Gasta 0.5 ciclo (não acessa memória).
mov eax,[esi] ; Gasta 0.5 ciclo se linha exclusiva.
              ; Gasta +1 ciclo se cache miss,
              ; Gasta +2 ciclos ou mais se evict ou
              ;   se shared e houver outra linha 'suja'.
mov [edi],eax ; Gasta 0.5 ciclo se linha exclusiva ou 
              ;   modificada,
              ; Gasta +1 ciclo ou mais se cache miss.
              ; Gasta +2 ciclos ou mais se shared.

Além disso, existe aquela história de “caminho”, que não é tão crítica assim… Ao invés de simplesmente multiplexar os caminhos os processadores podem (e, com certeza o fazem) paralelizar a escolha dos caminhos, fazendo de conta que uma linha seja acessada sempre integralmente. Isso quer dizer que não precisamos nos preocupar em cruzarmos a fronteira dos caminhos de uma linha de cache. Ao invés disso, temos que nos preocupar com o cruzamento de fronteira de uma linha, para evitar cache misses em potencial.

Outro adendo ao texto anterior é sobre os prefetches. As instruções tomam apenas um operando: Um ponteiro contendo o endereço linear. Com base nesse endereço o processador saberá qual “etiqueta” deve procurar no cache… O endereço pode ser qualquer um dentro da fronteira de uma linha (os 6 bits inferiores são ignorados):

mov ebx,0x600236
prefetchw [ebx]  ; Faz prefech, para escrita, da linha 
                 ; à partir do endereço linear 0x600200
                 ; até 0x60023F.

Notou que os seis bits inferiores foram zerados para obtermos o endereço inicial do bloco que será colocado na linha, cuja etiqueta será 0x18008 (0x600200 shr 6)?

As instruções de prefetching usam a mesma estrutura de mnemônicos que a instrução LEA. O único operando é uma referência à memória que usa qualquer um dos modos de endereçamento. Por exemplo:

prefetcht0 [ebp-8]
prefetchw [var+esi*4]
pretechnta [y]

Com o compilador GCC vocẽ pode usar a função __builtin_prefetch para o mesmo fim. Ela toma pelo menos um parâmetro, que é um ponteiro. Você pode passar dois outros parâmetros, opcionais: leitura/escrita e localidade:

void __builtin_prefetch(const void *ptr 
                        [, int rw [, int locality]]);

O parâmetro rw nos diz se o prefetch é para reading (valor 0) ou writing (valor 1). O parâmetro locality tem a ver com a “temporalidade” da linha e é aplicável apenas para o modo de leitura. É um valor entre 0 e 3, onde 0 é a menor prioridade e 3 a maior. A prioridade 3 executará a instrução PREFETCHNTA.

E aqui vale o aviso que, acredito, não tenha sido bem dado no artigo anterior: Prefetches explícitos devem ser evitados com todas as suas forças! Ao mudar a prioridade de linhas do cache você interfere nos algoritmos de prefetching que o processador já faz, automaticamente e, provavelmete, seu código fará um trabalho pior. As instruções de prefetching existem para serem usadas em casos excepcionais, críticos, e mesmo assim, elas são altamente dependentes de arquitetura… Um processador Haswell lida com prefetching de maneira diferente que um Broadwell ou um Nehalem…

Anúncios