Se eu tiver alguma variável atômica que
- é acessado com relativa pouca frequência (baixa contenção)
- é acessado de maneira uniforme e aleatória por threads/núcleos (ou seja, se o thread A grava na variável, com alta probabilidade de não ser A o próximo a acessar a variável)
então, quando A terminar suas gravações na variável, devo de alguma forma liberar explicitamente a variável do cache l1/l2/l3 do núcleo do thread A, de modo que quando algum outro thread B precisar acessar a variável algum tempo mais tarde, ele encontra uma linha de cache limpa na RAM, em vez de uma linha de cache suja pertencente a outro núcleo?
Algumas subquestões
- acessar uma linha de cache limpa da RAM é mais rápido ou mais lento do que acessar uma linha de cache suja de outro núcleo? Presumo que o primeiro seja mais rápido aqui, mas talvez não seja?
- em vez de ir até a RAM, seria suficiente liberar para o cache l3 compartilhado (e fora do l1/l2 por núcleo)?
- se seria benéfico liberar/gravar a linha de cache na RAM (ou l3), como posso fazer isso?
Além disso, que documentação/etc devo ler que cobre esse tipo de informação?
TL: DR: em x86 você deseja
cldemote
. Provavelmente não vale a pena fazer outras coisas, especialmente se o seu tópico de escritor puder estar fazendo um trabalho útil após esta loja. Ou, se isso não acontecer, e o sistema operacional não tiver outro thread para executar neste núcleo, colocar o núcleo em suspensão profunda envolverá a CPU escrevendo de volta suas linhas de cache sujas antes de desligar seus caches privados.Eu esperaria que a leitura da RAM geralmente fosse mais lenta do que uma linha suja em outro núcleo, especialmente em um sistema de soquete único. (No sistema NUMA de vários soquetes, se um núcleo remoto tiver uma cópia suja de uma linha de cache apoiada por DRAM local, isso pode mudar as coisas ou, pelo menos, tornar a DRAM menos atrasada.)
Se não existir uma instrução de write-back boa (e barata para o escritor), provavelmente é melhor não fazer nada do que ir longe demais.
A latência da DRAM primeiro deve falhar em L3, depois uma mensagem dessa fatia L3 deve chegar a um controlador de memória pela interconexão (por exemplo, barramento de anel ou malha) e, em seguida, você deve aguardar a latência do comando DRAM no barramento de memória externo. E já pode haver algumas solicitações enfileiradas no controlador de memória que o seu aguarda. Então os dados precisam voltar ao núcleo que fez a carga.
Uma linha suja em outro núcleo também envolve outra mensagem após a detecção de uma falha L3, mas para o núcleo que possui a linha. (As próprias tags L3 podem indicar isso, como nas CPUs Intel que usam L3 inclusivo por esse motivo. Caso contrário, um diretório separado atuando como um filtro de espionagem). Ele deve ser capaz de responder mais rápido que um controlador DRAM, pois basta ler os dados de seu rápido cache L2 ou L1d e enviá-los para a fatia L3. (E também diretamente ao núcleo que o queria?)
O caso ideal é um acerto no cache de último nível (normalmente L3), que protege o tráfego de coerência. Então você deseja que a linha seja removida do cache L1d/L2 privado do último núcleo para gravá-la. (Ou pelo menos gravado de volta para L3 e rebaixado para o estado Compartilhado nesses caches privados, não Exclusivo/Modificado. Portanto, uma leitura do mesmo núcleo poderia atingir L1d, precisando apenas de tráfego fora do núcleo (e RFO = Read For Ownership) se ele escreve novamente.)
Mas nem todos os ISAs têm instruções para fazer isso de forma barata (sem desacelerar muito o gravador) ou sem ir muito longe e forçar uma gravação na RAM. Uma instrução como x86
clwb
que força uma gravação na RAM, mas deixa a linha limpa em L3 depois, pode valer a pena ser considerada em alguns casos de uso, mas desperdiça largura de banda da DRAM. (Mas observe que o Skylake é implementadoclwb
comoclflushopt
; somente no Ice Lake e posteriores ele realmente mantém os dados em cache e também grava na DRAM).Se não for acessado com frequência, às vezes ele será gravado de volta em L3 apenas a partir da atividade normal no último núcleo a ser gravado (por exemplo, loop em um array), antes que qualquer outro núcleo o leia ou grave. Isso é ótimo, e qualquer coisa que expulse à força até mesmo de L3 impedirá que isso aconteça. Se a linha for acessada com frequência suficiente para permanecer normalmente quente no cache L3, você não vai querer anular isso.
Se o thread/núcleo do gravador não tiver mais nada para fazer após a gravação, você poderia imaginar acessar outras linhas de cache para tentar fazer com que a gravação importante fosse despejada pelo mecanismo pseudo-LRU normal, mas isso só valeria a pena se a carga a latência para o próximo leitor era tão importante que valia a pena perder muito tempo de CPU no thread de escrita e gerar tráfego de coerência extra para outras linhas de cache agora, para otimizar para esse tempo posterior em algum outro thread.
Relacionado:
Existe alguma maneira de escrever código de comunicação direto de núcleo a núcleo para CPU Intel? - As CPUs são muito bem otimizadas para gravação em um núcleo e leitura em outro núcleo, porque esse é um padrão comum em código real.
A barreira de memória de hardware torna a visibilidade das operações atômicas mais rápida, além de fornecer as garantias necessárias? (ou seja, quando este núcleo é confirmado para L1d ou quando invalida outros caches, eles terão que solicitar este: não, isso não torna isso mais rápido diretamente e não vale a pena fazer isso.)
x86 MESI invalida problema de latência da linha de cache - propõe que um terceiro thread leia os dados compartilhados a cada milissegundo para extrair dados do último gravador, tornando mais eficiente para um thread de alta prioridade eventualmente lê-los.
Inibição de cache da CPU (bastante centrada em x86)
Instruções sobre vários ISAs
Instrução RISC-V para gravar linha de cache suja no próximo nível de cache - não existe nenhuma para RISC-V, pelo menos não em 2020.
ARM/AArch64: Não sei, mas não ficaria surpreso se houvesse alguma coisa. Edições são bem-vindas.
Algum outro ISA com instruções interessantes de gerenciamento de cache?
x86 - nada de bom até o recente
cldemote
Armazenamentos NT: ignora o cache até L3 e despeja a linha à força, caso não estivesse anteriormente. Então isso é um desastre.
clflush
/clflushopt
- estes são despejados até a DRAM, você não quer isso. ( O oposto da dica de pré-busca de cache tem alguns números de desempenho para liberar pequenas matrizes.)clwb
- isso grava totalmente na DRAM, mas deixa os dados armazenados em cache no Ice Lake e posteriormente. ( No Skylake/Cascade Lake, na verdade, ele funciona da mesma forma queclflushopt
. Pelo menos funciona sem falhas, para que futuras bibliotecas de memória persistente possam usá-lo sem verificar o material da versão ISA.) E o commit para DRAM (possivelmente para um NV-DIMM) pode ser ordenado porsfence
, então presumivelmente o núcleo tem que rastreá-lo até o fim, ocupando espaço em suas filas?cldemote
em Tremont e Sapphire Rapids - projetado exatamente para este caso de uso : é uma dica de desempenho, como o oposto de uma pré-busca. Ele escreve de volta para L3. Ele é executado emnop
CPUs que não o suportam, uma vez que eles escolheram intencionalmente uma codificação que as CPUs existentes já executavam como um NOP anteriormente não documentado.Ao contrário
clwb
, ele não tem comportamento garantido (apenas uma dica) e nenhum pedido de ordem. até mesmo instruções de cerca, apenas wrt. armazena na mesma linha de cache. Portanto, o núcleo não precisa rastrear a solicitação depois de enviar uma mensagem pela interconexão com os dados a serem gravados em L3 (e notificando que a cópia da linha desse núcleo não está limpa).