web-dev-qa-db-fra.com

une alternative plus rapide à memcpy?

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

36
Tony Stark

memcpy est probablement le moyen le plus rapide de copier des octets dans la mémoire. Si vous avez besoin de quelque chose de plus rapide - essayez de trouver un moyen de ne pas copier des choses, par exemple. échanger des pointeurs uniquement, pas les données elles-mêmes.

117
nos

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 de mémoire rempli (2 emplacements, DDR4 de 16 Go chacun), 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 2 modules DDR4, c’est-à-dire que vous avez les 4 emplacements DDR4 occupés, vous pouvez obtenir une copie de mémoire supplémentaire deux fois plus rapide. Pour les systèmes de mémoire à trois (quatre) canaux, vous pouvez obtenir une copie de la mémoire 1,5 fois plus rapide si le code est étendu au code analogue AVX512. Avec AVX2, seuls les systèmes à trois ou quatre canaux avec tous les logements occupés ne sont pas censés être plus rapides, car pour les charger complètement, vous devez charger/stocker plus de 32 octets à la fois (48 octets pour trois et 64 octets pour quatre canaux). systèmes), tandis qu'AVX2 ne peut charger/stocker pas plus de 32 octets à la fois. Bien que le multithreading sur certains systèmes puisse résoudre ce problème sans AVX512 ni 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 le bloc aligné sur 32 octets.

Pour les blocs de taille non multiple et non alignés, le code prologue/épilogue peut être écrit en réduisant la largeur à 16 (SSE4.1), 8, 4, 2 et enfin 1 octet à la fois pour la tête et la queue du bloc. Également au milieu, 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 sur 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();
}

Une caractéristique essentielle de ce code est qu'il ignore le cache du processeur lors de la copie: lorsque le cache du processeur est impliqué (par exemple, des instructions AVX sans _stream_ sont utilisées), la vitesse de copie diminue plusieurs fois sur mon système.

Ma mémoire DDR4 est 2,6 GHz CL13. Ainsi, lors de la copie de 8 Go de données d'un tableau à un autre, j'ai obtenu 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 tampons d'entrée et de sortie est divisée 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, lors de la copie de 8 Go d'un tableau à un autre, vous effectuez une opération d'une valeur de 16 Go en accès mémoire.

Le multithreading modéré peut encore améliorer les performances environ 1,44 fois. L'augmentation totale par rapport à memcpy() atteint 2,55 fois sur ma machine . Voici comment les performances de copie de flux dépendent 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();
  }
}
14
Serge Rogatch

S'il vous plaît nous offrir plus de détails. Sur l'architecture i386, il est fort possible que memcpy soit le moyen le plus rapide de copier. Mais sur différentes architectures pour lesquelles 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 personnalisée ARM à l'aide du langage d'assemblage. Si vous transférez de gros morceaux de mémoire, alors DMA est probablement la réponse que vous recherchez.

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

11
INS

En fait, memcpy n’est PAS le moyen le plus rapide, surtout si vous l’appelez plusieurs fois. J'avais aussi besoin de code pour accélérer, et memcpy est lent car il contient trop de vérifications inutiles. Par exemple, il vérifie si les blocs de mémoire source et source se chevauchent et si la copie doit commencer à partir de l'arrière du bloc plutôt que de l'avant. Si de telles considérations ne vous intéressent pas, vous pouvez certainement faire beaucoup mieux. J'ai du code, mais voici peut-être une version encore meilleure:

Mémoire très rapide pour le traitement des images?

Si vous effectuez une recherche, vous pouvez également trouver d'autres implémentations. Mais pour une vitesse réelle, vous avez besoin d'une version Assembly.

6
user2009004

Généralement, la bibliothèque standard livrée avec le compilateur implémente déjà memcpy() de la manière la plus rapide possible pour la plate-forme cible.

6
sharptooth

Agner Fog a une implémentation rapide de la mémoire http://www.agner.org/optimize/#asmlib

4
KindDragon

Il est généralement plus rapide de ne pas en faire une copie. Je ne sais pas si vous pouvez adapter votre fonction pour ne pas copier, mais cela vaut la peine de chercher.

3

Parfois, des fonctions comme memcpy, memset, ... sont implémentées de deux manières différentes:

  • une fois comme une vraie fonction
  • une fois comme une assemblée qui est immédiatement en ligne

Tous les compilateurs ne prennent pas la version inlined-Assembly par défaut, votre compilateur peut utiliser la variante de fonction par défaut, ce qui entraîne une surcharge en raison de l’appel de la fonction . option de ligne, pragma, ...).

Edit: voir http://msdn.Microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx pour une explication des éléments intrinsèques du compilateur Microsoft C.

3
Patrick

Si votre plate-forme le supporte, vérifiez si vous pouvez utiliser l'appel système mmap () pour laisser vos données dans le fichier ... En général, le système d'exploitation peut mieux le gérer. Et, comme tout le monde le dit, évitez de copier si possible; les pointeurs sont vos amis dans des cas comme celui-ci.

2
Andrew McGregor

Vérifiez votre manuel de compilation/plate-forme. Pour certains microprocesseurs et kits DSP, utiliser memcpy est beaucoup plus lent que fonctions intrinsèques ou DMA operations 

2
Yousf

Cette fonction peut provoquer une exception d'abandon de données si l'un des pointeurs (arguments d'entrée) n'est pas aligné sur 32 bits. 

1
Amr Fawzy

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, c'est qu'un appel répété à la meilleure instruction ASM pour copier le plus grand volume 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 un simple movs tant qu'il sait combien de données il doit copier. Vous pouvez le voir si vous écrivez une memcpy avec une valeur bien déterminée (constexpr). Si le compilateur ne connaît pas la valeur, il devra revenir à l'implémentation de memcpy au niveau octet - le problème étant que memcpy doit respecter la granularité sur un octet. Il se déplace toujours de 128 bits à la fois, mais après chaque 128b, il devra vérifier si il a assez de données pour copier en 128b ou s'il doit revenir à 64bits, puis à 32 et 8 (je pense que 16 pourrait être sous-optimal de toute façon, mais je ne sais pas avec certitude).

Vous voulez donc pouvoir indiquer à 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 de passer à memcpy une variable qui ne sera connue qu’au moment de 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 simple boucle for vaut mieux que memcpy pour cette raison (élimination d'un appel de fonction). Et quel vous ne voulez vraiment pas est passé à memcpy un nombre impair d'octets à copier.

1
Dorin Lazăr

Vous voudrez peut-être jeter un coup d'œil à ceci:

http://www.danielvik.com/2010/02/fast-memcpy-in-c.html

Une autre idée que je voudrais essayer est d'utiliser les techniques COW pour dupliquer le bloc de mémoire et laisser le système d'exploitation gérer la copie à la demande dès que la page est écrite. mmap(): Puis-je faire un memcpy de copie sur écriture sous Linux?

1
hurikhan77

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

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

Cela voudrait dire que votre problème ne concerne pas ce que vous utilisez pour la copie, mais comment votre système utilise la mémoire. Vous devrez peut-être réduire le cache de fichiers ou commencer à écrire plus tôt, ou verrouiller les pages en mémoire, etc.

0
n-alexander

Je suppose que vous souhaitez copier d’énormes zones de mémoire si les performances de memcpy sont devenues un problème pour vous?

Dans ce cas, je serais d'accord avec la suggestion de nos nos de trouver un moyen de NE PAS copier des choses ..

Au lieu de disposer d'une énorme quantité de mémoire à copier chaque fois que vous devez la modifier, vous devriez probablement essayer d'autres structures de données.

Sans vraiment savoir quoi que ce soit de votre domaine problématique, je vous suggère de jeter un bon regard sur les structures de données persistantes et de mettre en œuvre l'une des vôtres ou de réutiliser une implémentation existante.

0
Roland Tepp

Voici une version C alternative de memcpy qui est inlineable et je trouve qu'elle surpasse de 50% celle de GCC pour Arm64 dans l'application pour laquelle je l'ai utilisé. Il est indépendant de la plate-forme 64 bits. Le traitement de queue peut être supprimé si l'instance d'utilisation n'en a pas besoin pour un peu plus de vitesse. Copie les tableaux uint32_t, les types de données plus petits non testés, mais pouvant fonctionner. Peut-être capable de s'adapter à d'autres types de données. Copie 64 bits (deux index sont copiés simultanément). 32 bits devrait également fonctionner, mais plus lentement. Crédits au projet Neoscrypt.

    static inline void newmemcpy(void *__restrict__ dstp, 
                  void *__restrict__ srcp, uint len)
        {
            ulong *dst = (ulong *) dstp;
            ulong *src = (ulong *) srcp;
            uint i, tail;

            for(i = 0; i < (len / sizeof(ulong)); i++)
                *dst++ = *src++;
            /*
              Remove below if your application does not need it.
              If console application, you can uncomment the printf to test
              whether tail processing is being used.
            */
            tail = len & (sizeof(ulong) - 1);
            if(tail) {
                //printf("tailused\n");
                uchar *dstb = (uchar *) dstp;
                uchar *srcb = (uchar *) srcp;

                for(i = len - tail; i < len; i++)
                    dstb[i] = srcb[i];
            }
        }
0
Rauli Kumpulainen

nos ont raison, vous l'appelez trop.

Pour voir d'où vous l'appelez et pourquoi, il suffit de le suspendre quelques fois sous le débogueur et de regarder la pile.

0
Mike Dunlavey