74 votes

Un moyen plus rapide de remettre la mémoire à zéro qu'avec memset ?

J'ai appris que memset(ptr, 0, nbytes) est vraiment rapide, mais existe-t-il un moyen plus rapide (au moins sur x86) ?

Je suppose que memset utilise mov cependant, lors de la mise à zéro de la mémoire, la plupart des compilateurs utilisent xor car c'est plus rapide, correct ? edit1 : Faux, comme l'a souligné GregS, cela ne fonctionne qu'avec les registres. Qu'est-ce qui m'a pris ?

J'ai également demandé à une personne qui connaissait mieux l'assembleur que moi de regarder la stdlib, et il m'a dit que sur x86, memset ne profite pas pleinement des registres de 32 bits. Cependant, à ce moment-là, j'étais très fatigué, donc je ne suis pas sûr d'avoir compris correctement.

edit2 : J'ai réexaminé cette question et fait quelques tests. Voici ce que j'ai testé :

    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>
    #include <sys/time.h>

    #define TIME(body) do {                                                     \
        struct timeval t1, t2; double elapsed;                                  \
        gettimeofday(&t1, NULL);                                                \
        body                                                                    \
        gettimeofday(&t2, NULL);                                                \
        elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
        printf("%s\n --- %f ---\n", #body, elapsed); } while(0)                 \

    #define SIZE 0x1000000

    void zero_1(void* buff, size_t size)
    {
        size_t i;
        char* foo = buff;
        for (i = 0; i < size; i++)
            foo[i] = 0;

    }

    /* I foolishly assume size_t has register width */
    void zero_sizet(void* buff, size_t size)
    {
        size_t i;
        char* bar;
        size_t* foo = buff;
        for (i = 0; i < size / sizeof(size_t); i++)
            foo[i] = 0;

        // fixes bug pointed out by tristopia
        bar = (char*)buff + size - size % sizeof(size_t);
        for (i = 0; i < size % sizeof(size_t); i++)
            bar[i] = 0;
    }

    int main()
    {
        char* buffer = malloc(SIZE);
        TIME(
            memset(buffer, 0, SIZE);
        );
        TIME(
            zero_1(buffer, SIZE);
        );
        TIME(
            zero_sizet(buffer, SIZE);
        );
        return 0;
    }

les résultats :

zero_1 est le plus lent, sauf pour -O3. zero_sizet est le plus rapide avec des performances à peu près égales entre -O1, -O2 et -O3. memset a toujours été plus lent que zero_sizet. (deux fois plus lent pour -O3). une chose intéressante est qu'à -O3 zero_1 était aussi rapide que zero_sizet. cependant la fonction désassemblée avait environ quatre fois plus d'instructions (je pense que c'est dû au déroulement de la boucle). De plus, j'ai essayé d'optimiser davantage zero_sizet, mais le compilateur m'a toujours surpassé, mais ce n'est pas une surprise.

Pour l'instant, memset gagne, les résultats précédents étaient faussés par le cache du CPU. (tous les tests ont été effectués sous Linux) Des tests supplémentaires sont nécessaires. Je vais essayer l'assembleur ensuite :)

edit3 : correction d'un bug dans le code de test, les résultats du test ne sont pas affectés

edit4 : En fouillant dans le runtime C de VS2010 désassemblé, j'ai constaté que memset a une routine optimisée SSE pour le zéro. Il sera difficile de la battre.

5 votes

Au lieu de supposer que memset utilise mov pourquoi ne pas essayer de désassembler la sortie de votre compilateur ? Des compilateurs différents font des choses différentes. Si xor est plus rapide sur une architecture donnée, alors il ne serait pas surprenant que certains compilateurs optimisent memset(ptr, 0, nbytes) en xor des instructions.

14 votes

Je n'ai pas connaissance d'un compilateur qui utilise XOR pour mettre la mémoire à zéro. Peut-être un registre, mais pas la mémoire. Afin d'utiliser XOR pour mettre la mémoire à zéro, vous devez d'abord lire la mémoire, puis XOR, puis écrire la mémoire.

5 votes

Le cas échéant, calloc peut être effectivement libre, car l'implémentation peut mettre à zéro les pages à l'avance, alors que le CPU est par ailleurs inactif. Cela compte-t-il ? ;-)

39voto

Tim Points 206

X86 est une gamme assez large de dispositifs.

Pour une cible x86 totalement générique, un bloc d'assemblage avec "rep movsd" pourrait envoyer des zéros en mémoire sur 32 bits à la fois. Essayez de vous assurer que la majeure partie de ce travail est alignée sur les DWORD.

Pour les puces avec mmx, une boucle d'assemblage avec movq pouvait atteindre 64bits à la fois.

Vous pouvez peut-être obtenir d'un compilateur C/C++ qu'il utilise une écriture 64 bits avec un pointeur vers un long long ou _m64. La cible doit être alignée sur 8 octets pour obtenir les meilleures performances.

pour les puces avec sse, movaps est rapide, mais seulement si l'adresse est alignée sur 16 octets, donc utilisez un movsb jusqu'à ce qu'elle soit alignée, et ensuite complétez votre effacement avec une boucle de movaps

Win32 a "ZeroMemory()", mais je ne sais plus si c'est une macro de memset, ou une bonne implémentation.

7 votes

Réponse vieille de 10 ans, mais ZeroMemory est totalement une macro de memset :D

30voto

Ben Zotto Points 32105

memset est généralement conçu pour être très très rapide polyvalent le code de réglage/de mise à zéro. Il traite tous les cas avec des tailles et des alignements différents, ce qui affecte les types d'instructions que vous pouvez utiliser pour faire votre travail. En fonction du système sur lequel vous vous trouvez (et du fournisseur de votre stdlib), l'implémentation sous-jacente peut être en assembleur spécifique à cette architecture afin de tirer parti de ses propriétés natives. Elle peut également avoir des cas spéciaux internes pour gérer le cas de la mise à zéro (par opposition à la définition d'une autre valeur).

Cela dit, si vous avez une remise à zéro de la mémoire très spécifique, très critique en termes de performances, il est tout à fait possible de battre un modèle de mémoire spécifique. memset en le faisant vous-même. memset et ses amis de la bibliothèque standard sont toujours des cibles amusantes pour la programmation de la surenchère :)

2 votes

Aussi : memset pourrait en théorie avoir un cas spécial pour 0 qui est sélectionné à la compilation (soit par inlining ou comme une opération intrinsèque) quand cet argument est un littéral. Je ne sais pas si quelqu'un le fait ou non.

2 votes

@Steve Jessop : Idée intéressante (en particulier le fait que cela puisse se faire au moment de la compilation). Je me souviens avoir lu une fois l'implémentation de memset de quelqu'un qui avait des cas spéciaux pour à peu près tout ce pour quoi vous utiliseriez réellement memset.

34 votes

Gcc utilise généralement une implémentation en ligne intégrée de memset() . De manière assez amusante, je me souviens avoir lu un article sur une implémentation boguée de memset() qui mettait toujours la valeur à 0 - et cela n'a pas été remarqué pour les personnes suivantes années parce qu'apparemment la grande majorité du temps memset() est utilisé pour mettre à zéro !

24voto

Jens Gustedt Points 40410

Aujourd'hui, votre compilateur devrait faire tout le travail pour vous. Au moins de ce que je sais, gcc est très efficace pour optimiser les appels à memset (il vaut mieux vérifier l'assembleur, cependant).

Alors aussi, évitez memset si vous n'avez pas à le faire :

  • utiliser calloc pour la mémoire du tas
  • utiliser une initialisation appropriée ( ... = { 0 } ) pour la mémoire de la pile

Et pour les très gros morceaux, utilisez mmap si vous l'avez. Cela permet d'obtenir "gratuitement" du système une mémoire initialisée nulle.

0 votes

Non, la dernière fois que j'ai vérifié, gcc ne le faisait pas. Cependant, g++ optimise la suppression d'un appel à std::fill (à moins qu'il y ait une optmisation -ftree-loop-distribute-patterns activé, auquel cas il devient également un appel à memset) qui est l'analogue C++ de memset.

1 votes

Cela vaut peut-être la peine d'être mentionné : Je viens de faire des tests, et j'ai découvert une chose merveilleuse : avec le programme -ftree-loop-distribute-patterns qui change stdfill a memset le programme ×10 ( !) fois plus rapide que sans, c'est-à-dire quand stdfill est inlined par g++, et même si j'ajoute march=native . Par conséquent, gcc-4.9.2 n'est pas si bon en matière d'optimisation, car cela signifie qu'il existe un moyen d'optimiser stdfill encore plus. Btw, j'ai aussi fait un test avec clang, et j'ai trouvé qu'il est pire optimise - avec -O3 niveau, il ne supprime même pas push-pop du code.

1 votes

Le commentaire "gratuit" n'est pas totalement vrai. L'initialisation de la mémoire à zéro se produit simplement dans l'OS plutôt que sous le contrôle de votre programme. Qui sait qui a écrit la fonction mmap, et si l'informatique est efficace ? Si le facteur temps est important, il est préférable de récupérer la mémoire non initialisée, puis de la vider vous-même avec une routine assembleur.

6voto

Sparky Points 4660

Si je me souviens bien (il y a quelques années), l'un des développeurs principaux parlait d'un moyen rapide de bzero() sur PowerPC (les spécifications disaient que nous devions mettre à zéro presque toute la mémoire à la mise sous tension). Il se peut que cela ne se traduise pas bien (voire pas du tout) en x86, mais cela pourrait valoir la peine d'être exploré.

L'idée était de charger une ligne de cache de données, d'effacer cette ligne de cache de données, puis de réécrire la ligne de cache de données effacée en mémoire.

Pour ce que ça vaut, j'espère que ça aidera.

1 votes

Il n'est pas nécessaire de charger la ligne de cache, il suffit d'écrire des zéros dans la ligne de cache.

6voto

snemarch Points 3328

A moins que vous ayez des besoins spécifiques ou que vous sachiez que votre compilateur/stdlib est nul, restez avec memset. Il est polyvalent, et devrait avoir des performances décentes en général. De plus, les compilateurs peuvent avoir un temps plus facile pour optimiser/inliner memset() parce qu'il peut avoir un support intrinsèque pour lui.

Par exemple, Visual C++ générera souvent des versions en ligne de memcpy/memset qui sont aussi petit qu'un appel à la fonction de la bibliothèque, évitant ainsi les surcharges de type push/call/ret. Et il y a d'autres optimisations possibles lorsque le paramètre de taille peut être évalué au moment de la compilation.

Cela dit, si vous avez spécifique besoins (où la taille sera toujours petit *ou* énorme ), vous pouvez obtenir des gains de vitesse en descendant au niveau de l'assemblée. Par exemple, en utilisant des opérations d'écriture pour mettre à zéro d'énormes parties de la mémoire sans polluer votre cache L2.

Mais tout dépend - et pour les choses normales, veuillez vous en tenir à memset/memcpy :)

1 votes

Même les anciennes implémentations de gcc sur sparc remplacent memcpy et memset appels mith mov instruction lorsque les tailles étaient connues au moment de la compilation et qu'elles n'étaient pas trop grandes.

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