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 .

Foi útil?

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 /

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top