A CPU (seu controlador de memória especificamente) pode tirar proveito do fato de que a memória não está mutada
A vantagem é que esse fato evita que o compilador use as instruções do membar quando os dados são acessados.
Uma barreira de memória, também conhecida como membar, cerca de memória ou instrução de cerca, é um tipo de instrução de barreira que faz com que uma unidade central de processamento (CPU) ou compilador imponha uma restrição de ordem nas operações de memória emitidas antes e depois da instrução de barreira. Isso normalmente significa que certas operações são garantidas para serem realizadas antes da barreira e outras depois.
Barreiras de memória são necessárias porque a maioria das CPUs modernas emprega otimizações de desempenho que podem resultar em execução fora de ordem. Essa reordenação das operações de memória (cargas e armazenamentos) normalmente passa despercebida em um único encadeamento de execução, mas pode causar comportamento imprevisível em programas simultâneos e drivers de dispositivo, a menos que seja cuidadosamente controlado ...
Veja, quando os dados são acessados a partir de diferentes threads, na CPU com vários núcleos, é o seguinte: threads diferentes são executados em núcleos diferentes, cada um usando seu próprio cache (local ao núcleo) - uma cópia de algum cache global.
Se os dados são mutáveis e o programador precisa que ele seja consistente entre diferentes threads, é necessário tomar medidas para garantir a consistência. Para programador, isso significa usar construções de sincronização quando eles acessam (por exemplo, ler) dados em um encadeamento específico.
Para o compilador, a construção de sincronização no código significa que ele precisa inserir uma instrução membar para garantir que as alterações feitas na cópia dos dados em um dos núcleos sejam propagadas adequadamente ("publicadas"), para garantir que os caches em outros núcleos tenha a mesma cópia (atualizada).
Um pouco simplificador, veja a nota abaixo , eis o que acontece no processador multi-core para membar:
- Todos os núcleos param o processamento - para evitar a gravação acidental no cache.
- Todas as atualizações feitas nos caches locais são gravadas novamente no global - para garantir que o cache global contenha os dados mais recentes. Isso leva algum tempo.
- Os dados atualizados são gravados de volta do cache global para os locais - para garantir que os caches locais contenham os dados mais recentes. Isso leva algum tempo.
- Todos os núcleos retomam a execução.
Veja bem, todos os núcleos não estão fazendo nada enquanto os dados estão sendo copiados entre os caches globais e locais . Isso é necessário para garantir que os dados mutáveis sejam sincronizados corretamente (thread-safe). Se houver 4 núcleos, todos os 4 param e esperam enquanto os caches estão sendo sincronizados. Se houver 8, todos os 8 param. Se houver 16 ... bem, você tem 15 núcleos que não fazem exatamente nada enquanto esperam pelo que é necessário fazer em um deles.
Agora, vamos ver o que acontece quando os dados são imutáveis? Não importa qual thread acessa, é garantido o mesmo. Para programador, isso significa que não há necessidade de inserir construções de sincronização quando eles acessam dados (lidos) em um encadeamento específico.
Para o compilador, isso significa que não há necessidade de inserir uma instrução membar .
Como resultado, o acesso aos dados não precisa parar núcleos e aguardar enquanto os dados estão sendo gravados entre caches globais e locais. Essa é uma vantagem do fato de a memória não estar mutada .
Observe que a explicação um pouco simplificadora acima elimina alguns efeitos negativos mais complicados de os dados serem mutáveis, por exemplo, no pipelining . Para garantir a solicitação necessária, a CPU precisa invalidar as linhas de controle afetadas pelas alterações de dados - isso é mais uma penalidade no desempenho. Se isso for implementado através da invalidação direta (e, portanto, confiável) de todos os pipelines, o efeito negativo será amplificado.