Pergunta

Resumo:

O Memcpy parece incapaz de transferir mais de 2 GB/s no meu sistema em um aplicativo real ou de teste. O que posso fazer para obter cópias de memória para memória mais rápidas?

Detalhes completos:

Como parte de um aplicativo de captura de dados (usando algum hardware especializado), preciso copiar cerca de 3 GB/s de buffers temporários para a memória principal. Para adquirir dados, forneço ao driver de hardware uma série de buffers (2 MB cada). Os dados do Hardware DMAs para cada buffer e notifica meu programa quando cada buffer estiver cheio. Meu programa esvazia o buffer (memcpy para outro, maior bloco de RAM) e reosunha o buffer processado ao cartão a ser preenchido novamente. Estou tendo problemas com o Memcpy movendo os dados com rapidez suficiente. Parece que a cópia de memória a memória deve ser rápida o suficiente para suportar 3 GB/s no hardware em que estou executando. O Lavalys Everest me dá um resultado de referência de cópia de memória de 9337 MB/SEC, mas não posso chegar perto dessas velocidades com o Memcpy, mesmo em um programa de teste simples.

Isolei o problema de desempenho adicionando/removendo a chamada de memcpy dentro do código de processamento do buffer. Sem o memcpy, posso executar a taxa de dados completa- cerca de 3 GB/s. Com o Memcpy ativado, estou limitado a cerca de 550 MB/s (usando o compilador atual).

Para comparar o Memcpy no meu sistema, escrevi um programa de teste separado que apenas chama o Memcpy em alguns blocos de dados. (Eu publiquei o código abaixo) Eu executei isso tanto no compilador/IDE que estou usando (National Instruments CVI), bem como o Visual Studio 2010. Enquanto não estou usando o Visual Studio, estou disposto Para fazer a troca se ele produzir o desempenho necessário. No entanto, antes de se mover cegamente, eu queria ter certeza de que isso resolveria meus problemas de desempenho memcpy.

Visual C ++ 2010: 1900 Mb/s

NI CVI 2009: 550 MB/s

Embora não esteja surpreso que o CVI seja significativamente mais lento que o Visual Studio, estou surpreso que o desempenho do memcpy seja tão baixo. Embora eu não tenha certeza se isso é diretamente comparável, isso é muito menor que a largura de banda de referência do Everest. Embora eu não precise de esse nível de desempenho, é necessário um mínimo de 3 GB/s. Certamente a implementação padrão da biblioteca não pode ser tão pior do que o que o Everest está usando!

O que, se alguma coisa, posso fazer para tornar os memcpy mais rapidamente nessa situação?


Detalhes do hardware: AMD Magny Cours- 4x Octal Core 128 GB DDR3 Windows Server 2003 Enterprise x64

Programa de teste:

#include <windows.h>
#include <stdio.h>

const size_t NUM_ELEMENTS = 2*1024 * 1024;
const size_t ITERATIONS = 10000;

int main (int argc, char *argv[])
{
    LARGE_INTEGER start, stop, frequency;

    QueryPerformanceFrequency(&frequency);

    unsigned short * src = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);
    unsigned short * dest = (unsigned short *) malloc(sizeof(unsigned short) * NUM_ELEMENTS);

    for(int ctr = 0; ctr < NUM_ELEMENTS; ctr++)
    {
        src[ctr] = rand();
    }

    QueryPerformanceCounter(&start);

    for(int iter = 0; iter < ITERATIONS; iter++)
        memcpy(dest, src, NUM_ELEMENTS * sizeof(unsigned short));

    QueryPerformanceCounter(&stop);

    __int64 duration = stop.QuadPart - start.QuadPart;

    double duration_d = (double)duration / (double) frequency.QuadPart;

    double bytes_sec = (ITERATIONS * (NUM_ELEMENTS/1024/1024) * sizeof(unsigned short)) / duration_d;

    printf("Duration: %.5lfs for %d iterations, %.3lfMB/sec\n", duration_d, ITERATIONS, bytes_sec);

    free(src);
    free(dest);

    getchar();

    return 0;
}

EDIT: Se você tiver cinco minutos extras e deseja contribuir, pode executar o código acima em sua máquina e postar seu tempo como um comentário?

Foi útil?

Solução

Eu encontrei uma maneira de aumentar a velocidade nessa situação. Escrevi uma versão multithread do memcpy, dividindo a área a ser copiada entre os threads. Aqui estão alguns números de escala de desempenho para um tamanho de bloco definido, usando o mesmo código de tempo encontrado acima. Eu não tinha ideia de que o desempenho, especialmente para esse pequeno tamanho de bloco, escalaria para esses threads. Suspeito que isso tenha algo a ver com o grande número de controladores de memória (16) nesta máquina.

Performance (10000x 4MB block memcpy):

 1 thread :  1826 MB/sec
 2 threads:  3118 MB/sec
 3 threads:  4121 MB/sec
 4 threads: 10020 MB/sec
 5 threads: 12848 MB/sec
 6 threads: 14340 MB/sec
 8 threads: 17892 MB/sec
10 threads: 21781 MB/sec
12 threads: 25721 MB/sec
14 threads: 25318 MB/sec
16 threads: 19965 MB/sec
24 threads: 13158 MB/sec
32 threads: 12497 MB/sec

Não entendo o enorme salto de desempenho entre 3 e 4 threads. O que causaria um salto assim?

Incluí o código memcpy que escrevi abaixo para outros que podem ter esse mesmo problema. Observe que não há verificação de erros neste código- isso pode precisar ser adicionado para o seu aplicativo.

#define NUM_CPY_THREADS 4

HANDLE hCopyThreads[NUM_CPY_THREADS] = {0};
HANDLE hCopyStartSemaphores[NUM_CPY_THREADS] = {0};
HANDLE hCopyStopSemaphores[NUM_CPY_THREADS] = {0};
typedef struct
{
    int ct;
    void * src, * dest;
    size_t size;
} mt_cpy_t;

mt_cpy_t mtParamters[NUM_CPY_THREADS] = {0};

DWORD WINAPI thread_copy_proc(LPVOID param)
{
    mt_cpy_t * p = (mt_cpy_t * ) param;

    while(1)
    {
        WaitForSingleObject(hCopyStartSemaphores[p->ct], INFINITE);
        memcpy(p->dest, p->src, p->size);
        ReleaseSemaphore(hCopyStopSemaphores[p->ct], 1, NULL);
    }

    return 0;
}

int startCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        hCopyStartSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        hCopyStopSemaphores[ctr] = CreateSemaphore(NULL, 0, 1, NULL);
        mtParamters[ctr].ct = ctr;
        hCopyThreads[ctr] = CreateThread(0, 0, thread_copy_proc, &mtParamters[ctr], 0, NULL); 
    }

    return 0;
}

void * mt_memcpy(void * dest, void * src, size_t bytes)
{
    //set up parameters
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        mtParamters[ctr].dest = (char *) dest + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].src = (char *) src + ctr * bytes / NUM_CPY_THREADS;
        mtParamters[ctr].size = (ctr + 1) * bytes / NUM_CPY_THREADS - ctr * bytes / NUM_CPY_THREADS;
    }

    //release semaphores to start computation
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
        ReleaseSemaphore(hCopyStartSemaphores[ctr], 1, NULL);

    //wait for all threads to finish
    WaitForMultipleObjects(NUM_CPY_THREADS, hCopyStopSemaphores, TRUE, INFINITE);

    return dest;
}

int stopCopyThreads()
{
    for(int ctr = 0; ctr < NUM_CPY_THREADS; ctr++)
    {
        TerminateThread(hCopyThreads[ctr], 0);
        CloseHandle(hCopyStartSemaphores[ctr]);
        CloseHandle(hCopyStopSemaphores[ctr]);
    }
    return 0;
}

Outras dicas

Não tenho certeza se é feito no tempo de execução ou se você precisa fazer o tempo de compilação, mas deve ter SSE ou extensões semelhantes ativadas, pois a unidade vetorial geralmente pode escrever 128 bits na memória em comparação com 64 bits para a CPU.

Tentar esta implementação.

Sim, e verifique se isso Ambas A fonte e o destino estão alinhados a 128 bits. Se sua fonte e destino não estiverem alinhados, respectivamente entre si, seu memcpy () terá que fazer uma magia séria. :)

Você tem algumas barreiras para obter o desempenho da memória necessário:

  1. Largura de banda - Há um limite para a rapidez com que os dados podem passar da memória para a CPU e voltar novamente. De acordo com Este artigo da Wikipedia, 266MHz DDR3 RAM possui um limite superior de cerca de 17 GB/s. Agora, com um memcpy, você precisa reduzir pela metade para obter sua taxa de transferência máxima, pois os dados são lidos e depois gravados. Nos resultados de referência, parece que você não está executando a RAM mais rápida possível no seu sistema. Se você pode pagar, atualize a placa -mãe / RAM (e não será barato, os overclockers no Reino Unido atualmente têm 3x4 GB PC16000 a £ 400)

  2. O SO - Windows é um sistema operacional multitarefa preventivo, de modo que, com frequência, seu processo será suspenso para permitir que outros processos visam e faça coisas. Isso irá bloquear seus caches e impedir sua transferência. Na pior das hipóteses, todo o seu processo pode ser armazenado em cache no disco!

  3. A CPU -os dados que estão sendo movidos tem um longo caminho a percorrer: RAM -> Cache L2 -> Cache L1 -> CPU -> L1 -> L2 -> RAM. Pode até haver um cache L3. Se você deseja envolver a CPU, você realmente deseja carregar L2 enquanto copia L1. Infelizmente, as CPUs modernas podem percorrer um bloco de cache L1 mais rápido do que o tempo necessário para carregar o L1. A CPU possui um controlador de memória que ajuda muito nesses casos em que seus dados de streaming na CPU sequencialmente, mas você ainda terá problemas.

Obviamente, a maneira mais rápida de fazer algo é não fazê -lo. Os dados capturados podem ser gravados em qualquer lugar da RAM ou o buffer usado em um local fixo. Se você pode escrevê -lo em qualquer lugar, não precisará do memcpy. Se for corrigido, você poderia processar os dados em vigor e usar um sistema de tipo duplo de buffer? Ou seja, comece a capturar dados e, quando estiver meio cheio, comece a processar a primeira metade dos dados. Quando o buffer está cheio, comece a escrever dados capturados para o início e processe a segunda metade. Isso exige que o algoritmo possa processar os dados mais rapidamente do que a placa de captura o produz. Ele também pressupõe que os dados sejam descartados após o processamento. Efetivamente, este é um memcpy com uma transformação como parte do processo de cópia, então você tem:

load -> transform -> save
\--/                 \--/
 capture card        RAM
   buffer

ao invés de:

load -> save -> load -> transform -> save
\-----------/
memcpy from
capture card
buffer to RAM

Ou obtenha RAM mais rápido!

EDIT: Outra opção é processar os dados entre a fonte de dados e o PC - você poderia colocar um DSP / FPGA lá? O hardware personalizado sempre será mais rápido que uma CPU de uso geral.

Outro pensamento: já faz um tempo desde que eu fiz alguma coisa gráfica de alto desempenho, mas você poderia dar os dados na placa gráfica e depois dMá -los novamente? Você pode até aproveitar o CUDA para fazer parte do processamento. Isso retiraria completamente a CPU do loop de transferência de memória.

Uma coisa a estar ciente é que seu processo (e, portanto, o desempenho de memcpy()) é impactado pelo agendamento do sistema operacional de tarefas - é difícil dizer quanto de um fator isso está em seus horários, é difícil controlar o BU TIT. A operação do DMA do dispositivo não está sujeita a isso, pois não está sendo executada na CPU depois que é iniciada. Como seu aplicativo é um aplicativo em tempo real, você pode experimentar as configurações de prioridade do processo/thread do Windows, se ainda não o fizeram. Lembre -se de que você deve ter cuidado com isso, pois pode ter um impacto realmente negativo em outros processos (e a experiência do usuário na máquina).

Outra coisa a ter em mente é que a virtualização da memória do sistema operacional pode ter um impacto aqui - se as páginas de memória para a qual você copia não forem realmente apoiadas por páginas físicas de RAM, o memcpy() A operação irá culpar o sistema operacional para obter esse apoio físico no lugar. É provável que suas páginas de DMA sejam presas na memória física (já que elas precisam ser para a operação DMA), então a memória de origem para memcpy() Provavelmente não é um problema a esse respeito. Você pode considerar usar o Win32 VirtualAlloc() API para garantir que sua memória de destino para o memcpy() está comprometido (eu acho VirtualAlloc() é a API certa para isso, mas pode haver uma melhor que estou esquecendo - já faz um tempo desde que eu tive necessidade de fazer algo assim).

Finalmente, veja se você pode usar A técnica explicada por Skizz Para evitar o memcpy() No total - essa é a sua melhor aposta se os recursos permitirem.

Primeiro de tudo, você precisa verificar se a memória está alinhada em limites de 16 bytes, caso contrário, você recebe penalidades. Esta é a coisa mais importante.

Se você não precisar de uma solução compatível com padrão, pode verificar se as coisas melhoram usando alguma extensão específica do compilador, como memcpy64 (Verifique com o seu documento do compilador se houver algo disponível). Fato é isso memcpyDeve ser capaz de lidar com uma cópia de byte, mas mover 4 ou 8 bytes por vez é muito mais rápido se você não tiver essa restrição.

Novamente, é uma opção para você escrever código de montagem embutida?

Talvez você possa explicar um pouco mais sobre como está processando a área de memória maior?

Seria possível em seu aplicativo simplesmente aprovar a propriedade do buffer, em vez de copiá -lo? Isso eliminaria completamente o problema.

Ou você está usando memcpy Para mais do que apenas copiar? Talvez você esteja usando a área maior de memória para criar um fluxo seqüencial de dados do que você capturou? Especialmente se você estiver processando um personagem de cada vez, poderá se encontrar no meio do caminho. Por exemplo, pode ser possível adaptar seu código de processamento para acomodar um fluxo representado como 'uma matriz de buffers', em vez de 'uma área de memória contínua'.

Você pode escrever uma melhor implementação de memcpy usando registros SSE2. A versão no VC2010 já faz isso. Portanto, a pergunta é mais, se você estiver entregando a memória alinhada.

Talvez você possa fazer melhor do que a versão do VC 2010, mas precisa de algum entendimento, de como fazê -lo.

PS: Você pode passar o buffer para o programa de modo de usuário em uma chamada invertida, para evitar completamente a cópia.

Uma fonte que eu recomendaria que você leia é o Mplayer's fast_memcpy função. Considere também os padrões de uso esperados e observe que as CPUs modernas têm instruções especiais da loja que permitem informar à CPU se precisará ou não ler os dados que está escrevendo. Usar as instruções que indicam que você não estará lendo de volta os dados (e, portanto, não precisam ser armazenados em cache) pode ser uma grande vitória para grande memcpy operações.

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