Quando cálculos limitados de largura de banda de memória são executados em ambientes de memória compartilhada (por exemplo, encadeados via OpenMP, Pthreads ou TBB), há um dilema de como garantir que a memória seja distribuída corretamente na memória física , de modo que cada encadeamento acesse a memória em um barramento de memória "local". Embora as interfaces não sejam portáveis, a maioria dos sistemas operacionais possui maneiras de definir a afinidade do encadeamento (por exemplo, pthread_setaffinity_np()
em muitos sistemas POSIX, sched_setaffinity()
no Linux, SetThreadAffinityMask()
no Windows). Também existem bibliotecas, como hwloc, para determinar a hierarquia de memória, mas, infelizmente, a maioria dos sistemas operacionais ainda não fornece maneiras de definir políticas de memória NUMA. Linux é uma exceção notável, com libnumapermitindo que o aplicativo manipule a diretiva de memória e a migração de páginas com granularidade de páginas (na linha principal desde 2004, portanto amplamente disponível). Outros sistemas operacionais esperam que os usuários observem uma política implícita de "primeiro toque".
Trabalhar com uma política de "primeiro toque" significa que o chamador deve criar e distribuir threads com qualquer afinidade que planeja usar posteriormente ao gravar na memória recém-alocada. (Pouquíssimos sistemas são configurados de modo que malloc()
encontrem páginas, eles apenas prometem encontrá-las quando estão com falhas, talvez por threads diferentes.) Isso implica que a alocação usando calloc()
ou inicializando imediatamente a memória após a alocação memset()
é prejudicial, pois tenderá a falhar toda a memória no barramento de memória do núcleo executando o encadeamento de alocação, levando à pior largura de banda da memória quando a memória é acessada de vários encadeamentos. O mesmo se aplica ao new
operador C ++ , que insiste em inicializar muitas novas alocações (por exemplo,std::complex
) Algumas observações sobre esse ambiente:
- A alocação pode ser "coletiva de encadeamentos", mas agora a alocação se torna misturada no modelo de encadeamento, o que é indesejável para bibliotecas que talvez precisem interagir com clientes usando diferentes modelos de encadeamento (talvez cada um com seus próprios conjuntos de encadeamentos).
- O RAII é considerado uma parte importante do C ++ idiomático, mas parece ser prejudicial ao desempenho da memória em um ambiente NUMA. O posicionamento
new
pode ser usado com memória alocada viamalloc()
ou rotinas delibnuma
, mas isso altera o processo de alocação (que eu acredito que é necessário). - Edição: minha declaração anterior sobre o operador
new
estava incorreta, ele pode suportar vários argumentos, consulte a resposta de Chetan. Acredito que ainda exista uma preocupação em obter bibliotecas ou contêineres STL para usar afinidade especificada. Vários campos podem ser compactados e pode ser inconveniente garantir que, por exemplo, sejastd::vector
realocado com o gerenciador de contexto correto ativo. - Cada encadeamento pode alocar e danificar sua própria memória privada, mas a indexação nas regiões vizinhas é mais complicada. (Considere um produto de vetor de matriz esparsa com uma partição de linha da matriz e vetores; a indexação da parte não proprietária de x requer uma estrutura de dados mais complicada quando x não é contíguo na memória virtual.)
Alguma solução para alocação / inicialização do NUMA é considerada idiomática? Eu deixei de fora outras dicas críticas?
(Não quero que meus exemplos de C ++ impliquem ênfase nessa linguagem; no entanto, a linguagem C ++ codifica algumas decisões sobre gerenciamento de memória que uma linguagem como C não faz, portanto, tende a haver mais resistência ao sugerir que os programadores de C ++ façam essas as coisas de maneira diferente.)
fonte
Esta resposta é uma resposta a dois equívocos relacionados ao C ++ na pergunta.
Não é uma resposta direta para problemas com vários núcleos mencionados. Apenas respondendo aos comentários que classificam os programadores de C ++ como fanáticos de C ++ para que a reputação seja mantida;).
Para apontar 1. C ++ "novo" ou alocação de pilha não insiste em inicializar novos objetos, sejam PODs ou não. O construtor padrão da classe, conforme definido pelo usuário, tem essa responsabilidade. O primeiro código abaixo mostra lixo impresso se a classe é POD ou não.
Para o ponto 2. O C ++ permite sobrecarregar "new" com vários argumentos. O segundo código abaixo mostra esse caso para alocar objetos únicos. Deve dar uma idéia e talvez seja útil para a situação que você tem. operador new [] também pode ser modificado adequadamente.
// Código do ponto 1.
O compilador 11.1 da Intel mostra essa saída (que é obviamente a memória não inicializada apontada por "a").
// Código para o ponto 2.
fonte
std::complex
que são explicitamente inicializadas.std::complex
?Temos a infraestrutura de software para paralelizar a montagem em cada célula em vários núcleos usando os Threading Building Blocks (em essência, você tem uma tarefa por célula e precisa agendá-las nos processadores disponíveis - não é assim que é implementado, mas é a ideia geral). O problema é que, para a integração local, você precisa de vários objetos temporários (scratch) e precisa fornecer pelo menos o número de tarefas que podem ser executadas em paralelo. Vemos pouca aceleração, presumivelmente porque quando uma tarefa é colocada em um processador, ela pega um dos objetos temporários que normalmente estarão no cache de algum outro núcleo. Tivemos duas perguntas:
(i) Essa é realmente a razão? Quando executamos o programa no cachegrind, vejo que estou usando basicamente o mesmo número de instruções que ao executar o programa em um único encadeamento, mas o tempo total de execução acumulado em todos os encadeamentos é muito maior que o do encadeamento único. É realmente porque eu continuamente falha no cache?
(ii) Como posso descobrir onde estou, onde estão cada um dos objetos arranhados e qual objeto arranhado eu precisaria para acessar o que está quente no cache do meu núcleo atual?
Por fim, não encontramos respostas para nenhuma dessas soluções e, após alguns trabalhos, decidimos que não possuímos as ferramentas para investigar e resolver esses problemas. Eu sei como, pelo menos em princípio, resolver o problema (ii) (ou seja, usando objetos locais do encadeamento, assumindo que os encadeamentos permaneçam presos aos núcleos do processador - outra conjectura que não é trivial para testar), mas não tenho ferramentas para testar o problema (Eu).
Portanto, da nossa perspectiva, lidar com o NUMA ainda é uma questão não resolvida.
fonte
Além do hwloc, existem algumas ferramentas que podem relatar no ambiente de memória de um cluster HPC e que podem ser usadas para definir uma variedade de configurações NUMA.
Eu recomendaria o LIKWID como uma dessas ferramentas, pois evita uma abordagem baseada em código, permitindo, por exemplo, fixar um processo em um núcleo. Essa abordagem de ferramentas para abordar a configuração de memória específica da máquina ajudará a garantir a portabilidade do seu código entre os clusters.
Você pode encontrar uma breve apresentação descrevendo-a no ISC'13 " LIKWID - Lightweight Performance Tools " e os autores publicaram um artigo sobre o Arxiv " Melhores práticas para engenharia de desempenho assistida por HPM no moderno processador multicore ". Este documento descreve uma abordagem para interpretar os dados dos contadores de hardware para desenvolver código de desempenho específico para a arquitetura e a topologia de memória da sua máquina.
fonte