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
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
Spécifications de l'ordinateur portable
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.
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?
[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:
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()
.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.memcpy
(104 MS). LE RAM SUR LE SYST&EGRAVE;ME HASWELL EST DDR3-1600 (IDENTIQUE AUX AUTRES SYST&EGRAVE;MES)._/ _ MISES &AGRAVE; JOUR
/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%.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:
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.
Il est possible que certaines améliorations du processeur de votre ordinateur portable IvyBridge contribuent à ce gain par rapport aux serveurs SandyBridge.
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.
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.
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.
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.
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;
}
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 .