web-dev-qa-db-fra.com

Comment augmenter les performances de memcpy

Sommaire:

memcpy semble incapable de transférer plus de 2 Go/s sur mon système dans une application réelle ou de test. Que puis-je faire pour obtenir des copies mémoire à mémoire plus rapides?

Tous les détails:

Dans le cadre d'une application de capture de données (en utilisant du matériel spécialisé), j'ai besoin de copier environ 3 Go/s des tampons temporaires dans la mémoire principale. Pour acquérir des données, je fournis au pilote matériel une série de tampons (2 Mo chacun). Le DMA matériel donne des données à chaque tampon, puis informe mon programme lorsque chaque tampon est plein. Mon programme vide le tampon (memcpy vers un autre bloc de RAM plus grand) et replace le tampon traité sur la carte à remplir à nouveau. J'ai des problèmes avec memcpy pour déplacer les données assez rapidement. Il semble que la copie de mémoire à mémoire devrait être suffisamment rapide pour prendre en charge 3 Go/s sur le matériel sur lequel j'exécute. Lavalys EVEREST me donne un résultat de référence de copie de mémoire de 9337 Mo/sec, mais je ne peux pas me rapprocher de ces vitesses avec memcpy, même dans un programme de test simple.

J'ai isolé le problème de performances en ajoutant/supprimant l'appel memcpy dans le code de traitement du tampon. Sans memcpy, je peux exécuter un débit de données complet - environ 3 Go/sec. Avec le memcpy activé, je suis limité à environ 550 Mo/s (en utilisant le compilateur actuel).

Afin de comparer memcpy sur mon système, j'ai écrit un programme de test séparé qui appelle simplement memcpy sur certains blocs de données. (J'ai posté le code ci-dessous) J'ai exécuté ceci à la fois dans le compilateur/IDE que j'utilise (National Instruments CVI) ainsi que Visual Studio 2010. Bien que je n'utilise pas actuellement Visual Studio, je suis prêt pour effectuer le changement s'il donnera les performances nécessaires. Cependant, avant d'avancer aveuglément, je voulais m'assurer que cela résoudrait mes problèmes de performances de memcpy.

Visual C++ 2010: 1900 Mo/s

NI CVI 2009: 550 Mo/sec

Bien que je ne sois pas surpris que CVI soit considérablement plus lent que Visual Studio, je suis surpris que les performances de memcpy soient si faibles. Bien que je ne sois pas sûr que ce soit directement comparable, c'est beaucoup plus bas que la bande passante de référence EVEREST. Bien que je n'aie pas besoin de ce niveau de performances, un minimum de 3 Go/s est nécessaire. L'implémentation standard de la bibliothèque ne peut certainement pas être aussi pire que ce qu'EVEREST utilise!

Que puis-je faire pour accélérer la mémcpy dans cette situation?


Détails du matériel: AMD Magny Cours - 4x octal core 128 Go DDR3 Windows Server 2003 Enterprise X64

Programme de test:

#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: Si vous avez cinq minutes supplémentaires et que vous souhaitez contribuer, pouvez-vous exécuter le code ci-dessus sur votre machine et publier votre temps en tant que commentaire?

47
leecbaker

J'ai trouvé un moyen d'augmenter la vitesse dans cette situation. J'ai écrit une version multi-thread de memcpy, divisant la zone à copier entre les threads. Voici quelques chiffres de mise à l'échelle des performances pour une taille de bloc définie, en utilisant le même code de synchronisation que celui trouvé ci-dessus. Je n'avais aucune idée que les performances, en particulier pour cette petite taille de bloc, seraient adaptées à autant de threads. Je soupçonne que cela a quelque chose à voir avec le grand nombre de contrôleurs de mémoire (16) sur cette machine.

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

Je ne comprends pas l'énorme saut de performance entre 3 et 4 threads. Qu'est-ce qui provoquerait un saut comme ça?

J'ai inclus le code memcpy que j'ai écrit ci-dessous pour d'autres qui peuvent rencontrer ce même problème. Veuillez noter qu'il n'y a pas d'erreur lors de la vérification de ce code - il peut être nécessaire de l'ajouter pour votre application.

#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;
}
32
leecbaker

Je ne sais pas si cela est fait au moment de l'exécution ou si vous devez le faire lors de la compilation, mais vous devriez avoir SSE ou des extensions similaires activées car l'unité vectorielle peut souvent écrire 128 bits dans le mémoire par rapport à 64 bits pour le CPU.

Essayez cette implémentation .

Oui, et assurez-vous que les deux la source et la destination sont alignées sur 128 bits. Si votre source et votre destination ne sont pas alignées l'une par rapport à l'autre, votre memcpy () devra faire de la magie sérieuse. :)

9
onemasse

Vous avez quelques obstacles à l'obtention des performances de mémoire requises:

  1. Bande passante - il y a une limite à la vitesse à laquelle les données peuvent passer de la mémoire au CPU et vice-versa. Selon cet article Wikipedia , 266 MHz DDR3 RAM a une limite supérieure d'environ 17 Go/s. Maintenant, avec un memcpy, vous devez le diviser par deux pour obtenir votre transfert maximum puisque les données sont lues puis écrites. D'après les résultats de votre benchmark, il semble que vous n'utilisez pas le plus rapidement possible RAM dans votre système. Si vous pouvez vous le permettre, mettez à niveau la carte mère/RAM (et ce ne sera pas bon marché, les Overclockers au Royaume-Uni ont actuellement 3x4GB PC16000 à 400 £))

  2. Le système d'exploitation - Windows est un système d'exploitation multitâche préemptif, de sorte que de temps en temps, votre processus sera suspendu pour permettre à d'autres processus de jeter un œil et de faire des choses. Cela encombrera vos caches et bloquera votre transfert. Dans le pire des cas, tout votre processus pourrait être mis en cache sur le disque!

  3. Le CPU - les données déplacées ont un long chemin à parcourir: RAM -> Cache L2 -> Cache L1 -> CPU -> L1 -> L2 -> RAM. Il peut même y avoir un Cache L3. Si vous voulez impliquer le CPU, vous voulez vraiment charger L2 pendant la copie de L1. Malheureusement, les CPU modernes peuvent parcourir un bloc de cache L1 plus rapidement que le temps nécessaire pour charger le L1. Le CPU a un contrôleur de mémoire qui aide beaucoup dans ces cas où vos données en streaming dans le CPU séquentiellement mais vous allez toujours avoir des problèmes.

Bien sûr, le moyen le plus rapide de faire quelque chose est de ne pas le faire. Les données capturées peuvent-elles être écrites n'importe où dans RAM ou est le tampon utilisé à un emplacement fixe. Si vous pouvez les écrire n'importe où, alors vous n'avez pas besoin du memcpy du tout. S'il est fixe, pourriez-vous traiter les données en place et utiliser un système de type double tampon? C'est-à-dire commencer à capturer les données et quand elles sont à moitié pleines, commencer à traiter la première moitié des données. Lorsque le tampon est plein, commencez à écrire les données capturées au début et au processus la seconde moitié. Cela nécessite que l'algorithme puisse traiter les données plus rapidement que la carte de capture ne les produit. Il suppose également que les données sont supprimées après le traitement. En fait, il s'agit d'un memcpy avec une transformation dans le cadre du processus de copie, donc vous ai:

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

au lieu de:

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

Ou obtenez une RAM plus rapide!

EDIT: Une autre option est de traiter les données entre la source de données et le PC - pourriez-vous y mettre un DSP/FPGA? Le matériel personnalisé sera toujours plus rapide qu'un CPU à usage général.

Une autre pensée: cela fait un moment que je n'ai pas fait de trucs graphiques haute performance, mais pourriez-vous DMA les données dans la carte graphique puis DMA it Vous pourriez même profiter de CUDA pour effectuer une partie du traitement, ce qui retirerait complètement le processeur de la boucle de transfert de mémoire.

5
Skizz

Une chose à savoir est que votre processus (et donc les performances de memcpy()) est impacté par la planification des tâches par le système d'exploitation - il est difficile de dire à quel point cela est important dans votre timing, bu tit est difficile à contrôler. Le périphérique DMA n'est pas soumis à cela, car il ne s'exécute pas sur le processeur une fois qu'il a démarré. Étant donné que votre application est une application en temps réel, vous souhaiterez peut-être testez les paramètres de priorité de processus/thread de Windows si vous ne l'avez pas déjà fait. Gardez à l'esprit que vous devez faire attention à cela car cela peut avoir un impact vraiment négatif sur d'autres processus (et l'expérience utilisateur sur la machine).

Une autre chose à garder à l'esprit est que la virtualisation de la mémoire du système d'exploitation peut avoir un impact ici - si les pages de mémoire vers lesquelles vous copiez ne sont pas réellement sauvegardées par des pages physiques RAM, la fonction memcpy() l'opération endommagera le système d'exploitation pour mettre en place ce support physique. Vos pages DMA sont susceptibles d'être verrouillées dans la mémoire physique (car elles doivent l'être pour le DMA), la mémoire source de memcpy() n'est donc probablement pas un problème à cet égard. Vous pouvez envisager d'utiliser l'API Win32 VirtualAlloc() pour vous assurer que votre mémoire de destination pour la memcpy() est validée (je pense que VirtualAlloc() est la bonne API pour cela, mais il y en a peut-être une meilleure que j'oublie - ça fait un moment que je n'ai pas eu un besoin de faire quelque chose comme ça).

Enfin, voyez si vous pouvez utiliser la technique expliquée par Skizz pour éviter complètement la memcpy() - c'est votre meilleur pari si les ressources le permettent.

5
Michael Burr

Vous pouvez écrire une meilleure implémentation de memcpy en utilisant les registres SSE2. La version VC2010 le fait déjà. La question est donc plus, si vous lui remettez de la mémoire alignée.

Peut-être que vous pouvez faire mieux que la version de VC 2010, mais elle nécessite une certaine compréhension de la façon de le faire.

PS: Vous pouvez passer le tampon au programme en mode utilisateur dans un appel inversé, pour empêcher complètement la copie.

2
Christopher

Tout d'abord, vous devez vérifier que la mémoire est alignée sur une limite de 16 octets, sinon vous obtenez des pénalités. C'est la chose la plus importante.

Si vous n'avez pas besoin d'une solution conforme aux normes, vous pouvez vérifier si les choses s'améliorent en utilisant une extension spécifique au compilateur telle que memcpy64 (vérifiez auprès de votre doc du compilateur s'il y a quelque chose de disponible). Le fait est que memcpy doit pouvoir traiter une copie à un seul octet, mais déplacer 4 ou 8 octets à la fois est beaucoup plus rapide si vous n'avez pas cette restriction.

Encore une fois, est-ce une option pour vous d'écrire du code d'assemblage en ligne?

2
Simone

Peut-être pouvez-vous expliquer un peu plus comment vous traitez la plus grande zone de mémoire?

Serait-il possible dans votre application de simplement transmettre la propriété du tampon, plutôt que de le copier? Cela éliminerait complètement le problème.

Ou utilisez-vous memcpy pour plus que la simple copie? Peut-être utilisez-vous la plus grande zone de mémoire pour créer un flux séquentiel de données à partir de ce que vous avez capturé? Surtout si vous traitez un personnage à la fois, vous pourrez peut-être vous rencontrer à mi-chemin. Par exemple, il peut être possible d’adapter votre code de traitement pour prendre en charge un flux représenté comme "un tableau de tampons", plutôt que "une zone de mémoire continue".

2
Stéphan Kochen

Une source que je vous recommanderais de lire est la fonction fast_memcpy De MPlayer. Tenez également compte des modèles d'utilisation attendus et notez que les processeurs modernes ont des instructions de magasin spéciales qui vous permettent d'indiquer au processeur si vous devrez ou non relire les données que vous écrivez. L'utilisation des instructions qui indiquent que vous ne lirez pas les données (et qu'elles n'ont donc pas besoin d'être mises en cache) peut être un énorme gain pour les grandes opérations memcpy.

1
R..