web-dev-qa-db-fra.com

Pourquoi la vitesse de memcpy () diminue-t-elle considérablement tous les 4 Ko?

J'ai testé la vitesse de memcpy() en remarquant que la vitesse chutait considérablement à i * 4Ko. Le résultat est le suivant: l'axe des Y correspond à la vitesse (Mo/seconde) et l'axe des X correspond à la taille du tampon pour memcpy(), passant de 1 Ko à 2 Mo. Les sous-figures 2 et 3 détaillent les parties de 1KB-150KB et 1KB-32KB.

Environnement:

CPU: CPU E5620 à 2,40 GHz pour Intel (X) Xeon (MD) 

Système d'exploitation: 2.6.35-22-generic # 33-Ubuntu

Drapeaux du compilateur GCC: -O3 -msse4 -DINTEL_SSE4 -Wall -std = c99

Graphs of memcpy speed showing troughs every 4k

Je suppose que cela doit être lié aux caches, mais je ne trouve pas de raison dans les cas suivants qui ne respectent pas le cache:

Étant donné que la dégradation des performances de ces deux cas est provoquée par des boucles peu amicales qui lisent des octets dispersés dans le cache, gaspillant le reste de l’espace d’une ligne de cache.

Voici mon code:

void memcpy_speed(unsigned long buf_size, unsigned long iters){
    struct timeval start,  end;
    unsigned char * pbuff_1;
    unsigned char * pbuff_2;

    pbuff_1 = malloc(buf_size);
    pbuff_2 = malloc(buf_size);

    gettimeofday(&start, NULL);
    for(int i = 0; i < iters; ++i){
        memcpy(pbuff_2, pbuff_1, buf_size);
    }   
    gettimeofday(&end, NULL);
    printf("%5.3f\n", ((buf_size*iters)/(1.024*1.024))/((end.tv_sec - \
    start.tv_sec)*1000*1000+(end.tv_usec - start.tv_usec)));
    free(pbuff_1);
    free(pbuff_2);
}

METTRE À JOUR

En tenant compte des suggestions de @usr, @ChrisW et @Leeor, j'ai refait le test plus précisément et le graphique ci-dessous montre les résultats. La taille de la mémoire tampon est comprise entre 26 Ko et 38 Ko, et je l'ai testée tous les 64 Ko (26 Ko, 26 Ko + 64 Ko, 26 Ko + 128 Ko, ......, 38 Ko). Chaque test effectue une boucle 100 000 fois en environ 0,15 seconde. Ce qui est intéressant, c’est que la chute ne se produit pas exactement dans la limite de 4 Ko, mais qu’elle se matérialise également en 4 * i + 2 KB, avec une amplitude nettement moins décroissante.

More graphs showing performance drops

PS

@Leeor a proposé un moyen de combler le problème en ajoutant un tampon factice de 2 Ko entre pbuff_1 et pbuff_2. Cela fonctionne, mais je ne suis pas sûr de l'explication de Leeor.

enter image description here

50
foool

La mémoire est généralement organisée en pages de 4 000 pages (bien qu'il existe également une prise en charge pour les tailles plus grandes). L'espace d'adressage virtuel que votre programme voit peut être contigu, mais ce n'est pas nécessairement le cas dans la mémoire physique. Le système d'exploitation, qui gère un mappage d'adresses virtuelles et physiques (dans la mappe de pages), essaie généralement de conserver les pages physiques ensemble également, mais ce n'est pas toujours possible et elles peuvent être fracturées (en particulier lors d'un usage prolongé où elles peuvent être permutées à l'occasion ).

Lorsque votre flux de mémoire franchit une limite de page de 4 ko, le processeur doit s'arrêter pour rechercher une nouvelle traduction. S'il a déjà vu la page, elle peut être mise en cache dans le TLB, et l'accès est optimisé pour être le plus rapide possible. est le premier accès (ou si vous avez trop de pages à conserver pour les TLB), la CPU devra bloquer l’accès à la mémoire et démarrer une page en parcourant les entrées de la carte, ce qui est relativement long, car chaque niveau est en fait une mémoire lue par elle-même (sur les machines virtuelles, elle est encore plus longue car chaque niveau peut nécessiter une page entière sur l'hôte). 

Votre fonction memcpy peut avoir un autre problème: lors de la première affectation de mémoire, le système d’exploitation construisait simplement les pages dans le pagemap, mais les marquait comme non explorées et non modifiées en raison d’optimisations internes. Le premier accès peut non seulement invoquer une consultation de page, mais éventuellement une aide informant le système d'exploitation que la page va être utilisée (et stockée dans les pages de mémoire tampon cible), ce qui nécessiterait une transition coûteuse vers un gestionnaire de système d'exploitation.

Pour éliminer ce bruit, allouez les tampons une fois, répétez plusieurs fois la copie et calculez le temps amorti. Par contre, cela vous donnerait des performances "chaudes" (c'est-à-dire après que les caches aient été réchauffés), de sorte que vous verrez la taille des caches se refléter sur vos graphiques. Si vous souhaitez obtenir un effet "froid" tout en ne souffrant pas de latence de pagination, vous pouvez vider les caches entre les itérations (assurez-vous simplement de ne pas chronométrer cela)

MODIFIER

Relisez la question et vous semblez faire une mesure correcte. Le problème avec mon explication est qu’il devrait afficher une augmentation progressive après le 4k*i, car à chaque baisse vous payez la pénalité à nouveau, mais vous devez ensuite profiter du tour gratuit jusqu’au prochain 4k. Cela n'explique pas pourquoi il y a de tels "pics" et après eux la vitesse redevient normale. 

Je pense que vous êtes confronté à un problème similaire au problème critique lié à votre question: lorsque la taille de votre tampon est de Nice round 4k, les deux tampons s'alignent sur les mêmes ensembles dans le cache et se renversent l'un l'autre. Votre L1 fait 32k, donc cela ne semble pas être un problème au début, mais en supposant que le L1 de données dispose de 8 façons différentes, il s'agit en fait d'un enveloppement 4k des mêmes ensembles, et vous avez 2 * 4k blocs avec le même alignement (en supposant que l’allocation ait été faite de manière contiguë) afin qu’ils se chevauchent sur les mêmes ensembles. Il suffit que la LRU ne fonctionne pas exactement comme prévu et vous continuerez à avoir des conflits.

Pour vérifier cela, je voudrais essayer de malloc un tampon factice entre pbuff_1 et pbuff_2, le rendre 2k grand et espère qu'il rompt l'alignement.

EDIT2:

Ok, puisque cela fonctionne, il est temps d'élaborer un peu. Supposons que vous affectiez deux matrices 4k aux plages 0x1000-0x1fff et 0x2000-0x2fff. la valeur 0 dans votre N1 contiendra les lignes à 0x1000 et 0x2000, la valeur 1 en 0x1040 et 0x2040, etc. À ces tailles, vous n’avez pas encore de problème avec la compression, elles peuvent toutes coexister sans dépasser l’associativité du cache. Cependant, chaque fois que vous effectuez une itération, vous avez une charge et un magasin accédant au même ensemble - je suppose que cela peut provoquer un conflit dans le HW. Pire - vous aurez besoin de plusieurs itérations pour copier une seule ligne, ce qui signifie que vous avez une congestion de 8 charges + 8 magasins (moins si vous vectorisez, mais quand même beaucoup), toutes dirigées vers le même ensemble médiocre, je suis jolie bien sûr, il y a un tas de collisions qui s'y cachent.

Je vois aussi que le guide d’optimisation d’Intel a quelque chose à dire à ce sujet (voir 3.6.8.2):

Un alias de mémoire de 4 Ko se produit lorsque le code accède à deux .__ différents. emplacements de mémoire avec un décalage de 4 KByte entre eux. Le 4-KByte situation de repliement peut se manifester dans une routine de copie en mémoire où le Les adresses du tampon source et du tampon de destination conservent un décalage constant et le décalage constant se trouve être un multiple de l'incrément d'octet d'une itération à la suivante.

...

les charges doivent attendre que les magasins soient retirés avant de pouvoir continuer. Par exemple, au décalage 16, la charge de la prochaine itération est Magasin d'itération en cours avec alias de 4 Ko; par conséquent, la boucle doit attendre jusqu'à ce que l'opération de stockage se termine, ce qui rend la boucle entière sérialisé. Le temps d'attente requis diminue avec une plus grande offset jusqu'à offset de 96 résout le problème (car il n'y a pas de magasins en attente au moment du chargement avec la même adresse).

29
Leeor

Je pense que c'est parce que:

  • Lorsque la taille de bloc est un multiple de 4 Ko, malloc alloue de nouvelles pages à partir du système d'exploitation.
  • Lorsque la taille de bloc n'est pas un multiple de 4 Ko, malloc alloue une plage à partir de son segment de mémoire (déjà alloué).
  • Lorsque les pages sont attribuées à partir du système d'exploitation, elles sont «froides»: les toucher pour la première fois coûte très cher.

Mon hypothèse est que, si vous faites une seule memcpy avant la première gettimeofday, cela «réchauffera» la mémoire allouée et vous ne verrez pas ce problème. Au lieu de faire un memcpy initial, même écrire un octet dans chaque page allouée de 4 Ko pourrait suffire à préchauffer la page.

Habituellement, quand je veux un test de performance comme le vôtre, je le code comme suit:

// Run in once to pre-warm the cache
runTest();
// Repeat 
startTimer();
for (int i = count; i; --i)
  runTest();
stopTimer();

// use a larger count if the duration is less than a few seconds
// repeat test 3 times to ensure that results are consistent
2
ChrisW

Étant donné que vous êtes en train de boucler plusieurs fois, je pense que les arguments sur les pages non mappées ne sont pas pertinents. À mon avis, ce que vous constatez est l’effet du préfeteur matériel qui ne souhaite pas franchir les limites de la page afin de ne pas causer de fautes de page (potentiellement inutiles).

0
virco