Em um código paralelo OpenMP, haveria algum benefício para o memset ser executado em paralelo?

Eu tenho blocos de memory que podem ser bastante grandes (maiores que o cache L2) e, às vezes, preciso configurá-los para todo o zero. memset é bom em um código serial, mas e o código paralelo? Alguém experimentou se chamar o memset de threads concorrentes na verdade acelera as coisas para grandes arrays? Ou até mesmo usando paralelos openmp simples for loops?

As pessoas em HPC costumam dizer que um segmento geralmente não é suficiente para saturar um único link de memory, o mesmo geralmente sendo verdade também para links de rede. Aqui está um memsetter habilitado OpenMP rápido e sujo que eu escrevi para você que preenche com zeros duas vezes 2 GiB de memory. E aqui estão os resultados usando o GCC 4.7 com diferentes números de threads em diferentes arquiteturas (valores máximos de várias execuções relatadas):

GCC 4.7, código compilado com -O3 -mtune=native -fopenmp :

Intel Xeon X7350 com quatro sockets – CPU quad-core pré-Nehalem com controlador de memory separado e barramento frontal

soquete único

 threads 1st touch rewrite 1 1452.223 MB/s 3279.745 MB/s 2 1541.130 MB/s 3227.216 MB/s 3 1502.889 MB/s 3215.992 MB/s 4 1468.931 MB/s 3201.481 MB/s 

(O primeiro toque é lento, pois a equipe de threads está sendo criada a partir do zero e o sistema operacional está mapeando páginas físicas para o espaço de endereço virtual reservado por malloc(3) )

Um thread já satura a largura de banda de memory de um único link < -> NB de CPU. (NB = Ponte Norte)

1 thread por soquete

 threads 1st touch rewrite 1 1455.603 MB/s 3273.959 MB/s 2 2824.883 MB/s 5346.416 MB/s 3 3979.515 MB/s 5301.140 MB/s 4 4128.784 MB/s 5296.082 MB/s 

Dois encadeamentos são necessários para saturar a largura de banda de memory total do link de memory NB < ->.

Octo-socket Intel Xeon X7550 – sistema NUMA de 8 vias com CPUs octo-core (CMT desativado)

soquete único

 threads 1st touch rewrite 1 1469.897 MB/s 3435.087 MB/s 2 2801.953 MB/s 6527.076 MB/s 3 3805.691 MB/s 9297.412 MB/s 4 4647.067 MB/s 10816.266 MB/s 5 5159.968 MB/s 11220.991 MB/s 6 5330.690 MB/s 11227.760 MB/s 

Pelo menos 5 threads são necessários para saturar a largura de banda de um link de memory.

1 thread por soquete

 threads 1st touch rewrite 1 1460.012 MB/s 3436.950 MB/s 2 2928.678 MB/s 6866.857 MB/s 3 4408.359 MB/s 10301.129 MB/s 4 5859.548 MB/s 13712.755 MB/s 5 7276.209 MB/s 16940.793 MB/s 6 8760.900 MB/s 20252.937 MB/s 

A largura de banda escala quase linearmente com o número de threads. Com base nas observações de soquete único, pode-se dizer que pelo menos 40 threads distribuídos como 5 threads por soquete seriam necessários para saturar todos os oito links de memory.

O problema básico em sistemas NUMA é a política de memory de primeiro toque – a memory é alocada no nó NUMA, onde o primeiro thread a tocar um endereço virtual em uma página específica é executado. A fixação de threads (binding a núcleos de CPU específicos) é essencial em tais sistemas, pois a migration de thread leva ao access remoto, que é mais lento. O suporte para pinnig está disponível na maioria dos tempos de execução do OpenMP. O GCC com sua libgomp possui a variável de ambiente GOMP_CPU_AFFINITY , a Intel possui a variável de ambiente KMP_AFFINITY , etc. Além disso, o OpenMP 4.0 introduziu o conceito de locais neutro do fornecedor.

Edit: Para completar, aqui estão os resultados da execução do código com uma matriz de 1 GiB no MacBook Air com Intel Core i5-2557M (CPU Sandy Bridge dual-core com HT e QPI). Compilador é GCC 4.2.1 (compilation Apple LLVM)

 threads 1st touch rewrite 1 2257.699 MB/s 7659.678 MB/s 2 3282.500 MB/s 8157.528 MB/s 3 4109.371 MB/s 8157.335 MB/s 4 4591.780 MB/s 8141.439 MB/s 

Por que esta alta velocidade com até mesmo um único segmento? Uma pequena exploração com o gdb mostra que memset(buf, 0, len) é traduzido pelo compilador OS X para bzero(buf, len) e que uma versão bzero$VARIANT$sse42 habilitada para SSE4.2 com o nome de bzero$VARIANT$sse42 é fornecida por libc.dylib e usado em tempo de execução. Ele usa a instrução MOVDQA para zero 16 bytes de memory de uma só vez. É por isso que, mesmo com um thread, a largura de banda da memory está quase saturada. Uma versão habilitada para AVX de VMOVDQA usando o VMOVDQA pode zerar 32 bytes de uma vez e provavelmente saturar o link da memory.

A mensagem importante aqui é que às vezes a vetorização e o multithreading não são ortogonais para acelerar a operação.

Bem, há sempre o cache L3 …

No entanto, é muito provável que isso já esteja vinculado à largura de banda da memory principal; adicionar mais paralelismo é improvável que melhore as coisas.