Je vais aller à l'encontre de la sagesse générale ici qui std::copy
entraînera 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, je hache 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 de 300 boucles suffit à stabiliser complètement mes résultats. J'ai effectué le test 5 fois chacun, en alternant entre la memcpy
version et la std::copy
version. Mon code tire parti de la saisie de données en aussi gros morceaux que possible (de nombreuses autres implémentations fonctionnent avec char
/ char *
, alors que j'opère avec T
/ T *
(où T
est le plus grand type dans l'implémentation de l'utilisateur qui a un comportement de débordement correct), donc un accès mémoire rapide sur le les plus grands types possibles sont essentiels aux performances de mon algorithme. Voici mes résultats:
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 totale de la vitesse de std :: copy sur 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 décidé de tester également mon implémentation MD5. Les résultats étaient beaucoup moins stables, j'ai donc 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 exécution à l'autre, donc je suppose qu'il y avait une sorte d'activité du système d'exploitation. J'ai décidé de recommencer.
Mêmes paramètres et indicateurs du compilateur. Il n'y a qu'une seule version de MD5, et c'est plus rapide que SHA-2, j'ai donc fait 3000 boucles sur un ensemble similaire de 5 chaînes de test.
Voici 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 moyenne totale de la vitesse de std :: copy sur memcpy: 0,11%
Code pour mon implémentation MD5
Ces résultats suggèrent qu'il existe une certaine optimisation que std :: copy utilisée dans mes tests SHA-2 qui std::copy
n'a pas pu être utilisée dans mes tests MD5. Dans les tests SHA-2, les deux tableaux ont été créés dans la même fonction qui a appelé 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 std::copy
accélérer à nouveau. La réponse s'est avérée simple: activez l'optimisation du temps de liaison. Voici mes résultats avec LTO activé (option -flto dans 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 totale de la vitesse de std :: copy sur memcpy: 0,72%
En résumé, il ne semble pas y avoir de pénalité de performance pour l'utilisation std::copy
. En fait, il semble y avoir un gain de performance.
Explication des résultats
Alors, pourquoi pourrait-il std::copy
augmenter les performances?
Premièrement, je ne m'attendrais pas à ce qu'il soit plus lent pour une implémentation, tant que l'optimisation de l'inlining est activée. Tous les compilateurs en ligne de manière agressive; c'est peut-être l'optimisation la plus importante car elle permet de nombreuses autres optimisations. std::copy
peut (et je soupçonne que toutes les implémentations du monde réel le font) détecter que les arguments sont trivialement copiables et que la mémoire est disposée séquentiellement. Cela signifie que dans le pire des cas, quand memcpy
c'est légal, std::copy
ne devrait pas être pire. L'implémentation triviale de std::copy
ce report memcpy
devrait répondre aux critères de votre compilateur de "toujours en ligne lors de l'optimisation de la vitesse ou de la taille".
Cependant, std::copy
conserve également plus de ses informations. Lorsque vous appelez std::copy
, la fonction conserve les types intacts. memcpy
fonctionne void *
, ce qui supprime presque toutes les informations utiles. Par exemple, si je passe dans un tableau de std::uint64_t
, le compilateur ou l'implémenteur de la bibliothèque peut être en mesure de profiter 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 celui-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 sur la partie non alignée à la fin. S'il est garanti que tout est aligné, le code devient plus simple et plus rapide, et plus facile pour le prédicteur de branche de votre processeur à être correct.
Optimisation prématurée?
std::copy
est dans une position intéressante. Je m'attends à ce qu'il ne soit jamais plus lent memcpy
et parfois plus rapide avec n'importe quel compilateur d'optimisation moderne. De plus, tout ce que vous pouvez memcpy
, vous le pouvez std::copy
. memcpy
ne permet aucun chevauchement dans les tampons, alors que les std::copy
supports se chevauchent dans un sens (avec std::copy_backward
pour l'autre sens de recouvrement). memcpy
ne fonctionne que sur des pointeurs, std::copy
fonctionne sur tous les itérateurs ( std::map
, std::vector
, std::deque
, ou mon propre type personnalisé). En d'autres termes, vous ne devriez l'utiliser que std::copy
lorsque vous avez besoin de copier des morceaux de données.
char
peut être signé ou non signé, selon l'implémentation. Si le nombre d'octets peut être> = 128, utilisezunsigned char
pour vos tableaux d'octets. (Le(int *)
casting serait aussi plus sûr(unsigned int *)
.)