Qual a importância do alinhamento da memória? Ainda importa?

15

Há algum tempo, pesquisei e li muito sobre o alinhamento de memória, como funciona e como usá-lo. O artigo mais relevante que encontrei no momento é este .

Mas mesmo com isso ainda tenho algumas perguntas sobre isso:

  1. Fora do sistema incorporado, geralmente temos uma grande quantidade de memória em nosso computador que torna o gerenciamento de memória muito menos crítico, sou totalmente otimista, mas agora é realmente algo que pode fazer a diferença se compararmos o mesmo programa com ou sem a sua memória reorganizada e alinhada?
  2. O alinhamento da memória tem outras vantagens? Li em algum lugar que a CPU funciona melhor / mais rápido com memória alinhada, porque isso exige menos instruções para processar (se um de vocês tiver um link para um artigo / benchmark sobre isso?), Nesse caso, a diferença é realmente significativa? Há mais vantagens do que esses dois?
  3. No link do artigo, no capítulo 5, o autor diz:

    Cuidado: em C ++, classes que parecem estruturas podem quebrar essa regra! (O que eles fazem ou não depende de como as classes base e as funções de membro virtual são implementadas e varia de acordo com o compilador.)

  4. O artigo fala principalmente sobre estruturas, mas a declaração de variáveis ​​locais também é afetada por essa necessidade?

    Você tem alguma idéia de como o alinhamento de memória funciona exatamente em C ++, pois parece ter algumas diferenças?

Esta pergunta anterior contém a palavra "alinhamento", mas não fornece respostas para as perguntas acima.

Kane
fonte
Os compiladores C ++ estão mais inclinados a fazer isso (insira o preenchimento onde for necessário ou benéfico) para você. No link que você mencionou, procure na seção 12 "Ferramentas" as coisas que você pode usar.
Rwong

Respostas:

11

Sim, o alinhamento e a organização dos seus dados podem fazer uma grande diferença no desempenho, não apenas alguns por cento, mas poucas ou muitas centenas de por cento.

Faça esse loop, duas instruções são importantes se você executar loops suficientes.

.globl ASMDELAY
ASMDELAY:
    subs r0,r0,#1
    bne ASMDELAY
    bx lr

Com e sem cache e com alinhamento com e sem lançamento de cache na previsão de ramificação, é possível variar o desempenho dessas duas instruções em uma quantidade significativa (marcações do timer):

min      max      difference
00016DDE 003E025D 003C947F

Um teste de desempenho que você pode fazer com muita facilidade. adicione ou remova nops ao redor do código em teste e faça um trabalho preciso de tempo, mova as instruções em teste ao longo de uma ampla variedade de endereços para tocar nas bordas das linhas de cache, etc.

O mesmo tipo de coisa com acessos de dados. Algumas arquiteturas reclamam de acessos desalinhados (executando uma leitura de 32 bits no endereço 0x1001, por exemplo), causando uma falha nos dados. Alguns deles você pode desativar a falha e sofrer o impacto no desempenho. Outros que permitem acessos desalinhados, você apenas obtém o desempenho.

Às vezes, são "instruções", mas na maioria das vezes são ciclos de relógio / ônibus.

Veja as implementações do memcpy no gcc para vários destinos. Digamos que você esteja copiando uma estrutura de 0x43 bytes, você pode encontrar uma implementação que copia um byte, deixando 0x42, depois copia 0x40 bytes em grandes blocos eficientes e, no último 0x2, pode ser feito como dois bytes individuais ou como uma transferência de 16 bits. O alinhamento e o destino entram em ação se os endereços de origem e destino estiverem no mesmo alinhamento, por exemplo, 0x1003 e 0x2003, então você pode fazer o byte, 0x40 em grandes blocos e depois 0x2, mas se um for 0x1002 e o outro 0x1003, obtém muito feio e muito lento.

Na maioria das vezes são ciclos de ônibus. Ou pior, o número de transferências. Pegue um processador com um barramento de dados de 64 bits de largura, como ARM, e faça uma transferência de quatro palavras (leitura ou gravação, LDM ou STM) no endereço 0x1004, que é um endereço alinhado por palavras e perfeitamente legal, mas se o barramento for 64 bits de largura, é provável que a instrução única se transforme em três transferências, neste caso, 32 bits em 0x1004, 64 bits em 0x1008 e 32 bits em 0x100A. Mas se você tivesse a mesma instrução, mas no endereço 0x1008, ele poderia fazer uma única transferência de quatro palavras no endereço 0x1008. Cada transferência tem um tempo de configuração associado. Portanto, a diferença de endereço de 0x1004 a 0x1008 por si só pode ser várias vezes mais rápida, mesmo / esp ao usar um cache e todos são hits do cache.

Falando nisso, mesmo se você fizer uma leitura de duas palavras no endereço 0x1000 vs 0x0FFC, o 0x0FFC com falhas de cache causará duas leituras de linha de cache em que 0x1000 é uma linha de cache, você terá a penalidade de ler uma linha de cache de maneira aleatória acesso (lendo mais dados do que usando), mas isso dobra. Como suas estruturas estão alinhadas ou seus dados em geral e sua frequência de acesso a esses dados, etc., podem causar problemas no cache.

Você pode acabar distribuindo seus dados de forma que, ao processar os dados, possa criar despejos, você pode ficar realmente azarado e acabar usando apenas uma fração do cache e, ao passar por ele, o próximo blob de dados colide com um blob anterior . Ao misturar seus dados ou reorganizar as funções no código-fonte, etc, você pode criar ou remover colisões, pois nem todos os caches são criados iguais, o compilador não irá ajudá-lo aqui. Até a detecção do impacto ou melhoria do desempenho é sua.

Todas as coisas que adicionamos para melhorar o desempenho, barramentos de dados mais amplos, pipelines, caches, previsão de ramificação, várias unidades / caminhos de execução, etc. Geralmente ajudarão, mas todos eles têm pontos fracos, que podem ser explorados intencionalmente ou acidentalmente. Há muito pouco que o compilador ou as bibliotecas podem fazer sobre isso, se você estiver interessado em desempenho, precisará ajustar e um dos maiores fatores de ajuste é o alinhamento do código e dos dados, não apenas os 32, 64, 128, 256 limites de bits, mas também onde as coisas são relativas umas às outras, você deseja que loops muito usados ​​ou dados reutilizados não cheguem à mesma maneira de cache, pois cada um deles quer o seu. Os compiladores podem ajudar, por exemplo, na ordenação de instruções para uma arquitetura super escalar, reorganizando as instruções que são importantes uma para a outra,

A maior supervisão é a suposição de que o processador é o gargalo. Não é verdade há uma década ou mais, alimentar o processador é o problema e é aí que problemas como o desempenho do alinhamento atingem, a troca de cache, etc. entram em jogo. Com um pouco de trabalho, mesmo no nível do código-fonte, reorganizar os dados em uma estrutura, ordenar as declarações de variável / estrutura, ordenar as funções no código-fonte e um pouco de código extra para alinhar os dados, pode melhorar o desempenho várias vezes acima ou abaixo. Mais.

old_timer
fonte
+1 se apenas no seu parágrafo final. A largura de banda da memória é o problema mais crítico para quem tenta escrever código rápido hoje, não para a contagem de instruções. E isso significa que otimizar as coisas para reduzir as falhas de cache, o que pode ser feito modificando o alinhamento em muitas circunstâncias, é extremamente importante.
Jules
Se o seu código e dados ficarem armazenados em cache e você executar ciclos / ciclos suficientes nesses dados, a instrução contará e onde as instruções estão dentro de uma linha de busca, onde as ramificações pousam no canal em relação ao que elas dependem. Mas em sistemas baseados em dram e / ou flash, você primeiro precisa se preocupar em alimentar o processador sim.
old_timer
15

Sim, o alinhamento da memória ainda é importante.

Alguns processadores, na verdade, não podem executar leituras em endereços não alinhados. Se você estiver usando esse hardware e armazenar seus números inteiros não alinhados, provavelmente precisará lê-los com duas instruções seguidas de mais algumas instruções para colocar os vários bytes nos lugares certos, para que você possa usá-los . Dados alinhados são críticos para o desempenho.

A boa notícia é que você não precisa se preocupar. Quase qualquer compilador para quase qualquer idioma produzirá código de máquina que respeite os requisitos de alinhamento do sistema de destino. Você só precisa começar a pensar sobre isso se estiver assumindo o controle direto da representação na memória de seus dados, o que não é necessário nem em um lugar tão próximo quanto antes. É uma coisa interessante a saber e absolutamente crítico para saber se você deseja entender o uso da memória de várias estruturas que está criando e como reorganizar as coisas para que sejam mais eficientes (evitando o preenchimento). Mas, a menos que você precise desse tipo de controle (e para a maioria dos sistemas não precisa), você pode passar por uma carreira inteira sem saber ou se importar com isso.

Matthew Walton
fonte
1
Em particular, o ARM não suporta acesso não alinhado. E essa é a CPU quase tudo o que o celular usa.
Jan Hudec
Observe também que o Linux emula o acesso não alinhado a algum custo de tempo de execução, mas o Windows (CE e Phone) não faz e a tentativa de acesso não alinhado simplesmente trava o aplicativo.
Jan Hudec
2
Embora isso seja verdade, observe que algumas plataformas (incluindo x86) têm requisitos de alinhamento diferentes, dependendo de quais instruções serão usadas , o que não é fácil para o compilador resolver por si próprio; portanto, às vezes, você precisa preencher para garantir certas operações (por exemplo, as instruções SSE, muitas das quais requerem alinhamento de 16 bytes) podem ser usadas para algumas operações. Além disso, a adição de preenchimento adicional para que dois itens frequentemente usados ​​juntos ocorram na mesma linha de cache (também 16 bytes) pode ter um efeito enorme no desempenho em alguns casos e também não é automatizada.
Jules
3

Sim, isso ainda importa e, em alguns algoritmos críticos de desempenho, você não pode confiar no compilador.

Vou listar apenas alguns exemplos:

  1. A partir desta resposta :

Normalmente, o microcódigo buscará a quantidade adequada de 4 bytes da memória, mas se não estiver alinhado, precisará buscar duas localizações de 4 bytes da memória e reconstruir a quantidade desejada de 4 bytes a partir dos bytes apropriados das duas localizações.

  1. O conjunto de instruções SSE requer alinhamento especial. Se não for atendido, você precisará usar funções especiais para carregar e armazenar dados na memória não alinhada. Isso significa duas instruções extras.

Se você não estiver trabalhando em algoritmos críticos de desempenho, esqueça os alinhamentos de memória. Não é realmente necessário para programação normal.

BЈовић
fonte
1

Nós tendemos a evitar situações onde isso importa. Se importa, importa. Dados não alinhados costumavam ocorrer, por exemplo, ao processar dados binários, o que parece ser evitado atualmente (as pessoas usam muito XML ou JSON).

Se você, de alguma maneira, conseguir criar uma matriz desalinhada de números inteiros, em um processador intel típico, o processamento de código dessa matriz será um pouco mais lento do que nos dados alinhados. Em um processador ARM, ele será executado um pouco mais devagar se você informar ao compilador que os dados estão desalinhados. Ele pode rodar muito, muito mais devagar ou gerar resultados incorretos, dependendo do modelo do processador e do sistema operacional, se você usar dados desalinhados sem informar o compilador.

Explicando a referência ao C ++: No C, todos os campos em uma estrutura devem ser armazenados em ordem crescente de memória. Portanto, se você possui os campos char / double / char e deseja alinhar tudo, terá um byte char, sete bytes não utilizados, oito bytes duplos, um byte char, sete bytes não utilizados. Nas estruturas C ++, é o mesmo para compatibilidade. Mas para estruturas, o compilador pode reordenar campos, portanto, você pode ter um byte char, outro byte char, seis bytes não utilizados e 8 bytes duplos. Usando 16 em vez de 24 bytes. Nas estruturas C, os desenvolvedores geralmente evitam essa situação e têm os campos em uma ordem diferente em primeiro lugar.

gnasher729
fonte
1
Dados não alinhados acontecem na memória. Programas que não possuem estruturas de dados compactadas adequadamente podem sofrer grandes penalidades de desempenho, mesmo para uma ordem de valores aparentemente inconseqüente. No código lthread, por exemplo, dois valores em uma única linha de cache causarão grandes interrupções no pipeline quando dois threads os acessarem ao mesmo tempo (ignorando os problemas de segurança do thread, é claro).
greyfade
Um compilador C ++ pode reordenar campos apenas sob certas condições, que provavelmente não serão atendidas se você não estiver ciente dessas regras. Além disso, não conheço nenhum compilador C ++ que realmente use essa liberdade.
Sjoerd
1
Eu nunca vi um compilador C reordenar campos. VI muitos inserção estofamento e alinhamento entre chars / ints, por exemplo, embora ..
PaulHK
1

Qual a importância do alinhamento da memória? Ainda importa?

Sim. Não. Depende.

Fora do sistema incorporado, geralmente temos uma grande quantidade de memória em nosso computador que torna o gerenciamento de memória muito menos crítico, sou totalmente otimista, mas agora é realmente algo que pode fazer a diferença se compararmos o mesmo programa com ou sem a sua memória reorganizada e alinhada?

Seu aplicativo terá uma área ocupada por memória menor e funcionará mais rápido se estiver alinhado corretamente. No aplicativo de desktop típico, isso não importa fora de casos raros / atípicos (como seu aplicativo sempre terminando com o mesmo gargalo de desempenho e exigindo otimizações). Ou seja, o aplicativo será menor e mais rápido se alinhado corretamente, mas, na maioria dos casos práticos, não deve afetar o usuário de uma maneira ou de outra.

O alinhamento da memória tem outras vantagens? Li em algum lugar que a CPU funciona melhor / mais rápido com memória alinhada, porque isso exige menos instruções para processar (se um de vocês tiver um link para um artigo / benchmark sobre isso?), Nesse caso, a diferença é realmente significativa? Há mais vantagens do que esses dois?

Pode ser. É algo que (possivelmente) você deve ter em mente ao escrever o código, mas na maioria dos casos isso simplesmente não deve importar (ou seja, eu ainda organizo minhas variáveis ​​de membro por área de memória e frequência de acesso - o que deve facilitar o armazenamento em cache - mas faço isso para facilidade de uso / leitura e refatoração do código, não para fins de armazenamento em cache).

Você tem alguma idéia de como o alinhamento de memória funciona exatamente em C ++, pois parece ter algumas diferenças?

Eu li sobre isso quando o material de alinhamento saiu (C ++ 11?). Não me incomodei com isso desde então (estou fazendo principalmente aplicativos de desktop e desenvolvimento de servidores de back-end atualmente).

utnapistim
fonte