Memcpy prend le même temps que memset

Je veux mesurer la bande passante de la mémoire en utilisant memcpy . J’ai modifié le code à partir de cette réponse: pourquoi la vectorisation de la boucle n’améliore pas les performances memset à l’utilisation de memset pour mesurer la bande passante. Le problème, c’est que memcpy n’est que légèrement plus lent que memset lorsque je pense qu’il sera environ deux fois plus lent, car il fonctionne deux fois plus en mémoire.

Plus spécifiquement, je lance plus de 100 fois les baies a et b (alloué, calloc ) 100 fois avec les opérations suivantes.

 operation time(s) ----------------------------- memset(a,0xff,LEN) 3.7 memcpy(a,b,LEN) 3.9 a[j] += b[j] 9.4 memcpy(a,b,LEN) 3.8 

Notez que memcpy n’est que légèrement plus lent que memset . Les opérations a[j] += b[j] (où j dépasse [0,LEN) ) devraient prendre trois fois plus de temps que memcpy car elles opèrent trois fois plus de données. Cependant, ce n’est que 2,5 fois plus lent que memset .

Ensuite, j’ai initialisé b à zéro avec memset(b,0,LEN) et ai testé à nouveau:

 operation time(s) ----------------------------- memcpy(a,b,LEN) 8.2 a[j] += b[j] 11.5 

Nous voyons maintenant que memcpy est environ deux fois plus lent que memset et a[j] += b[j] est environ trois fois plus lent que memset comme je l’attendais.

À tout le moins, je m’attendais à ce qu’avant memset(b,0,LEN) , la memcpy soit plus lente en raison de l’allocation paresseuse (premier contact) sur la première des 100 itérations.

Pourquoi ai-je seulement le temps que je memset(b,0,LEN) après memset(b,0,LEN) ?

test.c

 #include  #include  #include  void tests(char *a, char *b, const int LEN){ clock_t time0, time1; time0 = clock(); for (int i = 0; i < 100; i++) memset(a,0xff,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); memset(b,0,LEN); time0 = clock(); for (int i = 0; i < 100; i++) memcpy(a,b,LEN); time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); time0 = clock(); for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; time1 = clock(); printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC); } 

principal c

 #include  int tests(char *a, char *b, const int LEN); int main(void) { const int LEN = 1 << 30; // 1GB char *a = (char*)calloc(LEN,1); char *b = (char*)calloc(LEN,1); tests(a, b, LEN); } 

Comstackz avec (gcc 6.2) gcc -O3 test.c main.c Clang 3.8 donne essentiellement le même résultat.

Système de test: i7-6700HQ@2.60GHz (Skylake), DDR4 32 Go, Ubuntu 16.10. Sur mon système Haswell, les bandes passantes ont un sens avant memset(b,0,LEN) c’est-à-dire que je ne vois qu’un problème sur mon système Skylake.

J’ai découvert ce problème pour la première fois grâce aux opérations a[j] += b[k] de cette réponse, qui surestimait la bande passante.


Je suis venu avec un test plus simple

 #include  #include  #include  void __atsortingbute__ ((noinline)) foo(char *a, char *b, const int LEN) { for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j]; } void tests(char *a, char *b, const int LEN) { foo(a, b, LEN); memset(b,0,LEN); foo(a, b, LEN); } 

Cette sortie.

 9.472976 12.728426 

Cependant, si je memset(b,1,LEN) dans main après calloc (voir ci-dessous), il affiche

 12.5 12.5 

Cela m’amène à penser qu’il s’agit d’un problème d’allocation de système d’exploitation et non d’un compilateur.

 #include  int tests(char *a, char *b, const int LEN); int main(void) { const int LEN = 1 << 30; // 1GB char *a = (char*)calloc(LEN,1); char *b = (char*)calloc(LEN,1); //GCC optimizes memset(b,0,LEN) away after calloc but Clang does not. memset(b,1,LEN); tests(a, b, LEN); } 

Le fait est que malloc et calloc sur la plupart des platesformes n’allouent pas de mémoire; ils allouent un espace d’adressage .

malloc etc travailler par:

  • si la demande peut être satisfaite par la freelist, découpez-en une partie
    • en cas de calloc : l’équivalent de memset(ptr, 0, size) est émis
  • si non: demander au système d’exploitation d’étendre l’espace d’adressage.

Pour les systèmes avec pagination à la demande ( COW ) (une MMU pourrait aider ici), la deuxième option est la suivante:

  • créer suffisamment d’entrées de table de page pour la demande et les remplir avec une référence (COW) à /dev/zero
  • append ces PTE à l’espace d’adressage du processus

Cela ne consum aucune mémoire physique , sauf pour les tables de page.

  • Une fois que la nouvelle mémoire est référencée pour la lecture , la lecture proviendra de /dev/zero . Le périphérique /dev/zero est un périphérique très spécial, mappé dans ce cas sur chaque page de la nouvelle mémoire.
  • mais si la nouvelle page est écrite, la logique de la vache commence (via une erreur de page):
    • la mémoire physique est allouée
    • la page / dev / zero est copiée dans la nouvelle page
    • la nouvelle page est détachée de la page mère
    • et le processus d’appel peut enfin faire la mise à jour qui a commencé tout cela

Votre tableau b n’a probablement pas été écrit après mmap -ing (les demandes d’allocation énormes avec malloc / calloc sont généralement converties en mmap ). Et tout le tableau a été transformé en “zéro page” en lecture seule (partie du mécanisme COW ). La lecture de zéros d’une page est plus rapide que celle de plusieurs pages, car une page sera conservée dans le cache et dans le TLB. Ceci explique pourquoi tester avant memset (0) était plus rapide:

Cette sortie. 9.472976 12.728426

Cependant, si je memset(b,1,LEN) dans main après calloc (voir ci-dessous), il génère: 12.5 12.5

Et plus sur l’optimisation de malloc + memset / calloc + memset dans calloc de gcc (développé à partir de mon commentaire )

 //GCC optimizes memset(b,0,LEN) away after calloc but Clang does not. 

Cette optimisation a été proposée dans https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742 (optimisation de l’arborescence sous PR57742) le 2013-06-27 par Marc Glisse ( https://stackoverflow.com/users/ 1918193 ?) Comme prévu pour la version 4.9 / 5.0 de GCC:

memset (malloc (n), 0, n) -> calloc (n, 1)

calloc peut parfois être beaucoup plus rapide que malloc + bzero car il a une connaissance particulière que la mémoire est déjà nulle. Lorsque d’autres optimisations simplifient certains codes dans malloc + memset (0), il serait donc agréable de les remplacer par calloc. Malheureusement, je ne pense pas qu’il soit possible de faire une optimisation similaire en C ++ avec new, qui est l’endroit où ce code apparaît le plus facilement (en créant std :: vector (10000) par exemple). Et il y aurait aussi la complication là-dedans que la taille du memset serait un peu plus petite que celle du malloc (utiliser calloc irait tout de même bien, mais il devient plus difficile de savoir s’il s’agit d’une amélioration).

Mis en œuvre le 2014-06-24 ( https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742#c15 ) – https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=211956 (également https://patchwork.ozlabs.org/patch/325357/ )

  • tree-ssa-strlen.c … (handle_builtin_malloc, handle_builtin_memset): Nouvelles fonctions.

Le code actuel dans gcc/tree-ssa-strlen.c https://github.com/gcc-mirror/gcc/blob/7a31ada4c400351a35ab65f8dc0357e7c88805d5/gcc/tree-ssa-strlen.c#L1889 – si memset(0) get pointeur de malloc ou calloc , il convertira malloc en calloc , puis memset(0) sera supprimé:

 /* Handle a call to memset. After a call to calloc, memset(,0,) is unnecessary. memset(malloc(n),0,n) is calloc(n,1). */ static bool handle_builtin_memset (gimple_stmt_iterator *gsi) ... if (code1 == BUILT_IN_CALLOC) /* Not touching stmt1 */ ; else if (code1 == BUILT_IN_MALLOC && operand_equal_p (gimple_call_arg (stmt1, 0), size, 0)) { gimple_stmt_iterator gsi1 = gsi_for_stmt (stmt1); update_gimple_call (&gsi1, builtin_decl_implicit (BUILT_IN_CALLOC), 2, size, build_one_cst (size_type_node)); si1->length = build_int_cst (size_type_node, 0); si1->stmt = gsi_stmt (gsi1); } 

Cela a été discuté dans la liste de diffusion gcc-patches du 1 mars 2014 au 15 juil. 2014 avec le sujet ” calloc = malloc + memset

avec un commentaire notable de Andi Kleen ( http://halobates.de/blog/ , https://github.com/andikleen ): https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01818 .html

FWIW Je pense que la transformation va casser une grande variété de micro-critères.

calloc sait en interne que la mémoire fraîche du système d’exploitation est mise à zéro. Mais la mémoire ne peut pas encore être endommagée.

memset toujours des défauts dans la mémoire.

Donc si vous avez des tests comme

  buf = malloc(...) memset(buf, ...) start = get_time(); ... do something with buf end = get_time() 

Maintenant, les temps seront complètement décalés car les temps mesurés incluent les fautes de page.

Marc a répondu “C’est un bon point. Je suppose que travailler autour des optimisations du compilateur fait partie du jeu des micro-benchmarks, et leurs auteurs seraient déçus si le compilateur ne le gâchait pas régulièrement de manière nouvelle et amusante 😉 “, et Andi demanda: ” Je préférerais ne pas le faire. Je ne suis pas sûr que cela présente beaucoup d’avantages. Si vous voulez le conserver, assurez-vous qu’il existe un moyen simple de l’éteindre.

Marc montre comment désactiver cette optimisation: https://gcc.gnu.org/ml/gcc-patches/2014-06/msg01834.html

N’importe lequel de ces drapeaux fonctionne:

  • -fdisable-tree-strlen
  • -fno-builtin-malloc
  • -fno-builtin-memset (en supposant que vous avez écrit ‘memset’ explicitement dans votre code)
  • -fno-builtin
  • -ffreestanding
  • -O1
  • -Os

Dans le code, vous pouvez masquer que le pointeur transmis à memset est celui renvoyé par malloc en le stockant dans une variable volatile ou toute autre astuce à cacher au compilateur que nous sums en train de faire memset(malloc(n),0,n) .