Est-il préférable d’utiliser std :: memcpy () ou std :: copy () en termes de performances?

Est-il préférable d’utiliser memcpy comme indiqué ci-dessous ou est-il préférable d’utiliser std::copy() en termes de performances? Pourquoi?

 char *bits = NULL; ... bits = new (std::nothrow) char[((int *) copyMe->bits)[0]]; if (bits == NULL) { cout <bits, ((int *) copyMe->bits)[0]); 

Je vais aller à l’encontre de la sagesse générale ici que std::copy aura une légère perte de performance presque imperceptible. Je viens de faire un test et j’ai trouvé que c’était faux: j’ai remarqué une différence de performance. Cependant, le gagnant était std::copy .

J’ai écrit une implémentation C ++ SHA-2. Dans mon test, j’ai haché 5 chaînes en utilisant les quatre versions SHA-2 (224, 256, 384, 512), et je boucle 300 fois. Je mesure les temps en utilisant Boost.timer. Ce compteur à 300 boucles suffit à stabiliser complètement mes résultats. J’ai couru le test 5 fois chacun, alternant entre la version memcpy et la version std::copy . Mon code tire parti de la saisie de données dans des blocs aussi importants que possible (de nombreuses autres implémentations fonctionnent avec char / char * , alors que je fonctionne avec T / T * (où T est le plus grand type d’implémentation de l’utilisateur) Ainsi, l’access rapide à la mémoire sur les plus gros types que je peux est essentiel à la performance de mon algorithme.

Temps (en secondes) pour terminer l’exécution des tests SHA-2

 std::copy memcpy % increase 6.11 6.29 2.86% 6.09 6.28 3.03% 6.10 6.29 3.02% 6.08 6.27 3.03% 6.08 6.27 3.03% 

Augmentation moyenne de la vitesse de std :: copy over memcpy: 2.99%

Mon compilateur est gcc 4.6.3 sur Fedora 16 x86_64. Mes indicateurs d’optimisation sont -Ofast -march=native -funsafe-loop-optimizations .

Code pour mes implémentations SHA-2.

J’ai également décidé de tester mon implémentation MD5. Les résultats étaient beaucoup moins stables, alors j’ai décidé de faire 10 courses. Cependant, après mes premières tentatives, j’ai obtenu des résultats qui variaient énormément d’une course à l’autre, alors je suppose qu’il y avait une sorte d’activité du système d’exploitation. J’ai décidé de recommencer.

Mêmes parameters de compilateur et indicateurs. Il n’y a qu’une seule version de MD5, et c’est plus rapide que SHA-2, donc j’ai fait 3000 boucles sur un jeu similaire de 5 chaînes de test.

Ce sont mes 10 derniers résultats:

Temps (en secondes) pour terminer l’exécution des tests MD5

 std::copy memcpy % difference 5.52 5.56 +0.72% 5.56 5.55 -0.18% 5.57 5.53 -0.72% 5.57 5.52 -0.91% 5.56 5.57 +0.18% 5.56 5.57 +0.18% 5.56 5.53 -0.54% 5.53 5.57 +0.72% 5.59 5.57 -0.36% 5.57 5.56 -0.18% 

Diminution totale moyenne de la vitesse de std :: copy over memcpy: 0,11%

Code pour mon implémentation MD5

Ces résultats suggèrent qu’il existe une optimisation que std :: copy utilise dans mes tests SHA-2 que std::copy n’a pas pu utiliser dans mes tests MD5. Dans les tests SHA-2, les deux tableaux ont été créés dans la même fonction appelée std::copy / memcpy . Dans mes tests MD5, l’un des tableaux a été transmis à la fonction en tant que paramètre de fonction.

J’ai fait un peu plus de tests pour voir ce que je pouvais faire pour rendre std::copy plus rapide. La réponse s’est avérée simple: activer l’optimisation du temps de liaison. Ce sont mes résultats avec LTO activé (option -flto en gcc):

Temps (en secondes) pour terminer l’exécution des tests MD5 avec -flto

 std::copy memcpy % difference 5.54 5.57 +0.54% 5.50 5.53 +0.54% 5.54 5.58 +0.72% 5.50 5.57 +1.26% 5.54 5.58 +0.72% 5.54 5.57 +0.54% 5.54 5.56 +0.36% 5.54 5.58 +0.72% 5.51 5.58 +1.25% 5.54 5.57 +0.54% 

Augmentation moyenne de la vitesse totale de std :: copy over memcpy: 0.72%

En résumé, il ne semble pas y avoir de pénalité de performance pour l’utilisation de std::copy . En fait, il semble y avoir un gain de performance.

Explication des résultats

Alors, pourquoi std::copy pourrait-il donner un coup de pouce à la performance?

Tout d’abord, je ne m’attendrais pas à ce qu’il soit plus lent pour toute implémentation, tant que l’optimisation de l’inline est activée. Tous les compilateurs sont en ligne de manière agressive; c’est probablement l’optimisation la plus importante car elle permet beaucoup d’autres optimisations. std::copy peut (et je suppose que toutes les implémentations du monde réel le font) détecter que les arguments sont sortingvialement copiables et que la mémoire est disposée de manière séquentielle. Cela signifie que dans le pire des cas, lorsque memcpy est légal, std::copy ne devrait pas être pire. L’implémentation sortingviale de std::copy qui est memcpy à memcpy devrait répondre aux critères de votre compilateur: “toujours en ligne pour optimiser la vitesse ou la taille”.

Cependant, std::copy conserve également plus d’informations. Lorsque vous appelez std::copy , la fonction conserve les types intacts. memcpy fonctionne sur void * , ce qui élimine presque toutes les informations utiles. Par exemple, si je passe un tableau de std::uint64_t , le compilateur ou l’implémenteur de bibliothèque pourra tirer parti de l’alignement 64 bits avec std::copy , mais il peut être plus difficile de le faire avec memcpy . De nombreuses implémentations d’algorithmes comme celle-ci fonctionnent en travaillant d’abord sur la partie non alignée au début de la plage, puis sur la partie alignée, puis la partie non alignée à la fin. Si tout est garanti pour être aligné, le code devient plus simple et plus rapide, et plus facile pour le prédicteur de twig de votre processeur.

Optimisation prématurée?

std::copy est dans une position intéressante. Je m’attends à ce qu’il ne soit jamais plus lent que memcpy et parfois plus rapide avec tout compilateur d’optimisation moderne. De plus, tout ce que vous pouvez memcpy , vous pouvez std::copy . memcpy n’autorise aucun chevauchement dans les tampons, alors que std::copy supporte les chevauchements dans une direction (avec std::copy_backward pour l’autre sens du chevauchement). memcpy ne fonctionne que sur les pointeurs, std::copy fonctionne sur tous les iterators ( std::map , std::vector , std::deque ou mon propre type personnalisé). En d’autres termes, vous devez simplement utiliser std::copy lorsque vous devez copier des morceaux de données.

Tous les compilateurs que je connais remplaceront un simple std::copy par un memcpy lorsque cela est approprié, ou mieux encore, vectoriser la copie pour qu’elle soit encore plus rapide qu’un memcpy .

En tout cas: profil et découvrez vous-même. Différents compilateurs feront des choses différentes, et il est fort possible que cela ne fasse pas exactement ce que vous demandez.

Voir cette présentation sur les optimisations du compilateur (pdf).

Voici ce que GCC fait pour un simple std::copy d’un type POD.

 #include  struct foo { int x, y; }; void bar(foo* a, foo* b, size_t n) { std::copy(a, a + n, b); } 

Voici le désassemblage (avec seulement -O optimisation), montrant l’appel à memmove :

 bar(foo*, foo*, unsigned long): salq $3, %rdx sarq $3, %rdx testq %rdx, %rdx je .L5 subq $8, %rsp movq %rsi, %rax salq $3, %rdx movq %rdi, %rsi movq %rax, %rdi call memmove addq $8, %rsp .L5: rep ret 

Si vous changez la signature de la fonction en

 void bar(foo* __ressortingct a, foo* __ressortingct b, size_t n) 

la memmove devient alors une memcpy amélioration de la performance. Notez que memcpy lui-même sera fortement vectorisé.

Utilisez toujours std::copy car memcpy est limité aux structures POD de style C, et le compilateur remplacera probablement les appels à std::copy avec memcpy si les cibles sont en fait des POD.

De plus, std::copy peut être utilisé avec de nombreux types d’iterators, pas seulement des pointeurs. std::copy est plus flexible pour aucune perte de performance et est clairement gagnant.

En théorie, memcpy pourrait avoir un avantage de performance léger , imperceptible , infinitésimal , uniquement parce qu’il n’a pas les mêmes exigences que std::copy . De la page de memcpy de memcpy :

Pour éviter les débordements, la taille des tableaux pointés à la fois par les parameters de destination et les parameters source doit être au moins numérique et ne doit pas se chevaucher (pour les blocs de mémoire superposés, memmove est une approche plus sûre).

En d’autres termes, memcpy peut ignorer la possibilité de chevauchement des données. (Passer des tableaux qui se chevauchent à memcpy est un comportement indéfini.) Donc, memcpy n’a pas besoin de vérifier explicitement cette condition, alors que std::copy peut être utilisé tant que le paramètre OutputIterator n’est pas dans la plage source. Notez que ce n’est pas la même chose que de dire que la plage source et la plage de destination ne peuvent pas se chevaucher.

Donc, comme std::copy a des exigences quelque peu différentes, en théorie, il devrait être légèrement plus lent (avec un accent extrême sur un peu ), car il vérifiera probablement les tableaux C qui se chevauchent, ou bien déléguera la copie des tableaux C à memmove , qui doit effectuer la vérification. Mais dans la pratique, vous (et la plupart des profileurs) ne détecterez probablement même aucune différence.

Bien sûr, si vous ne travaillez pas avec des POD , vous ne pouvez pas utiliser memcpy .

Ma règle est simple. Si vous utilisez C ++, préférez les bibliothèques C ++ et non C 🙂

Si vous avez vraiment besoin de performances de copie maximales (que vous ne pouvez pas), n’utilisez ni l’une ni l’autre .

Il y a beaucoup à faire pour optimiser la copie de la mémoire – encore plus si vous souhaitez utiliser plusieurs threads / cœurs. Voir, par exemple:

Qu’est-ce qui manque / sous-optimal dans cette implémentation memcpy?

la question et certaines des réponses suggèrent des implémentations ou des liens vers des implémentations.

Juste un petit ajout: la différence de vitesse entre memcpy() et std::copy() peut varier considérablement selon que les optimisations sont activées ou désactivées. Avec g ++ 6.2.0 et sans optimisations, memcpy() l’emporte clairement:

 Benchmark Time CPU Iterations --------------------------------------------------- bm_memcpy 17 ns 17 ns 40867738 bm_stdcopy 62 ns 62 ns 11176219 bm_stdcopy_n 72 ns 72 ns 9481749 

Lorsque les optimisations sont activées ( -O3 ), tout se présente de la même manière:

 Benchmark Time CPU Iterations --------------------------------------------------- bm_memcpy 3 ns 3 ns 274527617 bm_stdcopy 3 ns 3 ns 272663990 bm_stdcopy_n 3 ns 3 ns 274732792 

Plus le tableau est grand, moins l’effet est perceptible, mais même à N=1000 memcpy() est environ deux fois plus rapide lorsque les optimisations ne sont pas activées.

Code source (nécessite Google Benchmark):

 #include  #include  #include  #include  constexpr int N = 10; void bm_memcpy(benchmark::State& state) { std::vector a(N); std::vector r(N); while (state.KeepRunning()) { memcpy(r.data(), a.data(), N * sizeof(int)); } } void bm_stdcopy(benchmark::State& state) { std::vector a(N); std::vector r(N); while (state.KeepRunning()) { std::copy(a.begin(), a.end(), r.begin()); } } void bm_stdcopy_n(benchmark::State& state) { std::vector a(N); std::vector r(N); while (state.KeepRunning()) { std::copy_n(a.begin(), N, r.begin()); } } BENCHMARK(bm_memcpy); BENCHMARK(bm_stdcopy); BENCHMARK(bm_stdcopy_n); BENCHMARK_MAIN() /* EOF */ 

Le profilage montre que l’instruction: std::copy() est toujours aussi rapide que memcpy() ou plus rapide est fausse.

Mon système:

HP-Compaq-dx7500-Microtower 3.13.0-24-générique # 47-Ubuntu SMP ven Mai 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU / Linux.

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

Le code (langue: c ++):

  const uint32_t arr_size = (1080 * 720 * 3); //HD image in rgb24 const uint32_t iterations = 100000; uint8_t arr1[arr_size]; uint8_t arr2[arr_size]; std::vector v; main(){ { DPROFILE; memcpy(arr1, arr2, sizeof(arr1)); printf("memcpy()\n"); } v.reserve(sizeof(arr1)); { DPROFILE; std::copy(arr1, arr1 + sizeof(arr1), v.begin()); printf("std::copy()\n"); } { time_t t = time(NULL); for(uint32_t i = 0; i < iterations; ++i) memcpy(arr1, arr2, sizeof(arr1)); printf("memcpy() elapsed %ds\n", time(NULL) - t); } { time_t t = time(NULL); for(uint32_t i = 0; i < iterations; ++i) std::copy(arr1, arr1 + sizeof(arr1), v.begin()); printf("std::copy() elapsed %ds\n", time(NULL) - t); } } 

g ++ -O0 -o test_stdcopy test_stdcopy.cpp

Profil de memcpy (): main: 21: maintenant: 1422969084: 04859: 2650 us
std :: copy () profile: main: 27: maintenant: 1422969084: 04862 écoulé: 2745 us
memcpy () écoulé 44 s std :: copy () écoulé 45 s

g ++ -O3 -o test_stdcopy test_stdcopy.cpp

Profil de memcpy (): main: 21: maintenant: 1422969601: 04939: 2385 us
std :: copy () profile: main: 28: maintenant: 1422969601: 04941 écoulé: 2690 us
memcpy () écoulé 27 s std :: copy () écoulé 43 s

Red Alert a souligné que le code utilise memcpy de tableau en tableau et std :: copy de tableau en vecteur. Cela pourrait être une raison de gagner en rapidité.

Depuis qu'il y a

v.reserve (sizeof (arr1));

il ne doit y avoir aucune différence de copie entre vecteur ou tableau.

Le code est corrigé pour utiliser array pour les deux cas. memcpy encore plus vite:

 { time_t t = time(NULL); for(uint32_t i = 0; i < iterations; ++i) memcpy(arr1, arr2, sizeof(arr1)); printf("memcpy() elapsed %ld s\n", time(NULL) - t); } { time_t t = time(NULL); for(uint32_t i = 0; i < iterations; ++i) std::copy(arr1, arr1 + sizeof(arr1), arr2); printf("std::copy() elapsed %ld s\n", time(NULL) - t); } memcpy() elapsed 44 s std::copy() elapsed 48 s