2 votes

Memset prend autant de temps que memcpy

Je veux mesurer la bande passante mémoire en utilisant memcpy. J'ai modifié le code de cette réponse : pourquoi la vectorisation de la boucle n'améliore pas les performances qui utilisait memset pour mesurer la bande passante. Le problème est que memcpy est seulement légèrement plus lent que memset alors que je m'attendais à ce qu'il soit environ deux fois plus lent car il opère sur deux fois la mémoire.

Plus spécifiquement, je parcours des tableaux de 1 Go, a et b (alloués avec calloc), 100 fois avec les opérations suivantes.

opération             temps(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

Remarquez que memcpy est seulement légèrement plus lent que memset. Les opérations a[j] += b[j] (où j va de [0,LEN)) devraient prendre trois fois plus de temps que memcpy car elles opèrent sur trois fois plus de données. Cependant, elles sont seulement environ 2,5 fois plus lentes que memset.

Ensuite, j'ai initialisé b à zéro avec memset(b,0,LEN) et j'ai retesté :

opération             temps(s)
-----------------------------
memcpy(a,b,LEN)       8.2
a[j] += b[j]          11.5

Maintenant, on observe 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 m'y attendais.

Je m'attendais au minimum à ce que avant memset(b,0,LEN) que memcpy serait plus lent en raison de l'allocation paresseuse (first touch) lors de la première des 100 itérations.

Pourquoi n'obtiens-je la durée attendue qu'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


    #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);
    }

Compiler 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,60 GHz (Skylake), 32 Go DDR4, Ubuntu 16.10. Sur mon système Haswell, les bandes passantes ont du sens avant `memset(b,0,LEN)`, c'est-à-dire que je ne rencontre de problème que sur mon système Skylake.

J'ai découvert ce problème lors des opérations `a[j] += b[k]` [dans cette réponse](https://stackoverflow.com/questions/42964820/why-is-this-simd-multiplication-not-faster-than-non-simd-multiplication/42972674#42972674) qui surestimait la bande passante.

------

J'ai imaginé un test plus simple

    #include 
    #include 
    #include 

    void __attribute__ ((noinline))  foo(char *a, char *b, const int LEN) {
      for (int i = 0; i < 100; i++) for(int j=0; j

``Cela affiche.

    9,472976
    12,728426

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

    12,5
    12,5

Cela me conduit à penser qu'il s'agit d'un problème d'allocation du système d'exploitation et non d'un problème de 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 optimise memset(b,0,LEN) après calloc mais Clang ne le fait pas.
        memset(b,1,LEN);
        tests(a, b, LEN);
    }`` ```

1voto

wildplasser Points 17900

Le point est que malloc et calloc sur la plupart des plateformes n'allouent pas de mémoire; ils allouent de l'espace d'adressage.

malloc etc fonctionnent de la manière suivante :

  • si la demande peut être satisfaite par la freelist, découper un morceau

  • dans le cas de calloc : l'équivalent de memset(ptr, 0, size) est émis

  • sinon : demander au système d'exploitation d'étendre l'espace d'adressage.

Pour les systèmes avec pagination à la demande (COW) (une MMU pourrait être utile ici), la deuxième option se résume à :

  • créer suffisamment d'entrées de table de pages pour la demande, et les remplir avec une référence (COW) à /dev/zero
  • ajouter ces PTEs à l'espace d'adressage du processus

Cela ne consommera aucune mémoire physique, à l'exception des Tables de Pages.

  • Une fois que la nouvelle mémoire est référencée en lecture, la lecture proviendra de /dev/zero. Le périphérique /dev/zero est un périphérique très spécial, dans ce cas, mappé sur chaque page de la nouvelle mémoire.

  • mais, si la nouvelle page est écrite, la logique COW entre en jeu (via une violation de page) :

  • de la mémoire physique est allouée

  • la page /dev/zero est copiée sur la nouvelle page

  • la nouvelle page est détachée de la page mère

  • et le processus appelant peut enfin effectuer la mise à jour qui a déclenché tout cela

1voto

osgx Points 28675

Votre tableau b n'a probablement pas été écrit après le mmap (les demandes d'allocation importantes avec malloc/calloc sont généralement converties en mmap). Et tout le tableau a été mappé vers une seule page en lecture seule "zero page" (partie du mécanisme de COW). Lire des zéros depuis une seule page est plus rapide que de lire depuis de nombreuses pages, car une seule page sera conservée dans le cache et dans le TLB. Cela explique pourquoi le test avant le memset(0) était plus rapide :

Cela affiche. 9.472976 12.728426

Cependant, si je fais un memset(b,1,LEN) dans le main après le calloc (voir ci-dessous) alors cela affiche : 12.5 12.5

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

//GCC optimise le memset(b,0,LEN) après calloc mais pas Clang.

Cette optimisation a été proposée dans https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57742 (arbre d'optimisation 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 significativement plus rapide que malloc+bzero car il sait que certaines mémoires sont déjà à zéro. Lorsque d'autres optimisations simplifient le code en malloc+memset(0), il serait donc intéressant de le remplacer par calloc. Malheureusement, je ne pense pas qu'il y ait de moyen de faire une optimisation similaire en C++ avec new, où un tel code apparaît le plus facilement (création de std::vector(10000) par exemple). Et il y aurait aussi la complication que la taille du memset serait un peu plus petite que celle du malloc (utiliser calloc serait toujours correct, mais il serait plus difficile de savoir si c'est une amélioration).

Implémenté 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) obtient un pointeur de malloc ou calloc, il convertira malloc en calloc et ensuite memset(0) sera supprimé :

/* Gérer un appel à memset.
   Après un appel à calloc, memset(,0,) est inutile.
   memset(malloc(n),0,n) est équivalent à calloc(n,1).  */
static bool
handle_builtin_memset (gimple_stmt_iterator *gsi)
 ...
  if (code1 == BUILT_IN_CALLOC)
    /* Ne modifie pas 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 1er mars 2014 au 15 juillet 2014 avec pour sujet "calloc = malloc + memset"

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

Pour ce que ça vaut, je crois que cette transformation va perturber une grande variété de micro-tests.

calloc sait internement que la mémoire fraîche de l'OS est à zéro. Mais la mémoire peut ne pas encore être accédée.

memset provoque toujours l'accès à la mémoire.

Donc si vous avez un test comme

   buf = malloc(...)
   memset(buf, ...) 
   start = get_time();
   ... faire quelque chose avec buf
   end = get_time()

Maintenant les temps seront complètement faussés parce que les temps mesurés incluent les défauts de page.

Marc a répondu "Bon point. Je suppose que contourner les optimisations du compilateur fait partie du jeu pour les micro-tests, et leurs auteurs seraient déçus si le compilateur ne perturbait pas régulièrement de nouvelles façons amusantes ;-)" et Andi a demandé: "Je préférerais ne pas le faire. Je ne suis pas sûr que cela ait beaucoup de bénéfices. Si vous souhaitez le conserver, veuillez vous assurer qu'il est facile de le désactiver."

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 ayez écrit 'memset' explicitement dans votre code)
  • -fno-builtin
  • -ffreestanding
  • -O1
  • -Os

Dans le code, vous pouvez cacher que le pointeur passé à memset est celui retourné par malloc en le stockant dans une variable volatile, ou toute autre astuce pour cacher au compilateur que nous faisons memset(malloc(n),0,n).

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