web-dev-qa-db-fra.com

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 << "ERROR Not enough memory.\n";
    exit(1);
}

memcpy (bits, copyMe->bits, ((int *) copyMe->bits)[0]);
152
user576670

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.

183
David Stone

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é.

77
Peter Alexander

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.

23
Puppy

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.

17
Charles Salvia

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

11
UmmaGumma

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=1000memcpy(), 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 */
2
Grumbel

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.

2
einpoklum