web-dev-qa-db-fra.com

Mauvaise performance mémoire sous Linux

Nous avons récemment acheté de nouveaux serveurs et nos performances de mémoire sont médiocres. La performance mémoire est 3 fois plus lente sur les serveurs par rapport à nos ordinateurs portables.

Spécifications du serveur

  • Châssis et Mobo: SUPER MICRO 1027GR-TRF
  • CPU: 2x Intel Xeon E5-2680 @ 2,70 Ghz
  • Mémoire: 8x 16 Go DDR3 1600 MHz

Edit: je teste également sur un autre serveur avec des spécifications légèrement plus élevées et affiche les mêmes résultats que le serveur ci-dessus

Spécifications du serveur 2

  • Châssis et Mobo: SUPER MICRO 10227GR-TRFT
  • CPU: 2x Intel Xeon E5-2650 v2 @ 2,6 Ghz
  • Mémoire: 8x 16 Go DDR3 1866 MHz

Spécifications de l'ordinateur portable

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

Système opérateur

$ 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

Également testé avec gcc 4.8.2 sur une suggestion de @stefan. Il n'y avait pas de différence de performance entre les compilateurs.

Code de test Le code de test ci-dessous est un test prédéfini permettant de dupliquer le problème rencontré dans notre code de production. Je sais que ce repère est simpliste, mais il a pu exploiter et identifier notre problème. Le code crée deux tampons de 1 Go et memcpys entre eux, chronométrant l'appel memcpy. Vous pouvez spécifier d'autres tailles de tampon sur la ligne de commande à l'aide de: ./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;
}

CMake Fichier à 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 de test

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 les memsets sur nos serveurs sont beaucoup plus lents que les memcpys et les memsets de nos ordinateurs portables.

Tailles de tampon variables

J'ai essayé des mémoires tampons de 100 Mo à 5 Go avec des résultats similaires (serveurs plus lents que les ordinateurs portables)

Affinité NUMA

J'ai entendu parler de personnes ayant des problèmes de performances avec NUMA. J'ai donc essayé de définir l'affinité entre mémoire et processeur avec numactl, mais les résultats sont restés les mêmes.

Serveur NUMA Server

$ 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 

Ordinateur portable NUMA

$ 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

Définition de l'affinité NUMA

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

Toute aide permettant de résoudre ce problème est grandement appréciée.

Edit: Options GCC

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

Compilation avec -march et -mtune réglés en natif

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

Résultat: même performance (pas d'amélioration)

Compiler 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: même performance (pas d'amélioration)

Edit: memset modifié pour écrire 0xF au lieu de 0 pour éviter la page NULL (@SteveCox)

Aucune amélioration en cas de memsetting avec une valeur autre que 0 (0xF utilisé dans ce cas).

Edit: Résultats Cachebench

Pour éviter que mon programme de test soit trop simpliste, j'ai téléchargé un véritable programme d'analyse comparative, LLCacheBench ( http://icl.cs.utk.edu/projects/llcbench/cachebench.html )

J'ai construit le test de performance sur chaque machine séparément pour éviter les problèmes d'architecture. Ci-dessous sont mes résultats.

laptop vs server memcpy performance

Notez que la très grande différence réside dans les performances des tampons de grande taille. La dernière taille testée (16777216) a atteint 18849,29 Mo/sec sur l’ordinateur portable et 6710,40 sur le serveur. C'est à peu près une différence de performance 3x. Vous pouvez également noter que la baisse de performances du serveur est beaucoup plus forte que sur un ordinateur portable.

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

Sur la base d'expérimentations, j'ai essayé d'utiliser memmove () au lieu de memcpy () dans mon cas de test et j'ai constaté une amélioration 2x du serveur. Memmove () sur l'ordinateur portable s'exécute plus lentement que memcpy () mais curieusement, il s'exécute à la même vitesse que memmove () sur le serveur. Cela pose la question, pourquoi la mémoire est-elle si lente?

Code mis à jour pour tester memmove avec memcpy. Je devais envelopper la fonction memmove () dans une fonction, car si je la laissais en ligne, GCC l’optimalisa et exécuta exactement la même chose que memcpy () (je suppose que gcc l’optimisa à memcpy car il savait que les emplacements ne se chevauchaient pas).

Résultats mis à jour

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

Edit: Naive Memcpy

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

Naive Memcpy 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 naïfs de Memcpy comparés à 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 de l'assemblage

Memcpy source simple

#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 sur l'ordinateur portable. Je gagne de la place 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

LE PROGRÈS!!!! asmlib

Sur la suggestion de @tbenson, j’ai essayé de fonctionner avec la version asmlib de memcpy. Mes résultats étaient initialement médiocres, mais après avoir changé SetMemcpyCacheLimit () en 1 Go (taille de ma mémoire tampon), je courais à la vitesse de la boucle naïve pour moi!

La mauvaise nouvelle est que la version asmlib de memmove est plus lente que la version glibc, elle tourne maintenant à la marque 300 ms (au même niveau que la version glibc de memcpy). Ce qui est étrange, c’est que sur un ordinateur portable, lorsque je mets un nombre important à SetMemcpyCacheLimit (), les performances en pâtissent ...

Dans les résultats ci-dessous, SetMemcpyCacheLimit est défini sur 1073741824 avec SetMache. 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

Nous commençons à nous pencher vers le problème du cache, mais quelle en serait la cause?

69
nick

[Je voudrais en faire 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 memcpy naïve (c'est-à-dire que vous convertissez en *p_dest-- = *p_src--), vous obtiendrez peut-être des performances bien plus mauvaises que dans la direction avant (~ 637 ms pour moi). Un changement intervenu dans memcpy() dans la glibc 2.12 a mis à jour plusieurs bugs liés à l'appel de memcpy sur des tampons se chevauchant ( http://lwn.net/Articles/414467/ ) et j'estime que le problème était dû au passage à une version memcpy qui fonctionne à l'envers. Ainsi, les copies inversées par rapport aux copies antérieures peuvent expliquer la disparité entre memcpy()memmove().
  • IL SEMBLE PR&EACUTE;F&EACUTE;RABLE DE NE PAS UTILISER DE MAGASINS NON TEMPORELS. DE NOMBREUSES IMPL&EACUTE;MENTATIONS memcpy() OPTIMIS&EACUTE;ES BASCULENT VERS DES MAGASINS NON TEMPORELS (QUI NE SONT PAS MIS EN CACHE) POUR LES M&EACUTE;MOIRES TAMPONS VOLUMINEUSES (C'EST-&AGRAVE;-DIRE PLUS GRANDES QUE LE CACHE DE DERNIER NIVEAU). J'AI TEST&EACUTE; LA VERSION DE MEMCPY D'AGNER FOG ( HTTP://WWW.AGNER.ORG/OPTIMIZE/#ASMLIB ) ET J'AI CONSTAT&EACUTE; QUE LA VITESSE &EACUTE;TAIT &AGRAVE; PEU PR&EGRAVE;S &EACUTE;QUIVALENTE &AGRAVE; CELLE DE LA VERSION DANS glibc. CEPENDANT, asmlib A UNE FONCTION (SetMemcpyCacheLimit) QUI PERMET DE D&EACUTE;FINIR LE SEUIL AU-DESSUS DUQUEL LES MAGASINS NON TEMPORELS SONT UTILIS&EACUTE;S. D&EACUTE;FINIR CETTE LIMITE &AGRAVE; 8 GO (OU JUSTE PLUS GRANDE QUE LA M&EACUTE;MOIRE TAMPON 1 GIB) POUR &EACUTE;VITER QUE LES M&EACUTE;MOIRES NON TEMPORELLES NE DOUBLENT LES PERFORMANCES DANS MON CAS (DUR&EACUTE;E JUSQU'&AGRAVE; 176 MS). BIEN S&UCIRC;R, CELA NE CORRESPONDAIT QU'&AGRAVE; LA PERFORMANCE NA&IUML;VE DANS LA DIRECTION AVANT, DONC CE N'EST PAS STELLAIRE.
  • LE BIOS DE CES SYST&EGRAVE;MES AUTORISE L'ACTIVATION/LA D&EACUTE;SACTIVATION DE QUATRE PR&EACUTE;-ANALYSEURS MAT&EACUTE;RIELS DIFF&EACUTE;RENTS (MLC STREAMER PREFETCHER, MLC SPATIAL PREFETCHER, DCU STREAMER PREFETCHER ET DCU IP PREFETCHER). J'AI ESSAY&EACUTE; DE LES D&EACUTE;SACTIVER, MAIS EN CONSERVANT AU MIEUX LA PARIT&EACUTE; DES PERFORMANCES ET EN R&EACUTE;DUISANT LES PERFORMANCES POUR QUELQUES-UNS DES PARAM&EGRAVE;TRES.
  • LA D&EACUTE;SACTIVATION DU MODE DRAM RAPL (LIMITE DE PUISSANCE MOYENNE COURANTE) N&RSQUO;A AUCUN IMPACT.
  • J'AI ACC&EGRAVE;S &AGRAVE; D'AUTRES SYST&EGRAVE;MES SUPERMICRO EX&EACUTE;CUTANT FEDORA 19 (GLIBC 2.17). AVEC UNE CARTE SUPERMICRO X9DRG-HF, FEDORA 19 ET XEON E5-2670, LES PERFORMANCES SONT SIMILAIRES &AGRAVE; CELLES D&EACUTE;CRITES CI-DESSUS. SUR UNE CARTE &AGRAVE; SOCKET UNIQUE SUPERMICRO X10SLM-F EX&EACUTE;CUTANT UN XEON E3-1275 V3 (HASWELL) ET FEDORA 19, JE VOIS 9,6 GO/S POUR memcpy (104 MS). LE RAM SUR LE SYST&EGRAVE;ME HASWELL EST DDR3-1600 (IDENTIQUE AUX AUTRES SYST&EGRAVE;MES).

_/ _ MISES &AGRAVE; JOUR

  • Je règle la gestion de l'alimentation du processeur sur Max Performance et désactive l'hyperthreading dans le BIOS. Sur la base de /proc/cpuinfo, les cœurs ont ensuite été cadencés à 3 GHz. Cependant, cela a étrangement diminué les performances de la mémoire d'environ 10%.
  • memtest86 + 4.10 indique une bande passante dans la mémoire principale de 9091 Mo/s. Je n'ai pas pu trouver si cela correspond à lire, écrire ou copier.
  • Le STREAM Benchmark indique 13422 Mo/s pour la copie, mais ils comptent les octets lus et écrits, ce qui correspond à environ 6,5 Go/s si nous voulons comparer les résultats ci-dessus.
23
tbenson

Cela me semble normal.

La gestion de cartes mémoire ECC de 8x16 Go avec deux processeurs est un travail beaucoup plus difficile qu’un processeur unique avec 2x2GB. Vos clés de 16 Go sont une mémoire double face + elles peuvent avoir des tampons + ECC (même désactivées au niveau de la carte mère) ... tout ce qui rend le chemin de données vers RAM beaucoup plus long. Vous avez également 2 processeurs partageant le ram, et même si vous ne faites rien sur l'autre processeur, l'accès à la mémoire est toujours limité. Changer ces données demande un peu plus de temps. Il suffit de regarder l’énorme performance perdue sur les PC qui partagent une certaine RAM avec une carte graphique.

Néanmoins, vos serveurs sont toujours des pompes de données puissantes. Je ne suis pas sûr que la duplication de 1 Go se produise très souvent dans les logiciels réels, mais je suis sûre que vos 128 Go sont beaucoup plus rapides que tous les disques durs, même les meilleurs disques SSD, et c’est là que vous pouvez tirer parti 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 manière dont une architecture basée sur du matériel standard peut être beaucoup plus efficace que de gros serveurs. Combien de PC grand public pourrait-on se permettre avec l'argent dépensé sur ces gros serveurs?

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

EDIT: (Il m'a fallu tellement de temps pour écrire cette réponse que j'ai raté la partie graphique.)

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

  • tester un: allouer deux blocs contigus de 500 Mo de RAM et les copier de l'un à l'autre (ce que vous avez déjà fait)
  • testez deux: allouez 20 blocs (ou plus) de 500 Mo de mémoire et copiez-les du premier au dernier afin 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 dans différentes zones de mémoire et qu’elles nécessitent une opération de commutation à un moment donné sur le chemin de données pour pouvoir dialoguer avec une zone puis une autre (il existe un problème de mémoire double face).

Assurez-vous également que le thread est lié à un processeur?

EDIT 2:  

Il existe plusieurs types de délimiteurs de "zones" pour la mémoire. NUMA est un, mais ce n'est pas le seul. Par exemple, les bâtons à deux côtés nécessitent un drapeau pour s’adresser à un côté ou à l’autre. Regardez sur votre graphique comment les performances se dégradent avec une grosse quantité de mémoire, même sur un 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 RAM (une sorte de DMA). ) et que cette puce doit avoir moins de cache que votre CPU, cela pourrait expliquer pourquoi la copie muette avec CPU est plus rapide que memcpy.

10
bokan

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

  1. Page-crossing Prefetch - Le processeur de votre ordinateur portable prélèvera la page linéaire suivante chaque fois que vous atteignez la fin de la page actuelle, ce qui vous évite d'avoir à chaque fois une erreur TLB. Pour tenter de résoudre ce problème, essayez de créer le code de votre serveur pour les pages 2M/1G.

  2. Les schémas de remplacement de cache semblent également avoir été améliorés (voir un intéressant reverse engineering ici ). Si ce processeur utilise effectivement une stratégie d’insertion dynamique, il empêchera facilement vos données copiées d’essayer de masquer votre cache de dernier niveau (qu’il ne peut pas utiliser efficacement malgré tout en raison de la taille), et ménagera davantage de mémoire cache utile. comme le code, la pile, les données de table de page, etc.). Pour tester cela, vous pouvez essayer de reconstruire votre implémentation naïve en utilisant des charges/magasins en continu (movntdq ou similaires, vous pouvez également utiliser la commande gcc intégrée pour cela). Cette possibilité peut expliquer la chute soudaine de grandes tailles de fichiers.

  3. Je pense que certaines améliorations ont également été apportées avec la copie de chaîne ( ici ), elle peut ou non s'appliquer ici, selon l'apparence de votre code d'assemblage. Vous pouvez essayer de comparer avec Dhrystone pour vérifier s’il existe une différence inhérente. Cela peut aussi expliquer la différence entre memcpy et memmove.

Si vous pouviez vous procurer un serveur IvyBridge ou un ordinateur portable Sandy-Bridge, il serait plus simple de les tester tous ensemble.

8
Leeor

J'ai modifié le test afin d'utiliser le minuteur nsec sous Linux et j'ai trouvé des variations similaires sur différents processeurs, tous dotés d'une mémoire similaire. Tous exécutent RHEL 6. 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 également essayé de faire en sorte que la mémoire en ligne fasse 8 octets à la fois… .. Sur ces processeurs Intel, cela ne faisait aucune différence notable. Le cache fusionne toutes les opérations d'octet dans le nombre minimal d'opérations de mémoire. Je soupçonne que le code de la bibliothèque gcc tente d’être trop intelligent.

4
stark

Vous avez déjà répondu à la question ci-dessus , mais dans tous les cas, voici une implémentation utilisant AVX qui devrait être plus rapide pour les copies volumineuses si c'est ce qui vous préoccupe: 

#define ALIGN(ptr, align) (((ptr) + (align) - 1) & ~((align) - 1))

void *memcpy_avx(void *dest, const void *src, size_t n)
{
    char * d = static_cast<char*>(dest);
    const char * s = static_cast<const char*>(src);

    /* fall back to memcpy() if misaligned */
    if ((reinterpret_cast<uintptr_t>(d) & 31) != (reinterpret_cast<uintptr_t>(s) & 31))
        return memcpy(d, s, n);

    if (reinterpret_cast<uintptr_t>(d) & 31) {
        uintptr_t header_bytes = 32 - (reinterpret_cast<uintptr_t>(d) & 31);
        assert(header_bytes < 32);

        memcpy(d, s, min(header_bytes, n));

        d = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(d), 32));
        s = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(s), 32));
        n -= min(header_bytes, n);
    }

    for (; n >= 64; s += 64, d += 64, n -= 64) {
        __m256i *dest_cacheline = (__m256i *)d;
        __m256i *src_cacheline = (__m256i *)s;

        __m256i temp1 = _mm256_stream_load_si256(src_cacheline + 0);
        __m256i temp2 = _mm256_stream_load_si256(src_cacheline + 1);

        _mm256_stream_si256(dest_cacheline + 0, temp1);
        _mm256_stream_si256(dest_cacheline + 1, temp2);
    }

    if (n > 0)
        memcpy(d, s, n);

    return dest;
}
3
Guilherme

Spécifications du serveur 1

  • CPU: 2x Intel Xeon E5-2680 @ 2,70 Ghz

Spécifications du serveur 2

  • CPU: 2x Intel Xeon E5-2650 v2 @ 2,6 Ghz

Selon Intel ARK, les E5-2650 et E5-2680 ont une extension AVX.

Fichier CMake à construire

Cela fait partie de votre problème. CMake choisit pour vous des drapeaux plutôt pauvres. Vous pouvez le confirmer en exécutant make VERBOSE=1.

Vous devez ajouter à la fois -march=native et -O3 à vos CFLAGS et CXXFLAGS. Vous constaterez probablement une augmentation spectaculaire des performances. Il devrait engager les extensions AVX. Sans -march=XXX, vous obtenez effectivement une machine minimale i686 ou x86_64. Sans -O3, vous n'engagez pas les vectorisations de GCC.

Je ne sais pas si GCC 4.6 est capable d’AVX (et d’amis, comme BMI). Je sais que GCC 4.8 ou 4.9 est capable parce que je devais rechercher un bogue d'alignement qui causait un segfault quand GCC sous-traitait Memcpy's et memset à l'unité MMX. AVX et AVX2 permettent à la CPU de fonctionner simultanément sur des blocs de données de 16 octets et de 32 octets.

Si GCC manque une opportunité d'envoyer des données alignées à l'unité MMX, il se peut que le fait que les données soient alignées est manquant. Si vos données sont alignées sur 16 octets, essayez d’avertir GCC afin qu’il sache qu’il fonctionne sur de gros blocs. Pour cela, voir __builtin_assume_aligned de GCC. Consultez également des questions telles que Comment indiquer à GCC qu'un argument de pointeur est toujours aligné sur un double mot?

Cela semble aussi un peu suspect à cause du void*. Son genre de jeter des informations sur le pointeur. Vous devriez probablement garder les informations:

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

Peut-être quelque chose comme ce qui suit:

template <typename T>
void doMemmove(T* pDest, const T* pSource, std::size_t count)
{
  memmove(pDest, pSource, count*sizeof(T));
}

Une autre suggestion consiste à utiliser new et d'arrêter d'utiliser malloc. C’est un programme C++ et GCC peut émettre des hypothèses sur new qu’il ne peut pas faire sur malloc. Je crois que certaines des hypothèses sont détaillées dans la page d'options de GCC pour les fonctions intégrées.

Une autre suggestion consiste à utiliser le tas. C'est toujours 16 octets alignés sur les systèmes modernes typiques. GCC devrait reconnaître qu'il peut être déchargé sur l'unité MMX lorsqu'un pointeur du tas est impliqué (sans les problèmes potentiels de void* et de malloc).

Enfin, pendant un certain temps, Clang n’utilisait pas les extensions de CPU natives lors de l’utilisation de -march=native. Voir, par exemple, Ubuntu Issue 1616723, Clang 3.4 n'annonce que SSE2 , Ubuntu Issue 1616723, Clang 3.5 n'annonce que SSE2 et Ubuntu Issue 1616723, Clang 3.6 n'annonce que SSE2 .

0
jww