É volátil caro?

Depois de ler o JSR-133 Cookbook para Compiler Writers sobre a implementação do volátil, especialmente a seção “Interações com instruções atômicas”, presumo que ler uma variável volátil sem atualizá-la precisa de uma barreira LoadLoad ou LoadStore. Mais abaixo na página, vejo que LoadLoad e LoadStore estão efetivamente sem operação em CPUs X86. Isso significa que as operações de leitura volátil podem ser feitas sem uma invalidação de cache explícita no x86 e a leitura de uma variável normal é tão rápida (desconsiderando as restrições de reordenação do volátil)?

Eu acredito que não entendi isso corretamente. Alguém poderia se importar em me esclarecer?

EDIT: pergunto-me se existem diferenças em ambientes multi-processador. Em sistemas com CPU única, a CPU pode olhar para seus próprios caches de threads, como John V. afirma, mas em sistemas multi-CPU deve haver alguma opção de configuração para as CPUs que isso não é suficiente e memory principal precisa ser atingida em sistemas multi cpu, certo?

PS: No meu caminho para aprender mais sobre isso, eu tropecei nos seguintes ótimos artigos, e como essa questão pode ser interessante para os outros, compartilharei meus links aqui:

  • Teoria e prática de Java: Corrigindo o Modelo de Memória Java, Parte 1 e
  • Teoria e prática de Java: Corrigindo o modelo de memory Java, parte 2

Na Intel, uma leitura volátil não contida é bastante barata. Se considerarmos o seguinte caso simples:

public static long l; public static void run() { if (l == -1) System.exit(-1); if (l == -2) System.exit(-1); } 

Usando a capacidade do Java 7 para imprimir o código de assembly, o método de execução se parece com algo como:

 # {method} 'run2' '()V' in 'Test2' # [sp+0x10] (sp of caller) 0xb396ce80: mov %eax,-0x3000(%esp) 0xb396ce87: push %ebp 0xb396ce88: sub $0x8,%esp ;*synchronization entry ; - Test2::run2@-1 (line 33) 0xb396ce8e: mov $0xffffffff,%ecx 0xb396ce93: mov $0xffffffff,%ebx 0xb396ce98: mov $0x6fa2b2f0,%esi ; {oop('Test2')} 0xb396ce9d: mov 0x150(%esi),%ebp 0xb396cea3: mov 0x154(%esi),%edi ;*getstatic l ; - Test2::run@0 (line 33) 0xb396cea9: cmp %ecx,%ebp 0xb396ceab: jne 0xb396ceaf 0xb396cead: cmp %ebx,%edi 0xb396ceaf: je 0xb396cece ;*getstatic l ; - Test2::run@14 (line 37) 0xb396ceb1: mov $0xfffffffe,%ecx 0xb396ceb6: mov $0xffffffff,%ebx 0xb396cebb: cmp %ecx,%ebp 0xb396cebd: jne 0xb396cec1 0xb396cebf: cmp %ebx,%edi 0xb396cec1: je 0xb396ceeb ;*return ; - Test2::run@28 (line 40) 0xb396cec3: add $0x8,%esp 0xb396cec6: pop %ebp 0xb396cec7: test %eax,0xb7732000 ; {poll_return} ;... lines removed 

Se você olhar para as 2 referências a getstatic, o primeiro envolve um carregamento da memory, o segundo pula a carga, pois o valor é reutilizado do (s) registrador (es) já carregado (long é de 64 bits e no meu laptop de 32 bits usa 2 registros).

Se tornarmos a variável l volátil, o conjunto resultante é diferente.

 # {method} 'run2' '()V' in 'Test2' # [sp+0x10] (sp of caller) 0xb3ab9340: mov %eax,-0x3000(%esp) 0xb3ab9347: push %ebp 0xb3ab9348: sub $0x8,%esp ;*synchronization entry ; - Test2::run2@-1 (line 32) 0xb3ab934e: mov $0xffffffff,%ecx 0xb3ab9353: mov $0xffffffff,%ebx 0xb3ab9358: mov $0x150,%ebp 0xb3ab935d: movsd 0x6fb7b2f0(%ebp),%xmm0 ; {oop('Test2')} 0xb3ab9365: movd %xmm0,%eax 0xb3ab9369: psrlq $0x20,%xmm0 0xb3ab936e: movd %xmm0,%edx ;*getstatic l ; - Test2::run@0 (line 32) 0xb3ab9372: cmp %ecx,%eax 0xb3ab9374: jne 0xb3ab9378 0xb3ab9376: cmp %ebx,%edx 0xb3ab9378: je 0xb3ab93ac 0xb3ab937a: mov $0xfffffffe,%ecx 0xb3ab937f: mov $0xffffffff,%ebx 0xb3ab9384: movsd 0x6fb7b2f0(%ebp),%xmm0 ; {oop('Test2')} 0xb3ab938c: movd %xmm0,%ebp 0xb3ab9390: psrlq $0x20,%xmm0 0xb3ab9395: movd %xmm0,%edi ;*getstatic l ; - Test2::run@14 (line 36) 0xb3ab9399: cmp %ecx,%ebp 0xb3ab939b: jne 0xb3ab939f 0xb3ab939d: cmp %ebx,%edi 0xb3ab939f: je 0xb3ab93ba ;*return ;... lines removed 

Neste caso, ambas as referências getstatic à variável l envolvem uma carga da memory, ou seja, o valor não pode ser mantido em um registrador através de múltiplas leituras voláteis. Para garantir que haja uma leitura atômica, o valor é lido da memory principal para um registrador MMX movsd 0x6fb7b2f0(%ebp),%xmm0 tornando a operação de leitura uma única instrução (do exemplo anterior, vimos que o valor de 64 bits normalmente exigiria 32 bits lê em um sistema de 32 bits).

Assim, o custo total de uma leitura volátil será aproximadamente equivalente a uma carga de memory e pode ser tão barato quanto um access de cache L1. No entanto, se outro núcleo estiver gravando na variável volátil, a linha de cache será invalidada, exigindo uma memory principal ou talvez um access de cache L3. O custo real dependerá muito da arquitetura da CPU. Mesmo entre a Intel e a AMD, os protocolos de coerência de cache são diferentes.

De um modo geral, na maioria dos processadores modernos, uma carga volátil é comparável a uma carga normal. Uma loja volátil é cerca de 1/3 do tempo de uma input / saída do monitor. Isso é visto em sistemas que são coerentes com cache.

Para responder a pergunta do OP, as gravações voláteis são caras, enquanto as leituras normalmente não são.

Isso significa que as operações de leitura volátil podem ser feitas sem uma invalidação de cache explícita no x86, e é uma leitura rápida de uma variável normal (desconsiderando as restrições de reordenação do volátil)?

Sim, às vezes, ao validar um campo, a CPU pode nem atingir a memory principal; em vez disso, espiona outros caches de thread e obtém o valor a partir daí (explicação muito geral).

No entanto, eu sugiro a sugestão de Neil de que, se você tiver um campo acessado por vários segmentos, deverá enviá-lo como um AtomicReference. Sendo um AtomicReference, ele executa aproximadamente o mesmo rendimento para leituras / gravações, mas também é mais óbvio que o campo será acessado e modificado por vários threads.

Editar para responder a edição do OP:

A coerência do cache é um protocolo complicado, mas em resumo: as CPUs compartilham uma linha de cache comum que está conectada à memory principal. Se uma CPU carregar memory e nenhuma outra CPU a tiver, a CPU assumirá que é o valor mais atualizado. Se outra CPU tentar carregar a mesma localização de memory, a CPU já carregada estará ciente disso e, na verdade, compartilhará a referência em cache à CPU solicitante – agora a CPU de solicitação tem uma cópia dessa memory em seu cache de CPU. (Nunca teve que procurar na memory principal pela referência)

Há um pouco mais de protocolo envolvido, mas isso dá uma idéia do que está acontecendo. Também para responder à sua outra pergunta, com a ausência de múltiplos processadores, as leituras / gravações voláteis podem, na verdade, ser mais rápidas do que com múltiplos processadores. Existem algumas aplicações que, na verdade, rodam mais rápido simultaneamente com uma única CPU do que múltiplas.

Nas palavras do Modelo de Memória Java (como definido para o Java 5+ no JSR 133), qualquer operação – ler ou gravar – em uma variável volatile cria um relacionamento acontece antes de qualquer outra operação na mesma variável. Isso significa que o compilador e o JIT são forçados a evitar determinadas otimizações, como reordenar instruções no encadeamento ou executar operações apenas no cache local.

Como algumas otimizações não estão disponíveis, o código resultante é necessariamente mais lento do que teria sido, embora provavelmente não seja muito.

No entanto, você não deve fazer uma variável volatile menos que você saiba que ela será acessada de vários threads fora dos blocos synchronized . Mesmo assim, você deve considerar se o volátil é a melhor escolha versus synchronized , AtomicReference e seus amigos, as classs Lock explícitas, etc.

Acessar uma variável volátil é, em muitos aspectos, semelhante a envolver o access a uma variável comum em um bloco sincronizado. Por exemplo, o access a uma variável volátil impede que a CPU reordene as instruções antes e depois do access, e isso geralmente atrasa a execução (embora eu não possa dizer quanto).

Mais genericamente, em um sistema multiprocessador não vejo como o access a uma variável volátil pode ser feito sem penalidade – deve haver alguma maneira de garantir que uma gravação no processador A seja sincronizada com uma leitura no processador B.