diferenças de desempenho memcpy entre 32 e 64 processos de bit
Pergunta
Temos máquinas Core2 (Dell T5400) com XP64.
Observamos que ao executar processos de 32 bits, o desempenho de memcpy é da ordem de 1.2GByte / s; no entanto memcpy em um processo de 64 bits alcança cerca 2.2GByte / s (ou 2.4GByte / s com memcpy da Intel compilador CRT). Enquanto o A reação inicial pode ser apenas para explicar este longe como devido aos registros mais abrangentes disponíveis no código de 64 bits, observamos que o nosso próprio memcpy-like código de montagem SSE (que deve ser usando 128 bits largura de carga-lojas independentemente de 32/64-bitness de o processo) demonstra limites superiores semelhantes sobre a largura de banda de cópia alcança.
A minha pergunta é, qual é essa diferença, na verdade, devido a ? Fazer processos de 32 bits tem que saltar através alguns aros WOW64 extra para obter a RAM? É algo a ver com TLBs ou prefetchers ou ... o quê?
Obrigado por qualquer insight.
Também levantado em fóruns Intel .
Solução
É claro, você realmente precisa de olhar para as instruções de máquina reais que estão sendo executados dentro do loop mais interna do memcpy, por entrar no código de máquina com um depurador. Tudo o resto é apenas especulação.
Meu quess é que ele provavelmente não tem nada a ver com 32-bit contra 64-bit per se; meu palpite é que a rotina mais rápido biblioteca foi escrito usando lojas SSE não-temporais.
Se o loop interno contém qualquer variação das instruções de carga-armazenamento convencionais, então a memória de destino deve ser lido no cache da máquina, modificado, e escrito de volta para fora. Desde que leitura é totalmente desnecessário - os bits que está sendo lido são substituídas imediatamente - você pode economizar a metade da largura de banda da memória usando as instruções de "não-temporais" de gravação, que ignoram os caches. Dessa forma, a memória de destino é escrito apenas fazer uma viagem só de ida para a memória em vez de uma ida e volta.
Eu não sei biblioteca CRT do compilador Intel, então isso é apenas um palpite. Não há nenhuma razão específica para que o 32-bit libCRT não pode fazer a mesma coisa, mas a aceleração você cita é no estádio do que eu esperaria apenas convertendo as instruções movdqa para movnt ...
Desde memcpy não está fazendo todos os cálculos, é sempre limitado por quão rápido você pode ler e memória escrita.
Outras dicas
Eu acho o seguinte pode explicá-lo:
Para copiar dados da memória para um registo e de volta à memória, você fazer
mov eax, [address]
mov [address2], eax
Este move-se 32 bits (4 bytes) de endereço para endereço2. O mesmo acontece com o modo de bit de 64 bits em 64
mov rax, [address]
mov [address2], rax
Isso move 64 bits, 2 bytes, a partir do endereço de address2. "Mov" em si, independentemente de se tratar de 64 bit ou 32 bit tem uma latência de 0,5 e um caudal de 0,5 de acordo com as especificações da Intel. Latência é quantos ciclos de clock a instrução leva para viajar através do gasoduto ea taxa de transferência é o tempo que a CPU tem que esperar antes de aceitar a mesma instrução novamente. Como você pode ver, ele pode fazer dois mov é por ciclo de clock, no entanto, tem que esperar meio ciclo de clock entre duas mov de, assim, ele pode efetivamente só fazer uma mov por ciclo de clock (ou estou errado aqui e interpretar mal os termos? consulte PDF aqui para detalhes).
É claro que um mov reg, mem
pode ser maior do que 0,5 ciclos, dependendo se os dados estão no 1º ou cache de nível 2, ou não no cache em tudo e necessidades para ser agarrado a partir da memória. No entanto, o tempo de latência de acima ignora este fato (como o PDF afirma I ligada acima), ele assume todos os dados necessários para o mov estão já presentes (caso contrário, a latência vai aumentar em quanto tempo leva para buscar os dados de onde quer que seja agora - isso pode ser de vários ciclos de relógio e é completamente independente do comando a ser executado diz o PDF na página 482 / C-30)
O que é interessante, se o mov é de 32 ou 64 bits não desempenha nenhum papel. Isso significa que, a menos que a largura de banda de memória se torna o fator limitante, mov de 64 bits são igualmente rápida de 32 bit mov de, e uma vez que leva apenas metade como muitos mov de mover a mesma quantidade de dados de A para B quando se usa 64 bits, a lata de transferência (em teoria) ser duas vezes mais alta (o fato de que não é é provavelmente porque a memória não é rápido ilimitado).
Ok, agora você pensa quando se usa os registros maiores SSE, você deve chegar mais rápido de transferência, certo? AFAIK os registos XMM não são 256, 128, mas pouco ampla, Aliás ( referência na Wikipedia ). No entanto, você já pensou em latência e taxa de transferência? Ou os dados que você deseja mover é de 128 bits alinhados ou não. Dependendo disso, você quer movê-lo usando
movdqa xmm1, [address]
movdqa [address2], xmm1
ou se não alinhados
movdqu xmm1, [address]
movdqu [address2], xmm1
Bem, movdqa / movdqu tem uma latência de 1 e uma taxa de transferência de 1. Assim, as instruções demorar o dobro do tempo para ser executado e o tempo de espera após as instruções é o dobro do tempo como um mov normal.
E outra coisa não temos sequer levado em conta é o fato de que a CPU realmente divide instruções em micro-ops e pode executar estes em paralelo. Agora ele começa a ficar realmente complicado ... mesmo muito complicado para mim.
De qualquer forma, eu sei que a partir de dados experiência de carregamento de / para registradores XMM é muito mais lento do que o carregamento de dados de / para registros normais, assim que sua idéia para acelerar a transferência usando registros XMM estava condenado desde o primeiro segundo. Na verdade, estou surpreso que no final o memmove SSE não é muito mais lento do que o normal.
Eu finalmente chegou ao fundo desta (e Morre na resposta de Sente estava no caminho certo, graças)
No abaixo, dst e src são 512 MBytes std :: vector. Estou usando o compilador Intel 10.1.029 e CRT.
Em 64 bits tanto
memcpy(&dst[0],&src[0],dst.size())
e
memcpy(&dst[0],&src[0],N)
, onde N é declarado anteriormente const size_t N=512*(1<<20);
chamada
__intel_fast_memcpy
a maior parte do qual consiste em:
000000014004ED80 lea rcx,[rcx+40h]
000000014004ED84 lea rdx,[rdx+40h]
000000014004ED88 lea r8,[r8-40h]
000000014004ED8C prefetchnta [rdx+180h]
000000014004ED93 movdqu xmm0,xmmword ptr [rdx-40h]
000000014004ED98 movdqu xmm1,xmmword ptr [rdx-30h]
000000014004ED9D cmp r8,40h
000000014004EDA1 movntdq xmmword ptr [rcx-40h],xmm0
000000014004EDA6 movntdq xmmword ptr [rcx-30h],xmm1
000000014004EDAB movdqu xmm2,xmmword ptr [rdx-20h]
000000014004EDB0 movdqu xmm3,xmmword ptr [rdx-10h]
000000014004EDB5 movntdq xmmword ptr [rcx-20h],xmm2
000000014004EDBA movntdq xmmword ptr [rcx-10h],xmm3
000000014004EDBF jge 000000014004ED80
e é executado em -2200 MByte / s.
Mas em 32bit
memcpy(&dst[0],&src[0],dst.size())
chamadas
__intel_fast_memcpy
a maior parte dos quais é constituído por
004447A0 sub ecx,80h
004447A6 movdqa xmm0,xmmword ptr [esi]
004447AA movdqa xmm1,xmmword ptr [esi+10h]
004447AF movdqa xmmword ptr [edx],xmm0
004447B3 movdqa xmmword ptr [edx+10h],xmm1
004447B8 movdqa xmm2,xmmword ptr [esi+20h]
004447BD movdqa xmm3,xmmword ptr [esi+30h]
004447C2 movdqa xmmword ptr [edx+20h],xmm2
004447C7 movdqa xmmword ptr [edx+30h],xmm3
004447CC movdqa xmm4,xmmword ptr [esi+40h]
004447D1 movdqa xmm5,xmmword ptr [esi+50h]
004447D6 movdqa xmmword ptr [edx+40h],xmm4
004447DB movdqa xmmword ptr [edx+50h],xmm5
004447E0 movdqa xmm6,xmmword ptr [esi+60h]
004447E5 movdqa xmm7,xmmword ptr [esi+70h]
004447EA add esi,80h
004447F0 movdqa xmmword ptr [edx+60h],xmm6
004447F5 movdqa xmmword ptr [edx+70h],xmm7
004447FA add edx,80h
00444800 cmp ecx,80h
00444806 jge 004447A0
e é executado em ~ 1.350 MByte / s só.
NO ENTANTO
memcpy(&dst[0],&src[0],N)
, onde N é declarado anteriormente compila const size_t N=512*(1<<20);
(em 32 bits) para uma chamada direta para um
__intel_VEC_memcpy
a maior parte dos quais é constituído por
0043FF40 movdqa xmm0,xmmword ptr [esi]
0043FF44 movdqa xmm1,xmmword ptr [esi+10h]
0043FF49 movdqa xmm2,xmmword ptr [esi+20h]
0043FF4E movdqa xmm3,xmmword ptr [esi+30h]
0043FF53 movntdq xmmword ptr [edi],xmm0
0043FF57 movntdq xmmword ptr [edi+10h],xmm1
0043FF5C movntdq xmmword ptr [edi+20h],xmm2
0043FF61 movntdq xmmword ptr [edi+30h],xmm3
0043FF66 movdqa xmm4,xmmword ptr [esi+40h]
0043FF6B movdqa xmm5,xmmword ptr [esi+50h]
0043FF70 movdqa xmm6,xmmword ptr [esi+60h]
0043FF75 movdqa xmm7,xmmword ptr [esi+70h]
0043FF7A movntdq xmmword ptr [edi+40h],xmm4
0043FF7F movntdq xmmword ptr [edi+50h],xmm5
0043FF84 movntdq xmmword ptr [edi+60h],xmm6
0043FF89 movntdq xmmword ptr [edi+70h],xmm7
0043FF8E lea esi,[esi+80h]
0043FF94 lea edi,[edi+80h]
0043FF9A dec ecx
0043FF9B jne ___intel_VEC_memcpy+244h (43FF40h)
e é executado em ~ 2100MByte / s (e provando 32bit não é de alguma forma largura de banda limitada).
Eu retiro minha afirmação de que meus próprios memcpy-como SSE sofre código de um
semelhante ~ 1300 Mbytes / limite em 32 bits constrói; Agora eu não tenho quaisquer problemas
ficando> 2GByte / s em 32 ou 64 bits; o truque (como os resultados anteriores sugerem)
é usar non-temporal lojas ( "Streaming") (por exemplo _mm_stream_ps
intrínseca).
Parece um pouco estranho que o 32bit "dst.size()
" memcpy não acabou
chamar a versão mais rápida "movnt
" (se você entrar em memcpy não é o mais
quantidade incrível de verificação CPUID
e lógica heurística por exemplo comparando o número
de bytes a ser copiado com o tamanho do cache etc antes de entrar em qualquer lugar perto de sua
dados reais), mas pelo menos eu entendo o comportamento observado agora (e é
não SysWow64 ou H / W relacionada).
Meu off-the-cuff palpite é que os processos de 64 bits está usando o tamanho da memória do processador nativo de 64 bits, que otimiza o uso do barramento de memória.
Obrigado pelo feedback positivo! Eu acho que posso parte explicar o que está acontecendo aqui.
Usando os estabelecimentos não temporais para memcpy é definitivamente o jejum se você só está cronometrando a chamada memcpy.
Por outro lado, se você está de benchmarking um aplicativo, as lojas movdqa têm o benefício que eles deixam a memória destino em cache. Ou pelo menos a parte dela que se encaixa em cache.
Então, se você está projetando uma biblioteca de tempo de execução e se você pode assumir que o aplicativo que chamado memcpy vai usar tamponar o destino imediatamente após a chamada memcpy, então você vai querer fornecer a versão movdqa. Isso otimiza efetivamente a viagem de volta de memória na CPU que se seguiria a versão movntdq, e todas as instruções a seguir a chamada irá correr mais rápido.
Mas, por outro lado, se o buffer de destino é grande em comparação com o cache do processador, que a otimização não funciona e a versão movntdq lhe daria mais rápido benchmarks de aplicação.
Assim, a idéia memcpy teria várias versões sob o capô. Quando o buffer de destino é pequeno em comparação com o cache do processador, o uso movdqa, caso contrário, então o buffer de destino é grande em comparação com o cache do processador, o uso movntdq. Parece que este é o que está acontecendo na biblioteca de 32 bits.
É claro, nada disso tem nada a ver com as diferenças entre 32-bit e 64-bit.
A minha conjectura é que a biblioteca de 64 bits só não é tão maduro. Os desenvolvedores simplesmente não ter chegado a cerca de fornecer tanto rotinas em que a versão de biblioteca ainda.
Eu não tenho uma referência na minha frente, então eu não tenho absolutamente positivo sobre as temporizações / instruções, mas ainda posso dar a teoria. Se você estiver fazendo um movimento de memória no modo de 32 bits, você vai fazer algo como um "movsd rep", que move um valor de 32 bits único a cada ciclo de clock. Sob o modo de 64 bits, você pode fazer uma "movsq rep", que faz um único 64-bit mover cada ciclo de clock. Essa instrução não está disponível para código de 32 bits, então você estaria fazendo 2 x movsd rep (a 1 ciclo de uma peça) para a metade a velocidade de execução.
muito simplificada, ignorando todas as questões largura de banda / alinhamento da memória, etc, mas isso é onde tudo começa ...
Aqui está um exemplo de uma rotina memcpy voltado especificamente para a arquitetura de 64 bits.
void uint8copy(void *dest, void *src, size_t n){
uint64_t * ss = (uint64_t)src;
uint64_t * dd = (uint64_t)dest;
n = n * sizeof(uint8_t)/sizeof(uint64_t);
while(n--)
*dd++ = *ss++;
}//end uint8copy()
O artigo completo está aqui: http://www.godlikemouse.com/2008/03/04/ optimizando-memcpy-rotinas /