udemy - O C++ 11 introduziu um modelo de memória padronizado. O que isso significa? E como isso afetará a programação em C++?




udemy c++ design patterns (4)

O C ++ 11 introduziu um modelo de memória padronizado, mas o que exatamente isso significa? E como isso afetará a programação em C ++?

Este artigo (de Gavin Clarke que cita Herb Sutter ) diz que,

O modelo de memória significa que o código C ++ agora tem uma biblioteca padronizada para chamar, independentemente de quem fez o compilador e em qual plataforma ele está sendo executado. Existe uma maneira padrão de controlar como os diferentes threads conversam com a memória do processador.

"Quando você está falando sobre dividir [código] em diferentes núcleos que estão no padrão, estamos falando sobre o modelo de memória. Vamos otimizá-lo sem quebrar as seguintes suposições que as pessoas vão fazer no código", disse Sutter .

Bem, eu posso memorizar este e outros parágrafos similares disponíveis on-line (como eu tive meu próprio modelo de memória desde o nascimento: P) e posso postar como uma resposta a perguntas feitas por outros, mas para ser honesto, eu não entendo exatamente isto.

Programadores C ++ costumavam desenvolver aplicativos multi-thread antes mesmo, então como isso importa se são threads POSIX, ou threads do Windows, ou encadeamentos C ++ 11? Quais são os benefícios? Eu quero entender os detalhes de baixo nível.

Eu também tenho a sensação de que o modelo de memória C ++ 11 está de alguma forma relacionado ao suporte multi-threading do C ++ 11, já que eu geralmente vejo esses dois juntos. Se for, como exatamente? Por que eles deveriam estar relacionados?

Como não sei como funcionam os componentes internos do multi-threading, e o que significa modelo de memória em geral, por favor me ajude a entender esses conceitos. :-)


Isso significa que o padrão agora define multi-threading e define o que acontece no contexto de vários threads. Claro, as pessoas usaram implementações variadas, mas isso é como perguntar por que deveríamos ter um std::string quando todos nós poderíamos estar usando uma classe de string roladas em casa.

Quando você está falando sobre threads POSIX ou threads do Windows, isso é um pouco ilusório, já que na verdade você está falando de threads x86, já que é uma função de hardware para rodar simultaneamente. O modelo de memória C ++ 0x faz garantias, se você estiver no x86, ou ARM, ou MIPS , ou qualquer outra coisa que você possa criar.


Para idiomas que não especificam um modelo de memória, você está escrevendo código para o idioma e o modelo de memória especificado pela arquitetura do processador. O processador pode optar por reordenar acessos de memória para desempenho. Portanto, se o seu programa tiver corridas de dados (uma corrida de dados é quando é possível que vários núcleos / hyper-threads acessem a mesma memória simultaneamente), seu programa não é de plataforma cruzada devido à sua dependência do modelo de memória do processador. Você pode consultar os manuais do software Intel ou AMD para descobrir como os processadores podem reorganizar os acessos à memória.

Muito importante, bloqueios (e semântica de simultaneidade com bloqueio) são normalmente implementados de forma multiplataforma ... Então, se você estiver usando bloqueios padrão em um programa multithread sem corridas de dados, então você não precisa se preocupar com modelos de memória entre plataformas .

Curiosamente, os compiladores da Microsoft para C ++ adquiriram / liberaram a semântica para o volátil, que é uma extensão do C ++ para lidar com a falta de um modelo de memória em C ++ http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx . No entanto, dado que o Windows é executado apenas em x86 / x64, isso não significa muito (os modelos de memória Intel e AMD tornam mais fácil e eficiente a implementação da semântica de aquisição / liberação em um idioma).


Se você usa mutexes para proteger todos os seus dados, realmente não precisa se preocupar. Mutexes sempre forneceram garantias suficientes de ordenação e visibilidade.

Agora, se você usou algoritmos atômicos ou livres de bloqueio, é necessário pensar no modelo de memória. O modelo de memória descreve precisamente quando os átomos fornecem garantias de ordenação e visibilidade, e fornece cercas portáteis para garantias codificadas manualmente.

Anteriormente, os atomics seriam feitos usando intrínsecos do compilador, ou alguma biblioteca de nível superior. Cercas teriam sido feitas usando instruções específicas da CPU (barreiras de memória).


Vou apenas dar a analogia com a qual eu entendo modelos de consistência de memória (ou modelos de memória, para breve). É inspirado no trabalho seminal de Leslie Lamport "Tempo, relógios e ordenação de eventos em um sistema distribuído" . A analogia é adequada e tem um significado fundamental, mas pode ser um exagero para muitas pessoas. No entanto, espero que forneça uma imagem mental (uma representação pictórica) que facilite o raciocínio sobre os modelos de consistência de memória.

Vamos ver as histórias de todas as localizações de memória em um diagrama de espaço-tempo no qual o eixo horizontal representa o espaço de endereço (ou seja, cada localização de memória é representada por um ponto nesse eixo) e o eixo vertical representa o tempo (veremos isso, em geral, não existe uma noção universal de tempo). O histórico de valores mantidos por cada localização de memória é, portanto, representado por uma coluna vertical nesse endereço de memória. Cada alteração de valor ocorre devido a um dos encadeamentos gravar um novo valor para esse local. Por uma imagem de memória , queremos dizer o agregado / combinação de valores de todos os locais de memória observáveis em um determinado momento por um segmento específico .

Citando "A Primer on Consistency Memory and Cache Coherence"

O modelo de memória intuitivo (e mais restritivo) é a consistência sequencial (SC) na qual uma execução multithread deve parecer uma intercalação das execuções sequenciais de cada thread constituinte, como se as threads fossem multiplexadas no tempo em um processador single-core.

Essa ordem de memória global pode variar de uma execução do programa para outra e pode não ser conhecida de antemão. O recurso característico de SC é o conjunto de fatias horizontais no diagrama endereço-espaço-tempo que representa planos de simultaneidade (isto é, imagens de memória). Em um determinado plano, todos os seus eventos (ou valores de memória) são simultâneos. Existe uma noção de Tempo Absoluto , na qual todos os threads concordam sobre quais valores de memória são simultâneos. No SC, a cada instante, há apenas uma imagem de memória compartilhada por todos os threads. Isso é, a cada instante de tempo, todos os processadores concordam com a imagem da memória (ou seja, o conteúdo agregado da memória). Isso não apenas implica que todos os threads exibem a mesma sequência de valores para todos os locais de memória, mas também que todos os processadores observam as mesmas combinações de valores de todas as variáveis. Isso é o mesmo que dizer que todas as operações de memória (em todos os locais de memória) são observadas na mesma ordem total por todos os threads.

Em modelos de memória relaxada, cada thread dividirá endereço-espaço-tempo à sua maneira, a única restrição é que fatias de cada thread não se cruzam porque todas as threads devem concordar com o histórico de cada localização de memória individual (é claro) , fatias de diferentes segmentos podem, e irão, se cruzar). Não existe um modo universal de fatiar (sem uma foliação privilegiada de endereço-espaço-tempo). Fatias não precisam ser planas (ou lineares). Eles podem ser curvados e isso é o que pode fazer um thread ler valores escritos por outro thread fora da ordem em que foram escritos. Histórias de locais de memória diferentes podem deslizar (ou ficar esticadas) arbitrariamente um em relação ao outro quando visualizadas por qualquer thread específico . Cada thread terá uma percepção diferente de quais eventos (ou, equivalentemente, valores de memória) são simultâneos. O conjunto de eventos (ou valores de memória) que são simultâneos a um thread não são simultâneos para outro. Assim, em um modelo de memória relaxado, todos os segmentos ainda observam o mesmo histórico (ou seja, seqüência de valores) para cada local da memória. Mas eles podem observar imagens de memória diferentes (ou seja, combinações de valores de todos os locais de memória). Mesmo se dois locais de memória diferentes forem escritos pelo mesmo thread em seqüência, os dois valores recém-gravados poderão ser observados em ordem diferente por outros threads.

[Foto da Wikipedia]

Leitores familiarizados com a Teoria Especial da Relatividade de Einstein perceberão o que eu estou aludindo. Traduzindo as palavras de Minkowski para o reino dos modelos de memória: o espaço de endereço e o tempo são sombras de endereço-espaço-tempo. Nesse caso, cada observador (ou seja, thread) projetará sombras de eventos (isto é, armazenamentos / cargas de memória) em sua própria linha de mundo (ou seja, seu eixo de tempo) e seu próprio plano de simultaneidade (seu eixo de espaço de endereço) . Os threads no modelo de memória C ++ 11 correspondem aos observadores que estão se movendo em relação um ao outro na relatividade especial. A consistência sequencial corresponde ao espaço-tempo galileu (ou seja, todos os observadores concordam com uma ordem absoluta de eventos e um senso global de simultaneidade).

A semelhança entre os modelos de memória e a relatividade especial deriva do fato de que ambos definem um conjunto de eventos parcialmente ordenados, freqüentemente chamado de conjunto causal. Alguns eventos (isto é, armazenamentos de memória) podem afetar (mas não serem afetados por) outros eventos. Um encadeamento em C ++ 11 (ou observador em física) não é mais que uma cadeia (isto é, um conjunto totalmente ordenado) de eventos (por exemplo, carregamentos de memória e armazena possivelmente endereços diferentes).

Na relatividade, alguma ordem é restaurada ao quadro aparentemente caótico de eventos parcialmente ordenados, uma vez que a única ordenação temporal com a qual todos os observadores concordam é a ordenação entre eventos “semelhantes a tempo” (isto é, aqueles eventos que são em princípio conectáveis ​​por qualquer partícula que a velocidade da luz no vácuo). Somente os eventos relacionados a tempo são ordenados invariavelmente. Tempo em Física, Craig Callender .

No modelo de memória C ++ 11, um mecanismo similar (o modelo de consistência de liberação de aquisição) é usado para estabelecer essas relações de causalidade locais .

Para fornecer uma definição de consistência de memória e uma motivação para abandonar o SC, vou citar "A Primer on Consistency Memory and Cache Coherence"

Para uma máquina de memória compartilhada, o modelo de consistência de memória define o comportamento arquiteturalmente visível de seu sistema de memória. O critério de correção para um comportamento de partições do núcleo de um único processador entre " um resultado correto " e " muitas alternativas incorretas ". Isso ocorre porque a arquitetura do processador exige que a execução de um encadeamento transforme um determinado estado de entrada em um único estado de saída bem definido, mesmo em um núcleo fora de ordem. No entanto, os modelos de consistência de memória compartilhada referem-se às cargas e aos armazenamentos de vários encadeamentos e geralmente permitem muitas execuções corretas, ao mesmo tempo em que não permitem muitos (mais) incorretos. A possibilidade de várias execuções corretas é devida ao ISA, permitindo que vários encadeamentos sejam executados simultaneamente, muitas vezes com muitos possíveis intercalamentos legais de instruções de diferentes encadeamentos.

Modelos de consistência de memória relaxada ou fraca são motivados pelo fato de que a maioria dos pedidos de memória em modelos fortes é desnecessária. Se um encadeamento atualiza dez itens de dados e, em seguida, um sinalizador de sincronização, os programadores geralmente não se importam se os itens de dados são atualizados em ordem um com o outro, mas apenas que todos os itens de dados são atualizados antes do sinalizador ser atualizado (geralmente implementado usando instruções FENCE) ). Os modelos relaxados buscam capturar essa maior flexibilidade de ordenação e preservar apenas as ordens que os programadores “ exigem ” para obter maior desempenho e correção do SC. Por exemplo, em determinadas arquiteturas, buffers de gravação FIFO são usados ​​por cada núcleo para manter os resultados de armazenamentos comprometidos (retirados) antes de gravar os resultados nos caches. Essa otimização melhora o desempenho, mas viola o SC. O buffer de gravação oculta a latência de manutenção de uma falha de armazenamento. Como as lojas são comuns, poder evitar a paralisação na maioria delas é um benefício importante. Para um processador de núcleo único, um buffer de gravação pode tornar-se invisível arquitetonicamente, garantindo que uma carga no endereço A retorne o valor do armazenamento mais recente para A, mesmo se um ou mais armazenamentos para A estiverem no buffer de gravação. Isso normalmente é feito ignorando o valor do armazenamento mais recente para A para o carregamento de A, onde “mais recente” é determinado pela ordem do programa ou por uma carga de A se um armazenamento para A estiver no buffer de gravação . Quando vários núcleos são usados, cada um terá seu próprio buffer de gravação ignorado. Sem buffers de gravação, o hardware é SC, mas com buffers de gravação, isso não ocorre, tornando os buffers de gravação visíveis arquiteturalmente em um processador multicore.

O reordenamento de armazenamento de loja pode acontecer se um núcleo tiver um buffer de gravação não-FIFO que permita que as lojas saiam em uma ordem diferente da ordem em que entraram. Isso pode ocorrer se a primeira loja erra no cache enquanto o segundo atinge ou se o segundo armazenamento pode coalescer com um armazenamento anterior (ou seja, antes do primeiro armazenamento). O reordenamento de carga-carga também pode acontecer em núcleos agendados dinamicamente que executam instruções fora da ordem do programa. Isso pode se comportar da mesma forma que reordenar lojas em outro núcleo (você pode criar um exemplo de entrelaçamento entre dois segmentos?). Reordenar um carregamento anterior com um armazenamento posterior (um reordenamento do armazenamento de carga) pode causar muitos comportamentos incorretos, como carregar um valor depois de liberar o bloqueio que o protege (se o armazenamento for a operação de desbloqueio). Observe que os reordenamentos de carga de loja também podem surgir devido ao bypass local no buffer de gravação FIFO comumente implementado, mesmo com um núcleo que executa todas as instruções na ordem do programa.

Como a coerência do cache e a consistência da memória são algumas vezes confusas, é instrutivo ter essa citação:

Ao contrário da consistência, a coerência do cache não é visível nem necessária para o software. A coerência procura tornar os caches de um sistema de memória compartilhada tão funcionalmente invisíveis quanto os caches em um sistema de núcleo único. A coerência correta garante que um programador não possa determinar se e onde um sistema possui caches, analisando os resultados de cargas e armazenamentos. Isso ocorre porque a coerência correta garante que os caches nunca ativem um comportamento funcional novo ou diferente (os programadores ainda podem inferir a provável estrutura do cache usando informações de tempo ). O principal objetivo dos protocolos de coerência de cache é manter a invariante de um único gravador e vários leitores (SWMR) para cada local de memória. Uma distinção importante entre coerência e consistência é que a coerência é especificada em uma base de localização por memória , enquanto a consistência é especificada com relação a todos os locais de memória.

Continuando com nossa imagem mental, a invariante SWMR corresponde à exigência física de que haja no máximo uma partícula localizada em qualquer local, mas pode haver um número ilimitado de observadores de qualquer local.





memory-model