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 << "ERROR Not enough memory.\n";
exit(1);
}
memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);
Je vais aller à l’encontre de la sagesse générale 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, je hachais 5 chaînes en utilisant les quatre versions SHA-2 (224, 256, 384, 512) et I boucle 300 fois. Je mesure les temps avec Boost.timer. Ce compteur de 300 boucles suffit à stabiliser complètement mes résultats. J'ai exécuté le test 5 fois, en alternant la version memcpy
et la version std::copy
version. Mon code tire parti de la récupération de données dans le plus grand nombre de morceaux possible (de nombreuses autres implémentations fonctionnent avec char
/char *
, alors que je travaille avec T
/T *
(où T
est le type le plus volumineux de l'implémentation de l'utilisateur qui présente un comportement de débordement correct), l'accès rapide à la mémoire sur les types les plus volumineux possibles est donc essentiel aux performances de mon algorithme. Ce sont mes résultats:
Temps (en secondes) requis pour 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 over memcpy: 2,99%
Mon compilateur est gcc 4.6.3 sur Fedora 16 x86_64. Mes drapeaux d'optimisation sont -Ofast -march=native -funsafe-loop-optimizations
.
Code pour mes implémentations SHA-2.
J'ai décidé de faire un test sur mon implémentation MD5 aussi. Les résultats étaient beaucoup moins stables, alors j'ai décidé de faire 10 essais. Cependant, après mes premières tentatives, les résultats variaient énormément d’une exécution à l’autre; je suppose donc qu’il y avait une activité quelconque liée au système d’exploitation. J'ai décidé de recommencer.
Paramètres et indicateurs du compilateur identiques. Il n'y a qu'une version de MD5, et elle est plus rapide que SHA-2. J'ai donc créé 3 000 boucles sur un ensemble similaire de 5 chaînes de test.
Voici mes 10 résultats finaux:
Temps (en secondes) requis pour 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 over memcpy: 0,11%
Code pour mon implémentation MD5
Ces résultats suggèrent qu'il existe une optimisation que std :: copy utilisée dans mes tests SHA-2 que std::copy
ne pouvait pas 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 que std::copy
plus vite encore. La réponse s’est avérée simple: activer 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 over memcpy: 0.72%
En résumé, l'utilisation de std::copy
. En fait, il semble y avoir un gain de performance.
Explication des résultats
Alors, pourquoi pourrait std::copy
donner un coup de pouce de performance?
Premièrement, je ne m'attendrais pas à ce que cela soit plus lent pour toute implémentation, tant que l'optimisation de l'inline est activée. Tous les compilateurs en ligne agressivement; il s’agit peut-être de l’optimisation la plus importante car elle permet de nombreuses 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 copiables de manière triviale 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 triviale de std::copy
qui diffère de memcpy
devrait répondre aux critères de votre compilateur, à savoir "toujours en ligne lors de l'optimisation pour la vitesse ou la taille".
Toutefois, std::copy
conserve également plus d'informations. Quand vous appelez std::copy
, la fonction garde les types intacts. memcpy
fonctionne sur void *
, qui rejette presque toutes les informations utiles. Par exemple, si je passe dans un tableau de std::uint64_t
, le compilateur ou le réalisateur de la bibliothèque peut tirer parti de l’alignement sur 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. Si tout est garanti pour être aligné, alors le code devient plus simple et plus rapide, et le prédicteur de branche de votre processeur est plus facile à obtenir.
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 rapidement avec n'importe quel 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 le chevauchement dans un sens (avec std::copy_backward
pour l’autre direction de chevauchement). memcpy
ne fonctionne que sur les 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 devriez simplement utiliser std::copy
lorsque vous devez copier des fragments de données.
Tous les compilateurs que je connais remplaceront un simple std::copy
avec memcpy
lorsque cela est approprié, voire mieux, vectorisez la copie pour qu’elle soit encore plus rapide que memcpy
.
En tout cas: profil et découvre toi-même. Différents compilateurs feront différentes choses, 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 fait GCC pour un simple std::copy
de type POD.
#include <algorithm>
struct foo
{
int x, y;
};
void bar(foo* a, foo* b, size_t n)
{
std::copy(a, a + n, b);
}
Voici le démontage (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 modifiez la signature de la fonction en
void bar(foo* __restrict a, foo* __restrict b, size_t n)
alors le memmove
devient un memcpy
pour une légère amélioration des performances. Notez que memcpy
lui-même sera fortement vectorisé.
Toujours utiliser std::copy
parce que memcpy
est limité aux structures POD de style C et que le compilateur remplacera probablement les appels à std::copy
avec memcpy
si les cibles sont en fait POD.
De plus, std::copy
peut être utilisé avec de nombreux types d'itérateurs, pas seulement les pointeurs. std::copy
est plus flexible sans perte de performance et est clairement gagnant.
En théorie, memcpy
pourrait avoir un léger , imperceptible , infinitésimal , avantage en termes de performances, uniquement parce qu'il n'a pas les mêmes exigences que std::copy
. De la page de manuel de memcpy
:
Pour éviter les débordements, la taille des tableaux pointés par les paramètres de destination et source doit être au moins num octets, et ne doit pas se chevaucher (pour le chevauchement mémoire, memmove est une approche plus sûre).
En d'autres termes, memcpy
peut ignorer la possibilité de chevauchement de données. (Passer des tableaux superposés à memcpy
représente un comportement indéfini.) Ainsi, memcpy
n'a pas besoin de vérifier explicitement cette condition, alors que std::copy
peut être utilisé tant que le paramètre OutputIterator
ne figure pas dans la plage source. Notez que n'est pas identique à dire que la plage source et la plage de destination ne peuvent pas se chevaucher.
Donc depuis std::copy
a des exigences quelque peu différentes, en théorie il devrait être légèrement (avec un accent extrême sur légèrement ) plus lent, car il vérifiera le chevauchement des tableaux C, 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 aucune différence.
Bien sûr, si vous ne travaillez pas avec POD , vous ne pouvez pas utiliser memcpy
de toute façon.
Ma règle est simple. Si vous utilisez C++, préférez les bibliothèques C++ et non C :)
Juste un ajout mineur: la différence de vitesse entre memcpy()
et std::copy()
peut varier considérablement si les optimisations sont activées ou non. Avec g ++ 6.2.0 et sans optimisations, memcpy()
gagne 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 semble à peu près pareil:
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()
, il est environ deux fois plus rapide lorsque les optimisations ne sont pas activées.
Code source (nécessite Google Benchmark):
#include <string.h>
#include <algorithm>
#include <vector>
#include <benchmark/benchmark.h>
constexpr int N = 10;
void bm_memcpy(benchmark::State& state)
{
std::vector<int> a(N);
std::vector<int> r(N);
while (state.KeepRunning())
{
memcpy(r.data(), a.data(), N * sizeof(int));
}
}
void bm_stdcopy(benchmark::State& state)
{
std::vector<int> a(N);
std::vector<int> r(N);
while (state.KeepRunning())
{
std::copy(a.begin(), a.end(), r.begin());
}
}
void bm_stdcopy_n(benchmark::State& state)
{
std::vector<int> a(N);
std::vector<int> 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 */
Si vous avez vraiment besoin de performances de copie maximales (ce que vous n’aurez peut-être pas), n’en utilisez aucun .
Il y a un lot qui peut être fait pour optimiser la copie en mémoire - encore plus si vous êtes prêt à utiliser plusieurs threads/cœurs pour cela. Voir par exemple:
Que manque-t-il/sous-optimal dans cette implémentation memcpy?
la question et certaines des réponses ont suggéré des implémentations ou des liens vers des implémentations.