web-dev-qa-db-fra.com

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

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

Je suppose que memset utilise mov, cependant pour la mise à zéro de la mémoire, la plupart des compilateurs utilisent xor car c'est plus rapide, n'est-ce pas? edit1: Faux, comme l'a souligné GregS, cela ne fonctionne qu'avec les registres. À quoi je pensais?

De plus, j'ai demandé à une personne qui en savait plus sur Assembler de regarder stdlib, et il m'a dit que sur x86, memset ne tire pas pleinement parti des registres 32 bits. Cependant, à ce moment-là, j'étais très fatiguée et je ne suis donc pas tout à fait sûr de l'avoir bien comprise.

edit2: J'ai revisité ce problème 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;
    }

résultats:

zéro_1 est le plus lent, sauf pour -O3. zero_sizet est le plus rapide avec des performances à peu près égales sur -O1, -O2 et -O3. memset était toujours plus lent que zero_sizet. (deux fois plus lent pour -O3). Ce qui est intéressant, c’est qu’à -O3, zero_1 était aussi rapide que zero_sizet. Cependant, la fonction désassemblée avait à peu près quatre fois plus d'instructions (je pense causée par le déroulement d'une boucle). Aussi, j'ai essayé d'optimiser encore plus zero_sizet, mais le compilateur m'a toujours surpassé, mais pas de surprise ici.

Pour l'instant memset gagne, les résultats précédents ont été déformés par le cache du processeur. (tous les tests ont été exécutés sous Linux) D'autres tests sont nécessaires. Je vais essayer ensuite d'assembleur :)

edit3: bug corrigé dans le code du test, les résultats du test ne sont pas affectés

edit4: Tout en fouillant dans le runtime désassemblé du VS2010, j’ai remarqué que memset possède une routine SSE optimisée pour zéro. Il sera difficile de battre cette.

57
maep

x86 est assez large gamme de périphériques.

Pour une cible x86 totalement générique, un bloc Assembly avec "rep movsd" pourrait faire apparaître des zéros dans la mémoire 32 bits à la fois. Essayez de vous assurer que la majeure partie de ce travail est alignée sur DWORD.

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

Vous pourriez peut-être faire en sorte qu'un compilateur C/C++ utilise une écriture 64 bits avec un pointeur sur 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 uniquement si l'adresse est alignée sur 16 octets; utilisez donc un movsb jusqu'à alignement, puis complétez votre effacement avec une boucle de movaps

Win32 a "ZeroMemory ()", mais j’oublie s’il s’agit d’une macro à memset, ou d’une "bonne" implémentation.

32
Tim

memset est généralement conçu pour être très très rapide sage général code de réglage/remise à 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 effectuer votre travail. Selon le système sur lequel vous vous trouvez (et le fournisseur de votre bibliothèque stdlib), l'implémentation sous-jacente peut être un assembleur spécifique à cette architecture pour tirer parti de ses propriétés natives. Il pourrait également avoir des cas spéciaux internes pour traiter le cas de la réduction à zéro (par opposition à la définition d'une autre valeur).

Cela dit, si vous devez effectuer une réduction à zéro de la mémoire très spécifique et très critique en termes de performances, il est certainement possible de battre une implémentation spécifique de memset en le faisant vous-même. memset et ses amis de la bibliothèque standard sont toujours des cibles amusantes pour la programmation à sens unique. :)

26
Ben Zotto

De nos jours, votre compilateur devrait faire tout le travail à votre place. Au moins de ce que je sais, gcc est très efficace pour optimiser les appels de memset (mieux, vérifiez l’assembleur, cependant).

Ensuite, évitez aussi memset si vous n’avez pas à:

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

Et pour les très gros morceaux, utilisez mmap si vous en avez. Cela ne fait que zéro mémoire initialisée du système "gratuitement".

23
Jens Gustedt

Sauf si vous avez des besoins spécifiques ou si vous savez que votre compilateur/stdlib est nul, restez fidèle à memset. Il est polyvalent et devrait avoir des performances décentes en général. De plus, les compilateurs ont peut-être plus de facilité à optimiser/insérer memset () car ils peuvent avoir un support intrinsèque.

Par exemple, Visual C++ génère souvent des versions en ligne de memcpy/memset qui sont aussi petites qu'un appel à la fonction de bibliothèque, évitant ainsi le surdébit Push/call/ret. Et il existe d'autres optimisations possibles lorsque le paramètre de taille peut être évalué au moment de la compilation.

Cela dit, si vous avez des besoins spécifiques (où la taille sera toujours minuscule * ou * énorme), vous pouvez augmenter la vitesse en descendant au niveau de l’Assemblée. Par exemple, utilisez des opérations d'écriture directe pour mettre à zéro des morceaux de mémoire énormes sans polluer votre cache L2.

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

5
snemarch

Si je me souviens bien (il y a quelques années), l'un des développeurs expérimentés parlait d'un moyen rapide de bzero () sur PowerPC (les spécifications stipulaient qu'il fallait mettre à zéro presque toute la mémoire à la mise sous tension). Cela pourrait ne pas bien traduire (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, puis d'écrire en arrière la ligne de cache de données effacée.

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

5
Sparky

Voir aussi la question Strange Assembly du tableau 0-initialization pour une comparaison de memset et de = { 0 }.

2
Johann Gerell

C'est une question intéressante. J'ai réalisé cette mise en œuvre légèrement plus rapide (mais difficilement mesurable) lors de la compilation 32 bits sur VC++ 2012. Elle peut probablement être améliorée sur de nombreux points. Ajouter cela dans votre propre classe dans un environnement multithread vous apporterait probablement encore plus de gains de performances car il existe des problèmes de goulot d'étranglement signalés avec memset() dans des scénarios multithread.

// MemsetSpeedTest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "Windows.h"
#include <time.h>

#pragma comment(lib, "Winmm.lib") 
using namespace std;

/** a signed 64-bit integer value type */
#define _INT64 __int64

/** a signed 32-bit integer value type */
#define _INT32 __int32

/** a signed 16-bit integer value type */
#define _INT16 __int16

/** a signed 8-bit integer value type */
#define _INT8 __int8

/** an unsigned 64-bit integer value type */
#define _UINT64 unsigned _INT64

/** an unsigned 32-bit integer value type */
#define _UINT32 unsigned _INT32

/** an unsigned 16-bit integer value type */
#define _UINT16 unsigned _INT16

/** an unsigned 8-bit integer value type */
#define _UINT8 unsigned _INT8

/** maximum allo

wed value in an unsigned 64-bit integer value type */
    #define _UINT64_MAX 18446744073709551615ULL

#ifdef _WIN32

/** Use to init the clock */
#define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency);

/** Use to start the performance timer */
#define TIMER_START QueryPerformanceCounter(&t1);

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl;
#else
/** Use to init the clock */
#define TIMER_INIT clock_t start;double diff;

/** Use to start the performance timer */
#define TIMER_START start=clock();

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl;
#endif    


void *MemSet(void *dest, _UINT8 c, size_t count)
{
    size_t blockIdx;
    size_t blocks = count >> 3;
    size_t bytesLeft = count - (blocks << 3);
    _UINT64 cUll = 
        c 
        | (((_UINT64)c) << 8 )
        | (((_UINT64)c) << 16 )
        | (((_UINT64)c) << 24 )
        | (((_UINT64)c) << 32 )
        | (((_UINT64)c) << 40 )
        | (((_UINT64)c) << 48 )
        | (((_UINT64)c) << 56 );

    _UINT64 *destPtr8 = (_UINT64*)dest;
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 2;
    bytesLeft = bytesLeft - (blocks << 2);

    _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 1;
    bytesLeft = bytesLeft - (blocks << 1);

    _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll;

    if (!bytesLeft) return dest;

    _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx];
    for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll;

    return dest;
}

int _tmain(int argc, _TCHAR* argv[])
{
    TIMER_INIT

    const size_t n = 10000000;
    const _UINT64 m = _UINT64_MAX;
    const _UINT64 o = 1;
    char test[n];
    {
        cout << "memset()" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                memset((void*)test, 0, n);  

        TIMER_STOP;
    }
    {
        cout << "MemSet() took:" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                MemSet((void*)test, 0, n);

        TIMER_STOP;
    }

    cout << "Done" << endl;
    int wait;
    cin >> wait;
    return 0;
}

La sortie est la suivante lors de la compilation des versions pour les systèmes 32 bits:

memset() took:
5.569000
MemSet() took:
5.544000
Done

La sortie est la suivante lors de la compilation des versions pour les systèmes 64 bits:

memset() took:
2.781000
MemSet() took:
2.765000
Done

Ici vous pouvez trouver le code source de Berkley memset(), qui, à mon avis, est l'implémentation la plus courante.

2
user152949

Il y a une faille fatale dans ce test par ailleurs génial et utile: comme memset est la première instruction, il semble exister une "surcharge de mémoire" qui le rend extrêmement lent. Déplacer le timing de memset à la deuxième place et autre chose à la première place ou simplement chronométrer deux fois memset fait de memset le plus rapide avec tous les commutateurs de compilation !!!

2
Chris

La fonction memset est conçue pour être flexible et simple, même au détriment de la vitesse. Dans de nombreuses implémentations, il s'agit d'une simple boucle while qui copie la valeur spécifiée octet par octet sur le nombre d'octets donné. Si vous voulez un memset plus rapide (ou memcpy, memmove, etc.), il est presque toujours possible de le coder vous-même.

La personnalisation la plus simple consisterait à effectuer des opérations "set" sur un octet jusqu'à ce que l'adresse de destination soit alignée sur 32 ou 64 bits (quelle que soit l'architecture choisie par votre puce), puis de commencer à copier un registre complet de la CPU à la fois. Vous devrez peut-être effectuer quelques opérations "set" sur un octet à la fin si votre plage ne se termine pas sur une adresse alignée.

En fonction de votre processeur, vous pouvez également disposer de quelques instructions en continu SIMD pouvant vous aider. Celles-ci fonctionneront généralement mieux avec les adresses alignées. La technique ci-dessus d’utilisation des adresses alignées peut donc également s’avérer utile.

Pour réduire de grandes parties de la mémoire, vous pouvez également constater une augmentation de la vitesse en scindant la plage en sections et en traitant chaque section en parallèle (le nombre de sections étant identique à votre nombre ou à vos threads matériels/matériels).

Plus important encore, il n’ya aucun moyen de savoir si cela vous aidera, à moins d’essayer. Au minimum, regardez ce que votre compilateur émet pour chaque cas. Voyez ce que les autres compilateurs émettent pour leur "memset" standard (leur implémentation pourrait être plus efficace que celle de votre compilateur).

2
bta

memset pourrait être intégré par le compilateur sous la forme d’une série d’opcodes efficaces, déroulés pendant quelques cycles. Pour les blocs de mémoire très volumineux, comme le framebuffer 4000x2000 64 bits, vous pouvez essayer de l'optimiser sur plusieurs threads (que vous préparez pour cette tâche unique), chacun définissant sa propre partie. Notez qu'il existe également bzero (), mais il est plus obscur et moins susceptible d'être optimisé que memset, et le compilateur remarquera sûrement que vous passez 0.

Ce que le compilateur suppose en général, c’est que vous memset de grands blocs. Ainsi, pour des blocs plus petits, il serait probablement plus efficace de simplement faire *(uint64_t*)p = 0, si vous initiez un grand nombre de petits objets.

En règle générale, tous les processeurs x86 sont différents (à moins que vous ne compiliez pour une plate-forme normalisée), et quelque chose que vous optimisez pour Pentium 2 se comportera différemment sur Core Duo ou i486. Donc, si vous y tenez vraiment et que vous voulez presser les derniers morceaux de dentifrice, il est judicieux de vous envoyer plusieurs versions compilées et optimisées au format exe pour différents modèles de CPU populaires. D'après mon expérience personnelle, Clang -march = native a augmenté le FPS de mon jeu de 60 à 65 ans, par rapport à l'absence de marché.

0
SmugLispWeenie