Pergunta

O kernel Linux usa lock; addl $0,0(%%esp) como barreira de escreva, enquanto a biblioteca RE2 usa xchgl (%0),%0 como barreira de escreva. Qual é a diferença e qual é melhor?

O x86 também requer instruções de barreira de leitura? O RE2 define sua função de barreira de leitura como um não-Op no x86 enquanto o Linux o define como qualquer um lfence ou ninguém, dependendo se o SSE2 está disponível. Quando é lfence requeridos?

Foi útil?

Solução

O "trancar; addl $ 0,0 (%% ESP)"É mais rápido, caso testem o endereço 0 do estado de bloqueio em (%% Esp). Porque adicionamos o valor 0 à variável de bloqueio e o sinalizador zero será definido como 1 se o valor da variável de bloqueio no endereço (%% ESP ) é 0.


na parte da folha de dados da Intel:

Executa uma operação serializada em todas as instruções de memória de carga que foram emitidas antes da instrução LFEnce. Esta operação em serialização garante que todas as instruções de carga que precedem no programa Ordem a instrução LFEnce é globalmente visível antes de qualquer instrução de carga que segue a instrução LFEnce seja globalmente visível.

(Nota do editor: mfence ou a locka operação de ED é a única cerca útil (após uma loja) para consistência seqüencial. lfence faz não Block storeload reordenando pelo buffer da loja.)


Por exemplo: as instruções de gravação de memória como 'mov' são atômicas (elas não precisam de prefixo de bloqueio) se houver alinhado adequadamente. Mas essa instrução é normalmente executada no cache da CPU e não será visível globalmente neste momento para todos os outros threads, porque a cerca de memória deve ser executada primeiro para fazer com que esse thread aguarde até que as lojas anteriores estejam visíveis para outros threads.


Portanto, a principal diferença entre essas duas instruções é que xchgl A instrução não terá nenhum efeito nos sinalizadores condicionais. Certamente podemos testar o estado da variável de bloqueio com LOCK CMPXCHG instrução, mas isso é ainda mais complexo do que com bloqueio Adicionar $ 0 instrução.

Outras dicas

Citando os manuais do IA32 (Vol 3A, Capítulo 8.2: Ordenação de Memória):

Em um sistema de processador único para regiões de memória definidas como cache de gravação, o modelo de ordem de memória respeita os seguintes princípios [..

  • As leituras não são reordenadas com outras leituras
  • As gravações não são reordenadas com leituras mais antigas
  • Gravações na memória não são reordenadas com outras gravações, com exceção de
    • gravações executadas com o CLFLUSH instrução
    • Streaming Stores (gravações) executadas com as instruções de movimento não-temporal ([Lista de instruções aqui])
    • Operações de string (consulte a Seção 8.2.4.1)
  • As leituras podem ser reordenadas com gravações mais antigas para locais diferentes, mas não com gravações mais antigas no mesmo local.
  • Leituras ou gravações não podem ser reordenadas com instruções de E/S, instruções bloqueadas ou instruções serializadas
  • Leia não pode passar LFENCE e MFENCE instruções
  • As gravações não podem passar SFENCE e MFENCE instruções

Nota: O "em um sistema de processador único" acima é um pouco enganador. As mesmas regras mantêm para cada processador (lógico) individualmente; O manual continua descrevendo as regras de pedidos adicionais entre vários processadores. A única parte sobre isso referente à questão é que

  • As instruções bloqueadas têm um pedido total.

Em resumo, desde que você esteja escrevendo para escrever memória (que é toda a memória que você verá, desde que não seja um driver ou programador de gráficos), a maioria das instruções x86 é quase sequencialmente consistente - a única reordenação Uma CPU X86 pode executar é reordenada posteriormente (independente) leituras para executar antes das gravações. O principal sobre as barreiras de escrita é que elas têm um lock Prefixo (implícito ou explícito), que proíbe todos reordenados e garante que as operações sejam vistas na mesma ordem por todos os processadores em um sistema multiprocessador.

Além disso, na memória de gravação, as leituras nunca são reordenadas, portanto não há necessidade de barreiras de leitura. Os processadores X86 recentes têm um modelo de consistência de memória mais fraco para streaming de lojas e memória combinada de gravação (comumente usada para a memória gráfica mapeada). É aí que os vários fence As instruções entram em jogo; Eles não são necessários para nenhum outro tipo de memória, mas alguns drivers do kernel do Linux lidam com a memória combinada de gravação, então eles apenas definiram sua barreira de leitura dessa maneira. A lista de modelo de pedido por tipo de memória está na Seção 11.3.1 no vol. 3a dos manuais do IA-32. Versão curta: LEITAS DE ALTUMAS DESCURSO, ALTIMAÇÃO DE ALTIMAÇÃO DE ARMUBIÇÃO DE ALTIMAÇÃO E ESTRADA (Seguindo as regras conforme detalhado), Memória Incatível e Incatível Forte tem fortes garantias de ordem (sem reordenação de processador, leituras/gravações são imediatamente executadas, usadas para MMIO ) e a memória combinada de gravar tem pedidos fracos (ou seja, regras de pedidos relaxados que precisam de cercas).

lock addl $0, (%esp) é um substituto para mfence, não lfence.

O caso de uso é quando você precisa bloquear a reordenação do Storeload (o único tipo que o modelo de memória forte do X86 permite), mas você não precisa de uma operação atômica RMW em uma variável compartilhada. https://preshing.com/20120515/memory-reordering-caught-in-the-act/

por exemplo, assumindo alinhado std::atomic<int> a,b:

movl   $1, a             a = 1;    Atomic for aligned a
# barrier needed here
movl   b, %eax           tmp = b;  Atomic for aligned b

Suas opções são:

  • Fazer uma loja de consistência seqüencial com xchg, por exemplo mov $1, %eax / xchg %eax, a Então você não precisa de uma barreira separada; faz parte da loja. Eu acho que essa é a opção mais eficiente na maioria dos hardware moderno; C ++ 11 compiladores que não sejam o uso do GCC xchg para lojas SEQ_CST.
  • Usar mfence como uma barreira. (GCC usa mov + mfence para lojas SEQ_CST).
  • Usar lock addl $0, (%esp) como uma barreira. Algum lockA instrução ED é uma barreira completa. O Lock Xchg tem o mesmo comportamento que o MFENCE?

    (Ou para algum outro local, mas a pilha é quase sempre privada e quente em L1D, por isso é um candidato um tanto bom. No entanto, isso pode criar uma cadeia de dependência para algo usando os dados na parte inferior da pilha.)

Você só pode usar xchg Como uma barreira, dobrando -a em uma loja, porque ele grava incondicionalmente o local da memória com um valor que não depende do valor antigo.

Quando possível, usando xchg Para uma loja SEQ-CST, provavelmente é o melhor, mesmo que também lê a partir do local compartilhado. mfence é mais lento do que o esperado nas CPUs Intel recentes (As cargas e armazenamentos são as únicas instruções que são reordenadas?), também bloqueando a execução fora da ordem de instruções não memórias independentes da mesma maneira lfence faz.

Pode até valer a pena usar lock addl $0, (%esp)/(%rsp) ao invés de mfence mesmo quando mfence está disponível, mas não experimentei as desvantagens. Usando -64(%rsp) Ou algo pode tornar menos provável de alongar uma dependência de dados de algo quente (um endereço local ou de retorno), mas isso pode tornar ferramentas como Valgrind infeliz.


lfence nunca é útil para pedidos de memória, a menos que você esteja lendo a partir de vídeo RAM (ou alguma outra região de ordem fracamente ordenada) com cargas de movntdqa.

A execução serializada fora da ordem (mas não o buffer da loja) não é útil para impedir a reordenação de Storeload (o único tipo que o modelo de memória forte do X86 permite regiões de memória normais WB (Write-Back)).

Os casos de uso do mundo real para lfence são para bloquear a execução fora de ordem de rdtsc Para um tempo, blocos de código muito curtos, ou para mitigação de espectro, bloqueando especulações através de uma ramificação condicional ou indireta.

Veja também Quando devo usar _mm_sfence _mm_lfence e _mm_mfence (minha resposta e a resposta de @beoonrope) para mais sobre o porquê lfence não é útil e quando usar cada uma das instruções de barreira. (Ou na minha, o C ++ intrínseco ao programar em C ++ em vez de ASM).

Como um aparte para as outras respostas, os desenvolvedores de hotspot descobriram que lock; addl $0,0(%%esp) Com um deslocamento zero, pode não ser ideal, em alguns processadores, ele pode Introduzir dependências de dados falsos; relacionado bug jdk.

Tocar em um local de pilha com um deslocamento diferente pode melhorar o desempenho em algumas circunstâncias.

A parte importante de lock; addl e xchgl é o lock prefixo. Está implícito para xchgl. Realmente não há diferença entre os dois. Eu veria como eles montam e escolhem o que é mais curto (em bytes), pois isso geralmente é mais rápido para operações equivalentes em x86 (daí truques como xorl eax,eax)

A presença de SSE2 provavelmente é apenas um proxy para a condição real que é finalmente uma função de cpuid. Provavelmente acontece que o SSE2 implica a existência de lfence e a disponibilidade do SSE2 foi verificada/armazenada em cache na inicialização. lfence é necessário quando estiver disponível.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top