58 votes

Mauvaises performances de memcpy sous Linux

Nous avons récemment acheté de nouveaux serveurs et les performances de memcpy sont faibles. Les performances de memcpy sont 3x plus lentes sur les serveurs que sur nos ordinateurs portables.

Spécifications du serveur

  • Chassis et Mobo : SUPER MICRO 1027GR-TRF
  • CPU : 2x Intel Xeon E5-2680 @ 2.70 Ghz
  • Mémoire : 8x 16GB DDR3 1600MHz

Edit : Je teste également sur un autre serveur avec des spécifications légèrement supérieures et je vois les mêmes résultats que sur le serveur ci-dessus.

Spécifications du serveur 2

  • Chassis et Mobo : SUPER MICRO 10227GR-TRFT
  • Processeur : 2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
  • Mémoire : 8x 16GB DDR3 1866MHz

Spécifications des ordinateurs portables

  • Châssis : Lenovo W530
  • CPU : 1x Intel Core i7 i7-3720QM @ 2.6Ghz
  • Mémoire : 4x 4GB DDR3 1600MHz

Système d'exploitation

$ cat /etc/redhat-release
Scientific Linux release 6.5 (Carbon) 
$ uname -a                      
Linux r113 2.6.32-431.1.2.el6.x86_64 #1 SMP Thu Dec 12 13:59:19 CST 2013 x86_64 x86_64 x86_64 GNU/Linux

Compilateur (sur tous les systèmes)

$ gcc --version
gcc (GCC) 4.6.1

J'ai également testé avec gcc 4.8.2 suite à une suggestion de @stefan. Il n'y avait aucune différence de performance entre les compilateurs.

Code d'essai Le code de test ci-dessous est un test automatique pour reproduire le problème que je vois dans notre code de production. Je sais que ce benchmark est simpliste mais il a permis d'exploiter et d'identifier notre problème. Le code crée deux tampons de 1GB et les memcpys entre eux, en synchronisant l'appel memcpy. Vous pouvez spécifier d'autres tailles de tampon sur la ligne de commande en utilisant : ./big_memcpy_test [SIZE_BYTES].

#include <chrono>
#include <cstring>
#include <iostream>
#include <cstdint>

class Timer
{
 public:
  Timer()
      : mStart(),
        mStop()
  {
    update();
  }

  void update()
  {
    mStart = std::chrono::high_resolution_clock::now();
    mStop  = mStart;
  }

  double elapsedMs()
  {
    mStop = std::chrono::high_resolution_clock::now();
    std::chrono::milliseconds elapsed_ms =
        std::chrono::duration_cast<std::chrono::milliseconds>(mStop - mStart);
    return elapsed_ms.count();
  }

 private:
  std::chrono::high_resolution_clock::time_point mStart;
  std::chrono::high_resolution_clock::time_point mStop;
};

std::string formatBytes(std::uint64_t bytes)
{
  static const int num_suffix = 5;
  static const char* suffix[num_suffix] = { "B", "KB", "MB", "GB", "TB" };
  double dbl_s_byte = bytes;
  int i = 0;
  for (; (int)(bytes / 1024.) > 0 && i < num_suffix;
       ++i, bytes /= 1024.)
  {
    dbl_s_byte = bytes / 1024.0;
  }

  const int buf_len = 64;
  char buf[buf_len];

  // use snprintf so there is no buffer overrun
  int res = snprintf(buf, buf_len,"%0.2f%s", dbl_s_byte, suffix[i]);

  // snprintf returns number of characters that would have been written if n had
  //       been sufficiently large, not counting the terminating null character.
  //       if an encoding error occurs, a negative number is returned.
  if (res >= 0)
  {
    return std::string(buf);
  }
  return std::string();
}

void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
  memmove(pDest, pSource, sizeBytes);
}

int main(int argc, char* argv[])
{
  std::uint64_t SIZE_BYTES = 1073741824; // 1GB

  if (argc > 1)
  {
    SIZE_BYTES = std::stoull(argv[1]);
    std::cout << "Using buffer size from command line: " << formatBytes(SIZE_BYTES)
              << std::endl;
  }
  else
  {
    std::cout << "To specify a custom buffer size: big_memcpy_test [SIZE_BYTES] \n"
              << "Using built in buffer size: " << formatBytes(SIZE_BYTES)
              << std::endl;
  }

  // big array to use for testing
  char* p_big_array = NULL;

  /////////////
  // malloc 
  {
    Timer timer;

    p_big_array = (char*)malloc(SIZE_BYTES * sizeof(char));
    if (p_big_array == NULL)
    {
      std::cerr << "ERROR: malloc of " << SIZE_BYTES << " returned NULL!"
                << std::endl;
      return 1;
    }

    std::cout << "malloc for " << formatBytes(SIZE_BYTES) << " took "
              << timer.elapsedMs() << "ms"
              << std::endl;
  }

  /////////////
  // memset
  {
    Timer timer;

    // set all data in p_big_array to 0
    memset(p_big_array, 0xF, SIZE_BYTES * sizeof(char));

    double elapsed_ms = timer.elapsedMs();
    std::cout << "memset for " << formatBytes(SIZE_BYTES) << " took "
              << elapsed_ms << "ms "
              << "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
              << std::endl;
  }

  /////////////
  // memcpy 
  {
    char* p_dest_array = (char*)malloc(SIZE_BYTES);
    if (p_dest_array == NULL)
    {
      std::cerr << "ERROR: malloc of " << SIZE_BYTES << " for memcpy test"
                << " returned NULL!"
                << std::endl;
      return 1;
    }
    memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));

    // time only the memcpy FROM p_big_array TO p_dest_array
    Timer timer;

    memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));

    double elapsed_ms = timer.elapsedMs();
    std::cout << "memcpy for " << formatBytes(SIZE_BYTES) << " took "
              << elapsed_ms << "ms "
              << "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
              << std::endl;

    // cleanup p_dest_array
    free(p_dest_array);
    p_dest_array = NULL;
  }

  /////////////
  // memmove
  {
    char* p_dest_array = (char*)malloc(SIZE_BYTES);
    if (p_dest_array == NULL)
    {
      std::cerr << "ERROR: malloc of " << SIZE_BYTES << " for memmove test"
                << " returned NULL!"
                << std::endl;
      return 1;
    }
    memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));

    // time only the memmove FROM p_big_array TO p_dest_array
    Timer timer;

    // memmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));
    doMemmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));

    double elapsed_ms = timer.elapsedMs();
    std::cout << "memmove for " << formatBytes(SIZE_BYTES) << " took "
              << elapsed_ms << "ms "
              << "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
              << std::endl;

    // cleanup p_dest_array
    free(p_dest_array);
    p_dest_array = NULL;
  }

  // cleanup
  free(p_big_array);
  p_big_array = NULL;

  return 0;
}

Fichier CMake à construire

project(big_memcpy_test)
cmake_minimum_required(VERSION 2.4.0)

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

# create verbose makefiles that show each command line as it is issued
set( CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "Verbose" FORCE )
# release mode
set( CMAKE_BUILD_TYPE Release )
# grab in CXXFLAGS environment variable and append C++11 and -Wall options
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall -march=native -mtune=native" )
message( INFO "CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}" )

# sources to build
set(big_memcpy_test_SRCS
  main.cpp
)

# create an executable file named "big_memcpy_test" from
# the source files in the variable "big_memcpy_test_SRCS".
add_executable(big_memcpy_test ${big_memcpy_test_SRCS})

Résultats des tests

Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | NUMA nodes (numactl --hardware)
---------------------------------------------------------------------------------------------
Laptop 1         | 0           | 127         | 113         | 1
Laptop 2         | 0           | 180         | 120         | 1
Server 1         | 0           | 306         | 301         | 2
Server 2         | 0           | 352         | 325         | 2

Comme vous pouvez le constater, les memcpys et memsets de nos serveurs sont beaucoup plus lents que les memcpys et memsets de nos ordinateurs portables.

Variation de la taille des tampons

J'ai essayé des tampons de 100 Mo à 5 Go avec des résultats similaires (serveurs plus lents que l'ordinateur portable).

Affinité NUMA

J'ai lu que des personnes avaient des problèmes de performance avec NUMA, j'ai donc essayé de définir l'affinité du CPU et de la mémoire en utilisant numactl mais les résultats sont restés les mêmes.

Matériel NUMA pour serveurs

$ numactl --hardware                                                            
available: 2 nodes (0-1)                                                                     
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23                                         
node 0 size: 65501 MB                                                                        
node 0 free: 62608 MB                                                                        
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31                                   
node 1 size: 65536 MB                                                                        
node 1 free: 63837 MB                                                                        
node distances:                                                                              
node   0   1                                                                                 
  0:  10  21                                                                                 
  1:  21  10 

Matériel NUMA pour ordinateurs portables

$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 16018 MB
node 0 free: 6622 MB
node distances:
node   0 
  0:  10

Réglage de l'affinité NUMA

$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test

Toute aide pour résoudre ce problème est la bienvenue.

Editer : Options GCC

Sur la base des commentaires, j'ai essayé de compiler avec différentes options GCC :

Compilation avec -march et -mtune en mode natif

g++ -std=c++0x -Wall -march=native -mtune=native -O3 -DNDEBUG -o big_memcpy_test main.cpp 

Résultat : Exactement les mêmes performances (aucune amélioration)

Compilation avec -O2 au lieu de -O3

g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp

Résultat : Exactement les mêmes performances (aucune amélioration)

Edit : Changed memset to write 0xF instead of 0 to avoid NULL page (@SteveCox)

Pas d'amélioration lors du memsetting avec une valeur autre que 0 (utilisé 0xF dans ce cas).

Edit : Résultats de Cachebench

Afin d'exclure que mon programme de test soit trop simpliste, j'ai téléchargé un vrai programme de benchmarking LLCacheBench ( http://icl.cs.utk.edu/projects/llcbench/cachebench.html )

J'ai construit le benchmark sur chaque machine séparément pour éviter les problèmes d'architecture. Voici mes résultats.

laptop vs server memcpy performance

Remarquez la TRES grande différence de performance sur les plus grandes tailles de tampon. La dernière taille testée (16777216) a atteint 18849.29 MB/sec sur l'ordinateur portable et 6710.40 sur le serveur. Cela représente une différence de performance de 3x. Vous pouvez également remarquer que la baisse de performance sur le serveur est beaucoup plus importante que sur l'ordinateur portable.

Edit : memmove() est 2x plus rapide que memcpy() sur le serveur.

Sur la base de quelques expérimentations, j'ai essayé d'utiliser memmove() au lieu de memcpy() dans mon cas de test et j'ai trouvé une amélioration de 2x sur le serveur. Memmove() sur l'ordinateur portable est plus lent que memcpy() mais curieusement, il fonctionne à la même vitesse que memmove() sur le serveur. Cela nous amène à la question suivante : pourquoi memcpy est-il si lent ?

Mise à jour du code pour tester memmove avec memcpy. J'ai dû envelopper le memmove() à l'intérieur d'une fonction parce que si je le laissais en ligne, GCC l'optimisait et exécutait exactement la même chose que memcpy() (je suppose que gcc l'a optimisé pour memcpy parce qu'il savait que les emplacements ne se chevauchaient pas).

Résultats actualisés

Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | memmove() | NUMA nodes (numactl --hardware)
---------------------------------------------------------------------------------------------------------
Laptop 1         | 0           | 127         | 113         | 161       | 1
Laptop 2         | 0           | 180         | 120         | 160       | 1
Server 1         | 0           | 306         | 301         | 159       | 2
Server 2         | 0           | 352         | 325         | 159       | 2

Editer : Memcpy naïf

Sur la base de la suggestion de @Salgar, j'ai implémenté ma propre fonction memcpy naïve et l'ai testée.

Memcpy naïf Source

void naiveMemcpy(void* pDest, const void* pSource, std::size_t sizeBytes)
{
  char* p_dest = (char*)pDest;
  const char* p_source = (const char*)pSource;
  for (std::size_t i = 0; i < sizeBytes; ++i)
  {
    *p_dest++ = *p_source++;
  }
}

Résultats de Memcpy naïf comparé à memcpy()

Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop 1         | 113         | 161         | 160
Server 1         | 301         | 159         | 159
Server 2         | 325         | 159         | 159

Edit : Sortie d'assemblage

Source simple de memcpy

#include <cstring>
#include <cstdlib>

int main(int argc, char* argv[])
{
  size_t SIZE_BYTES = 1073741824; // 1GB

  char* p_big_array  = (char*)malloc(SIZE_BYTES * sizeof(char));
  char* p_dest_array = (char*)malloc(SIZE_BYTES * sizeof(char));

  memset(p_big_array,  0xA, SIZE_BYTES * sizeof(char));
  memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));

  memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));

  free(p_dest_array);
  free(p_big_array);

  return 0;
}

Sortie d'assemblage : C'est exactement la même chose sur le serveur et l'ordinateur portable. J'économise de l'espace et ne colle pas les deux.

        .file   "main_memcpy.cpp"
        .section        .text.startup,"ax",@progbits
        .p2align 4,,15
        .globl  main
        .type   main, @function
main:
.LFB25:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movl    $1073741824, %edi
        pushq   %rbx
        .cfi_def_cfa_offset 24
        .cfi_offset 3, -24
        subq    $8, %rsp
        .cfi_def_cfa_offset 32
        call    malloc
        movl    $1073741824, %edi
        movq    %rax, %rbx
        call    malloc
        movl    $1073741824, %edx
        movq    %rax, %rbp
        movl    $10, %esi
        movq    %rbx, %rdi
        call    memset
        movl    $1073741824, %edx
        movl    $15, %esi
        movq    %rbp, %rdi
        call    memset
        movl    $1073741824, %edx
        movq    %rbx, %rsi
        movq    %rbp, %rdi
        call    memcpy
        movq    %rbp, %rdi
        call    free
        movq    %rbx, %rdi
        call    free
        addq    $8, %rsp
        .cfi_def_cfa_offset 24
        xorl    %eax, %eax
        popq    %rbx
        .cfi_def_cfa_offset 16
        popq    %rbp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE25:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.6.1"
        .section        .note.GNU-stack,"",@progbits

PROGRESS !!!! asmlib

Sur la base de la suggestion de @tbenson, j'ai essayé d'exécuter avec la fonction asmlib version de memcpy. Au départ, mes résultats étaient médiocres, mais après avoir changé SetMemcpyCacheLimit() à 1GB (taille de mon tampon), je fonctionnais à une vitesse comparable à celle de ma boucle for naïve !

La mauvaise nouvelle est que la version asmlib de memmove est plus lente que la version glibc, elle tourne maintenant autour de 300 ms (à égalité avec la version glibc de memcpy). Ce qui est étrange, c'est que sur l'ordinateur portable, lorsque je fixe la valeur de la fonction SetMemcpyCacheLimit() à un grand nombre, les performances s'en ressentent...

Dans les résultats ci-dessous, les lignes marquées avec SetCache ont SetMemcpyCacheLimit défini à 1073741824. Les résultats sans SetCache n'appellent pas SetMemcpyCacheLimit().

Résultats utilisant les fonctions de asmlib :

Buffer Size: 1GB  | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop            | 136         | 132         | 161
Laptop SetCache   | 182         | 137         | 161
Server 1          | 305         | 302         | 164
Server 1 SetCache | 162         | 303         | 164
Server 2          | 300         | 299         | 166
Server 2 SetCache | 166         | 301         | 166

Je commence à penser à un problème de cache, mais quelle en serait la cause ?

16voto

tbenson Points 176

[J'en ferais bien un commentaire, mais je n'ai pas assez de réputation pour le faire].

J'ai un système similaire et je vois des résultats similaires, mais je peux ajouter quelques points de données :

  • Si vous inversez la direction de votre naïveté memcpy (c'est-à-dire convertir en *p_dest-- = *p_src-- ), alors vous risquez d'obtenir des performances bien pires que pour la direction avant (~637 ms pour moi). Il y a eu un changement dans memcpy() dans la glibc 2.12 qui exposait plusieurs bogues pour l'appel de memcpy sur des tampons qui se chevauchent ( http://lwn.net/Articles/414467/ ) et je pense que le problème a été causé par le passage à une version de memcpy qui fonctionne à l'envers. Donc, les copies à l'envers par rapport aux copies à l'endroit peuvent expliquer la memcpy() / memmove() disparité.
  • Il semble préférable de ne pas utiliser de magasins non temporels. De nombreuses solutions optimisées memcpy() Les implémentations passent à des magasins non temporels (qui ne sont pas mis en cache) pour les grands buffers (c'est-à-dire plus grands que le cache de dernier niveau). J'ai testé la version de memcpy d'Agner Fog ( http://www.agner.org/optimize/#asmlib ) et a constaté qu'elle était approximativement de la même vitesse que la version en glibc . Cependant, asmlib a une fonction ( SetMemcpyCacheLimit ) qui permet de fixer le seuil au-dessus duquel les magasins non temporels sont utilisés. En fixant cette limite à 8 Go (ou juste plus grand que le tampon de 1 Go) pour éviter les stockages non temporels, j'ai doublé les performances dans mon cas (temps réduit à 176 ms). Bien sûr, cela ne correspond qu'aux performances naïves dans le sens direct, ce n'est donc pas extraordinaire.
  • Le BIOS de ces systèmes permet d'activer/désactiver quatre préfacteurs matériels différents (MLC Streamer Prefetcher, MLC Spatial Prefetcher, DCU Streamer Prefetcher et DCU IP Prefetcher). J'ai essayé de désactiver chacun d'entre eux, mais en le faisant, j'ai au mieux maintenu la parité des performances et j'ai réduit les performances pour quelques-uns des paramètres.
  • La désactivation du mode DRAM RAPL (running average power limit) n'a aucun impact.
  • J'ai accès à d'autres systèmes Supermicro exécutant Fedora 19 (glibc 2.17). Avec une carte Supermicro X9DRG-HF, Fedora 19 et des processeurs Xeon E5-2670, je constate des performances similaires à celles mentionnées ci-dessus. Sur une carte Supermicro X10SLM-F à socket unique exécutant un Xeon E3-1275 v3 (Haswell) et Fedora 19, je vois 9,6 Go/s pour memcpy (104 ms). La RAM du système Haswell est de type DDR3-1600 (comme les autres systèmes).

MISES À JOUR

  • J'ai réglé la gestion de l'alimentation du CPU sur Max Performance et désactivé l'hyperthreading dans le BIOS. D'après /proc/cpuinfo les cœurs ont alors été cadencés à 3 GHz. Cependant, cela a bizarrement diminué les performances de la mémoire d'environ 10 %.
  • memtest86+ 4.10 rapporte une bande passante vers la mémoire principale de 9091 MB/s. Je n'ai pas pu trouver si cela correspond à la lecture, l'écriture ou la copie.
  • El Repère STREAM rapporte 13422 MB/s pour la copie, mais ils comptent les octets à la fois comme lus et écrits, donc cela correspond à ~6.5 GB/s si nous voulons comparer aux résultats ci-dessus.

7voto

bokan Points 2097

Cela me semble normal.

Gérer des bâtons de mémoire ECC de 8x16GB avec deux CPU est une tâche bien plus difficile qu'un seul CPU avec 2x2GB. Vos sticks de 16GB sont des mémoires double face + ils peuvent avoir des buffers + ECC (même désactivés au niveau de la carte mère)... tout cela rend le chemin des données vers la RAM beaucoup plus long. Vous avez également 2 CPU qui partagent la RAM, et même si vous ne faites rien sur l'autre CPU, il y a toujours un petit accès à la mémoire. La commutation de ces données nécessite un temps supplémentaire. Il suffit de regarder l'énorme perte de performance sur les PC qui partagent une partie de la RAM avec la carte graphique.

Pourtant vos seveurs sont des pompes à données très puissantes. Je ne suis pas sûr que dupliquer 1GB se produise très souvent dans les logiciels réels, mais je suis sûr que vos 128GB sont beaucoup plus rapides que n'importe quel disque dur, même le meilleur SSD et c'est là que vous pouvez tirer avantage de vos serveurs. Faire le même test avec 3 Go mettra le feu à votre ordinateur portable.

Cela semble être l'exemple parfait de la façon dont une architecture basée sur du matériel de base pourrait être beaucoup plus efficace que les gros serveurs. Combien de PC grand public pourrait-on s'offrir avec l'argent dépensé pour ces gros serveurs ?

Merci pour votre question très détaillée.

EDIT : (J'ai mis tellement de temps à écrire cette réponse que j'ai manqué la partie graphique).

Je pense que le problème vient de l'endroit où les données sont stockées. Pouvez-vous s'il vous plaît comparer ceci :

  • test un : allouer deux blocs contigus de 500Mb de ram et copier de l'un à l'autre (ce que vous avez déjà fait)
  • test deux : allouez 20 (ou plus) blocs de 500Mo de mémoire et copiez du premier au dernier, de façon à ce qu'ils soient éloignés les uns des autres (même si vous ne pouvez pas être sûr de leur position réelle).

De cette façon, vous verrez comment le contrôleur de mémoire gère les blocs de mémoire éloignés les uns des autres. Je pense que vos données sont placées sur différentes zones de la mémoire et qu'il faut une opération de commutation à un moment donné sur le chemin des données pour parler avec une zone puis l'autre (il y a un tel problème avec la mémoire double face).

De plus, vous assurez-vous que le thread est lié à un seul CPU ?

EDIT 2 :

Il existe plusieurs types de délimiteurs de "zones" pour la mémoire. NUMA en est un, mais ce n'est pas le seul. Par exemple, les sticks à deux faces nécessitent un drapeau pour adresser un côté ou l'autre. Regardez sur votre graphique comment les performances se dégradent avec de gros morceaux de mémoire même sur l'ordinateur portable (qui n'a pas de NUMA). Je ne suis pas sûr de cela, mais memcpy peut utiliser une fonction matérielle pour copier la RAM (une sorte de DMA) et cette puce doit avoir moins de cache que votre CPU, cela pourrait expliquer pourquoi la copie muette avec le CPU est plus rapide que memcpy.

6voto

Leeor Points 6919

Il est possible que certaines améliorations du processeur de votre ordinateur portable basé sur IvyBridge contribuent à ce gain par rapport aux serveurs basés sur SandyBridge.

  1. Préfetch à travers la page - le processeur de votre ordinateur portable prélèverait la page linéaire suivante dès que vous atteindriez la fin de la page actuelle, ce qui vous éviterait de manquer la TLB à chaque fois. Pour essayer d'atténuer ce problème, essayez de construire votre code serveur pour des pages de 2M / 1G.

  2. Les systèmes de remplacement du cache semblent également avoir été améliorés (voir une intéressante rétro-ingénierie aquí ). Si ce processeur utilise effectivement une politique d'insertion dynamique, cela empêcherait facilement vos données copiées d'essayer de détruire votre Last-Level-Cache (qu'il ne peut pas utiliser efficacement de toute façon à cause de sa taille), et garderait la place pour d'autres caches utiles comme le code, la pile, les données de la table des pages, etc ). Pour tester cela, vous pouvez essayer de reconstruire votre implémentation naïve en utilisant des chargements/stockages en streaming ( movntdq ou similaires, vous pouvez aussi utiliser le buildin de gcc pour cela). Cette possibilité peut expliquer la chute soudaine de la taille des grands ensembles de données.

  3. Je crois que certaines améliorations ont également été apportées à string-copy ( aquí ), il peut ou non s'appliquer ici, selon la façon dont votre code d'assemblage se présente. Vous pouvez essayer d'effectuer un benchmarking avec Dhrystone pour tester s'il y a une différence inhérente. Cela peut également expliquer la différence entre memcpy et memmove.

Si vous pouviez mettre la main sur un serveur basé sur IvyBridge ou un ordinateur portable Sandy-Bridge, il serait plus simple de tester tous ces éléments ensemble.

1voto

stark Points 4072

J'ai modifié le benchmark pour utiliser le timer nsec sous Linux et j'ai trouvé des variations similaires sur différents processeurs, tous avec une mémoire similaire. Les chiffres sont cohérents sur plusieurs exécutions.

Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, L2/L3 256K/20M, 16 GB ECC
malloc for 1073741824 took 47us 
memset for 1073741824 took 643841us
memcpy for 1073741824 took 486591us 

Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, L2/L3 256K/12M, 12 GB ECC
malloc for 1073741824 took 54us
memset for 1073741824 took 789656us 
memcpy for 1073741824 took 339707us

Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, L2 256K/8M, 12 GB ECC
malloc for 1073741824 took 126us
memset for 1073741824 took 280107us 
memcpy for 1073741824 took 272370us

Voici les résultats avec le code C en ligne -O3

Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, 256K/20M, 16 GB
malloc for 1 GB took 46 us
memset for 1 GB took 478722 us
memcpy for 1 GB took 262547 us

Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, 256K/12M, 12 GB
malloc for 1 GB took 53 us
memset for 1 GB took 681733 us
memcpy for 1 GB took 258147 us

Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, 256K/8M, 12 GB
malloc for 1 GB took 67 us
memset for 1 GB took 254544 us
memcpy for 1 GB took 255658 us

Pour le plaisir, j'ai aussi essayé de faire en sorte que le memcpy en ligne fasse 8 octets à la fois. Sur ces processeurs Intel, cela n'a fait aucune différence notable. Le cache fusionne toutes les opérations d'octets dans le nombre minimum d'opérations de mémoire. Je soupçonne le code de la bibliothèque gcc d'essayer d'être trop intelligent.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X