2010-06-03 8 views
34

J'ai une fonction qui fait memcpy, mais cela prend énormément de cycles. Existe-t-il une alternative/approche plus rapide que l'utilisation de memcpy pour déplacer un morceau de mémoire?alternative plus rapide à memcpy?

+1

Réponse courte: Peut-être, il est possible. Offrir plus de détails comme l'architecture, la plate-forme et d'autres. Dans le monde intégré, il est très probable de réécrire certaines fonctions de la libc qui ne fonctionnent pas très bien. – INS

Répondre

111

memcpy est susceptible d'être la plus rapide, vous pouvez copier les octets autour de la mémoire. Si vous avez besoin de quelque chose de plus rapide - essayez de trouver un moyen de pas copier des choses autour, par exemple. échanger des pointeurs uniquement, pas les données elles-mêmes.

+2

+1, Nous avons récemment eu un problème lorsque certains de notre code ont ralenti considérablement et consommé beaucoup de mémoire supplémentaire lors du traitement d'un certain fichier.Il s'est avéré que le fichier contenait d'énormes blocs de métadonnées alors que les autres mouches n'avaient pas de métadonnées ni de petits blocs. Et ces métadonnées ont été copiées, copiées, copiées, consommant du temps et de la mémoire. Copie remplacée avec la référence const-pass. – sharptooth

+6

C'est une bonne question à propos de memcpy plus rapide, mais cette réponse fournit une solution de contournement, pas une réponse. Par exemple. http://software.intel.com/en-us/articles/memcpy-performance/ explique quelques raisons assez sérieuses pour lesquelles memcpy est souvent beaucoup moins efficace qu'elle ne pourrait l'être. –

+0

Pourrait-il être possible d'utiliser une technique Copy on Write, soit au niveau bas, soit délibérément en code? Auriez-vous besoin de morceaux de mémoire de taille similaire à des entiers multiples de pages? Ensuite, laissez simplement les deux pointeurs pointés dans la vie réelle sur la même mémoire et laissez le gestionnaire de mémoire faire des copies de pages comme il le faut lorsque les données sont modifiées. –

6

Habituellement, la bibliothèque standard fourni avec le compilateur mettra en œuvre memcpy() le plus rapidement possible pour la plate-forme cible déjà.

3

Il est généralement plus rapide de ne pas faire de copie du tout. Si vous pouvez adapter votre fonction pour ne pas copier je ne sais pas, mais il vaut la peine de regarder.

3

Parfois, des fonctions comme memcpy, memset, ... sont mis en œuvre de deux façons différentes:

  • une fois comme une véritable fonction
  • une fois que certains montage qui est immédiatement inline

Pas tous Les compilateurs prennent par défaut la version inline-assembly, votre compilateur peut utiliser la variante de fonction par défaut, ce qui entraîne une surcharge due à l'appel de la fonction. Vérifiez votre compilateur pour voir comment prendre la variante intrinsèque de la fonction (option de ligne de commande, pragma, ...).

Edit: Voir http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx pour une explication de intrinsics sur le compilateur Microsoft C.

0

Je suppose que vous devez avoir d'énormes zones de mémoire que vous souhaitez copier autour, si la performance de memcpy est devenu un problème pour vous?

Dans ce cas, je suis d'accord avec la suggestion de nos à trouver un moyen de ne pas copier des trucs ..

Au lieu d'avoir une énorme blob de mémoire à copier autour chaque fois que vous devez changer, vous devrait probablement essayer quelques structures de données alternatives à la place. Sans vraiment savoir quoi que ce soit au sujet de votre problème, je vous suggère de jeter un coup d'œil à persistent data structures et d'implémenter le vôtre ou de réutiliser une implémentation existante.

2

Vérifiez votre manuel du compilateur/de la plate-forme. Pour certains micro-processeurs et kits DSP, l'utilisation de memcpy est beaucoup plus lente que les opérations intrinsic functions ou DMA.

2

Si votre plate-forme prend en charge, examiner si vous pouvez utiliser l'appel système mmap() pour laisser vos données dans le fichier ... généralement le système d'exploitation peut gérer que mieux. Et, comme tout le monde l'a dit, évitez de copier autant que possible; Les pointeurs sont votre ami dans des cas comme celui-ci.

10

Veuillez nous offrir plus de détails. Sur l'architecture i386, il est très possible que memcpy soit le moyen de copie le plus rapide. Mais sur une architecture différente pour laquelle le compilateur n'a pas de version optimisée, il est préférable de réécrire votre fonction memcpy. Je l'ai fait sur une architecture ARM personnalisée en utilisant le langage d'assemblage. Si vous transférez de gros morceaux de mémoire, alors DMA est probablement la réponse que vous cherchez.

Veuillez fournir plus de détails - architecture, système d'exploitation (le cas échéant).

+1

Pour ARM, la libc impl est maintenant plus rapide que ce que vous pourrez créer vous-même. Pour les petites copies (rien de moins qu'une page), il peut être plus rapide d'utiliser une boucle ASM dans vos fonctions. Mais, pour les grandes copies, vous ne serez pas capable de battre l'impli libc, car les processeurs diff ont des chemins de code "optimaux" légèrement différents. Par exemple, un Cortex8 fonctionne mieux avec les instructions de copie NEON, mais un Cortex9 est plus rapide avec les instructions ARM ldm/stm. Vous ne pouvez pas écrire un morceau de code rapide pour les deux processeurs, mais vous pouvez simplement appeler memcpy pour les tampons volumineux. – MoDJ

+0

@MoDJ: Je souhaite que la bibliothèque C standard inclue quelques variantes de memcpy différentes avec une sémantique généralement identique dans les cas où tous ont généré un comportement défini, mais différents cas optimisés et - dans certains cas - des restrictions à l'alignement aligné vs aligné. Si le code a généralement besoin de copier de petits nombres d'octets ou de mots connus pour être alignés, une implémentation naïve de caractère à la fois pourrait faire le travail en moins de temps que certaines mises en œuvre de memcpy() plus sophistiquées un plan d'action. – supercat

0

nos est juste, vous l'appelez trop.

Pour voir d'où vous l'appelez et pourquoi, mettez-le en pause plusieurs fois sous le débogueur et regardez la pile.

0

mémoire à la mémoire est généralement pris en charge dans le jeu de commandes de CPU et memcpy utilisera généralement que. Et c'est généralement le moyen le plus rapide.

Vous devriez vérifier ce que fait exactement votre CPU. Sous Linux, observez swapi in and out et l'efficacité de la mémoire virtuelle avec sar -B 1 ou vmstat 1 ou en regardant dans/proc/memstat. Vous pouvez voir que votre copie doit sortir beaucoup de pages pour libérer de l'espace, ou les lire, etc.

Cela signifie que votre problème ne réside pas dans ce que vous utilisez pour la copie, mais dans l'utilisation de votre système. Mémoire. Vous devrez peut-être diminuer le cache de fichiers ou commencer à écrire plus tôt, ou verrouiller les pages en mémoire, etc.

6

En fait, memcpy n'est PAS le moyen le plus rapide, surtout si vous l'appelez plusieurs fois. J'avais aussi du code dont j'avais vraiment besoin pour accélérer, et memcpy est lent car il y a trop de contrôles inutiles. Par exemple, il vérifie si les blocs de destination et de mémoire source se chevauchent et s'il doit commencer à copier depuis l'arrière du bloc plutôt que vers l'avant. Si vous ne vous souciez pas de telles considérations, vous pouvez certainement faire beaucoup mieux. J'ai un peu de code, mais voici peut-être une version toujours meilleure:

Very fast memcpy for image processing?.

Si vous recherchez, vous pouvez également trouver d'autres implémentations. Mais pour la vraie vitesse, vous avez besoin d'une version d'assemblage.

+0

J'ai essayé un code similaire à celui-ci en utilisant sse2. Il s'avère qu'il était plus lent sur mon système AMD par un facteur de 4x que le builtin. Il est toujours préférable de ne pas copier si vous pouvez l'aider. – Matt

+0

Bien que 'memmove' doive vérifier et gérer le chevauchement,' memcpy' n'est pas nécessaire pour cela. Le plus gros problème est que, pour être efficaces lors de la copie de gros blocs, les implémentations de 'memcpy' doivent sélectionner une approche de copie avant de pouvoir commencer à travailler. Si le code doit être capable de copier un nombre arbitraire d'octets, mais ce nombre sera un 90% du temps, deux 9% du temps, trois 0,9% du temps, etc. et les valeurs de 'count', 'dest', et' src' ne sera pas nécessaire par la suite, puis un 'if (count) do * aligné = dest + = * src; while (- count> 0); 'pourrait mieux que la routine" smarter ". – supercat

+0

BTW, sur certains systèmes embarqués, une autre raison 'memcpy' peut-être pas l'approche la plus rapide est qu'un contrôleur DMA peut parfois être en mesure de copier un bloc de mémoire avec moins de frais généraux que le CPU, mais le moyen le plus efficace pourrait être de démarrer le DMA et ensuite faire un autre traitement pendant que le DMA est en cours d'exécution. Sur un système avec des bus de données et de code frontaux séparés, il peut être possible de configurer le DMA afin qu'il copie les données à chaque cycle lorsque le CPU n'a besoin du bus de données pour rien d'autre. Cela peut atteindre de bien meilleures performances que l'utilisation du CPU pour la copie, en utilisant ... – supercat

1

Vous devriez vérifier le code d'assemblage généré pour votre code. Ce que vous ne voulez pas, c'est que l'appel memcpy génère un appel à la fonction memcpy dans la bibliothèque standard - ce que vous voulez est d'avoir un appel répété à la meilleure instruction ASM pour copier la plus grande quantité de données - quelque chose comme rep movsq.

Comment pouvez-vous y parvenir? Eh bien, le compilateur optimise les appels à memcpy en le remplaçant par mov s tant qu'il sait combien de données il doit copier. Vous pouvez le voir si vous écrivez un memcpy avec une valeur bien déterminée (constexpr). Si le compilateur ne connaît pas la valeur, il devra revenir à l'implémentation au niveau octet de memcpy - le problème étant que memcpy doit respecter la granularité d'un octet. Il bougera toujours 128 bits à la fois, mais après chaque 128b il devra vérifier s'il a assez de données à copier comme 128b ou il doit retomber à 64bits, puis à 32 et 8 (je pense que 16 pourrait être sous-optimal de toute façon, mais je ne sais pas à coup sûr).

Donc ce que vous voulez est soit pouvoir dire à memcpy quelle est la taille de vos données avec des expressions const que le compilateur peut optimiser.De cette façon, aucun appel à memcpy n'est effectué. Ce que vous ne voulez pas, c'est passer à memcpy une variable qui ne sera connue qu'à l'exécution. Cela se traduit par un appel de fonction et des tonnes de tests pour vérifier la meilleure instruction de copie. Parfois, une boucle simple est meilleure que memcpy pour cette raison (en éliminant un appel de fonction). Et ce vous ne voulez vraiment pas est de passer à memcpy un nombre impair d'octets à copier.

6

Ceci est une réponse pour x86_64 avec le jeu d'instructions AVX2 présent. Bien que quelque chose de similaire puisse s'appliquer à ARM/AArch64 avec SIMD.

Sur Ryzen 1800X avec un seul canal mémoire entièrement rempli (2 emplacements, 16 Go de DDR4 dans chaque), le code suivant est 1,56 fois plus rapide que memcpy() sur le compilateur MSVC++ 2017. Si vous remplissez les deux canaux de mémoire avec deux modules DDR4, c'est-à-dire que vous avez occupé les quatre emplacements DDR4, vous pouvez obtenir deux fois plus de temps de copie en mémoire. Pour les systèmes de mémoire à trois (4) canaux, vous pouvez obtenir une copie de mémoire encore plus rapide (1,5 fois) si le code est étendu au code AVX512. Avec AVX2, seuls les systèmes triple canal/quadruple avec tous les slots occupés ne devraient pas être plus rapides car pour les charger complètement, vous devez charger/stocker plus de 32 octets à la fois (48 octets pour les octets triple et 64 octets pour les canaux quad systèmes), tandis que AVX2 peut charger/stocker pas plus de 32 octets à la fois. Bien que le multithreading sur certains systèmes peut atténuer cela sans AVX512 ou même AVX2.

Voici donc le code de copie qui suppose que vous copiez un grand bloc de mémoire dont la taille est un multiple de 32 et que le bloc est aligné sur 32 octets.

Pour les blocs non-multi-tailles et non alignés, le code prologue/épilogue peut être écrit en réduisant la largeur à 16 (SSE4.1), 8, 4, 2 et finalement 1 octet à la fois pour la tête et la queue . Au centre également, un tableau local de 2-3 valeurs __m256i peut être utilisé comme proxy entre les lectures alignées de la source et les écritures alignées vers la destination.

#include <immintrin.h> 
#include <cstdint> 
/* ... */ 
void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) { 
    assert(nBytes % 32 == 0); 
    assert((intptr_t(pvDest) & 31) == 0); 
    assert((intptr_t(pvSrc) & 31) == 0); 
    const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc); 
    __m256i *pDest = reinterpret_cast<__m256i*>(pvDest); 
    int64_t nVects = nBytes/sizeof(*pSrc); 
    for (; nVects > 0; nVects--, pSrc++, pDest++) { 
    const __m256i loaded = _mm256_stream_load_si256(pSrc); 
    _mm256_stream_si256(pDest, loaded); 
    } 
    _mm_sfence(); 
} 

Un élément clé de ce code est qu'il ignore le cache du processeur lors de la copie: lorsque le cache du processeur est impliqué (à savoir les instructions AVX sans _stream_ sont utilisées), la vitesse de copie tombe plusieurs fois sur mon système.

Ma mémoire DDR4 est de 2,6 GHz CL13. Ainsi, lors de la copie 8 Go de données d'un tableau à l'autre, je suis les vitesses suivantes:

memcpy(): 17 208 004 271 bytes/sec. 
Stream copy: 26 842 874 528 bytes/sec. 

Notez que dans ces mesures la taille totale des deux tampons d'entrée et de sortie est divisé par le nombre de secondes écoulées. Parce que pour chaque octet du tableau il y a 2 accès mémoire: un pour lire l'octet du tableau d'entrée, un autre pour écrire l'octet dans le tableau de sortie. En d'autres termes, lorsque vous copiez 8 Go d'un tableau à un autre, vous effectuez des opérations d'accès en mémoire de 16 Go.

Le multithreading modéré peut encore améliorer les performances environ 1,44 fois, donc l'augmentation totale sur memcpy() atteint 2,55 fois sur ma machine. Voilà comment les performances de copie de flux dépend du nombre de threads utilisés sur ma machine:

Stream copy 1 threads: 27114820909.821 bytes/sec 
Stream copy 2 threads: 37093291383.193 bytes/sec 
Stream copy 3 threads: 39133652655.437 bytes/sec 
Stream copy 4 threads: 39087442742.603 bytes/sec 
Stream copy 5 threads: 39184708231.360 bytes/sec 
Stream copy 6 threads: 38294071248.022 bytes/sec 
Stream copy 7 threads: 38015877356.925 bytes/sec 
Stream copy 8 threads: 38049387471.070 bytes/sec 
Stream copy 9 threads: 38044753158.979 bytes/sec 
Stream copy 10 threads: 37261031309.915 bytes/sec 
Stream copy 11 threads: 35868511432.914 bytes/sec 
Stream copy 12 threads: 36124795895.452 bytes/sec 
Stream copy 13 threads: 36321153287.851 bytes/sec 
Stream copy 14 threads: 36211294266.431 bytes/sec 
Stream copy 15 threads: 35032645421.251 bytes/sec 
Stream copy 16 threads: 33590712593.876 bytes/sec 

Le code est:

void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) { 
    for (; nVects > 0; nVects--, pSrc++, pDest++) { 
    const __m256i loaded = _mm256_stream_load_si256(pSrc); 
    _mm256_stream_si256(pDest, loaded); 
    } 
} 

void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) { 
    assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0); 
    const uint32_t maxThreads = std::thread::hardware_concurrency(); 
    std::vector<std::thread> thrs; 
    thrs.reserve(maxThreads + 1); 

    const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput); 
    __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput); 
    const int64_t nVects = cnDoubles * sizeof(*gpdInput)/sizeof(*pSrc); 

    for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) { 
    auto start = std::chrono::high_resolution_clock::now(); 
    lldiv_t perWorker = div((long long)nVects, (long long)nThreads); 
    int64_t nextStart = 0; 
    for (uint32_t i = 0; i < nThreads; i++) { 
     const int64_t curStart = nextStart; 
     nextStart += perWorker.quot; 
     if ((long long)i < perWorker.rem) { 
     nextStart++; 
     } 
     thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart); 
    } 
    for (uint32_t i = 0; i < nThreads; i++) { 
     thrs[i].join(); 
    } 
    _mm_sfence(); 
    auto elapsed = std::chrono::high_resolution_clock::now() - start; 
    double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count(); 
    printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double)/nSec); 

    thrs.clear(); 
    } 
}