Skip to content

Comment puis-je évaluer avec précision la vitesse d'accès non aligné sur x86_64.

Si vous trouvez un détail que vous ne comprenez pas, vous pouvez le laisser dans la section des commentaires et nous essaierons de vous aider aussi vite que possible.

Solution :

Méthode de chronométrage. J'aurais probablement fait en sorte que le test soit sélectionné par une arg de ligne de commande, afin de pouvoir le chronométrer avec... perf stat ./unaligned-testet obtenir des résultats de compteur de perf au lieu de simples temps d'horloge murale pour chaque test. De cette façon, je n'aurais pas à me soucier du turbo / de l'économie d'énergie, puisque je pourrais mesurer en cycles d'horloge du noyau. (Ce n'est pas la même chose que gettimeofday / rdtsc cycles de référence, sauf si vous désactivez le turbo et les autres variations de fréquence).


Vous ne testez que le débit, pas la latence, car aucune des charges n'est dépendante.

Vos chiffres de cache seront pires que vos chiffres de mémoire, mais vous ne réaliserez peut-être pas que c'est parce que vos chiffres de cache peuvent être dus à un goulot d'étranglement sur le nombre de registres à charge fractionnée qui gèrent les charges/stores qui traversent une limite de ligne de cache. Dans le cas de la lecture séquentielle, les niveaux extérieurs du cache ne verront toujours qu'une séquence de demandes de lignes de cache entières. Seules les unités d'exécution recevant des données de L1D doivent se préoccuper de l'alignement. Pour tester le désalignement pour le cas sans cache, vous pourriez faire des charges dispersées, donc les divisions de lignes de cache devraient amener deux lignes de cache dans L1.

Les lignes de cache ont une largeur de 64B 1 Les lignes de cache ont une largeur de 64B, donc vous testez toujours un mélange de splits de lignes de cache et d'accès à l'intérieur d'une ligne de cache. Tester des charges toujours divisées serait un goulot d'étranglement plus difficile sur les ressources microarchitecturales de la charge divisée. (En fait, en fonction de votre CPU, l'option largeur de recherche dans le cache pourrait être plus étroite que la taille de la ligne. Les CPU Intel récents peuvent aller chercher n'importe quel chunk non aligné à l'intérieur d'une ligne de cache, mais c'est parce qu'ils ont un matériel spécial pour rendre cela rapide. D'autres processeurs ne sont plus rapides que lorsqu'ils récupèrent un morceau de 16B naturellement aligné ou autre. @BeeOnRope dit que les CPU AMD peuvent se soucier des limites de 16B et 32B).

Vous ne testez pas store->load forwarding du tout. Pour les tests existants, et une façon agréable de visualiser les résultats pour différents alignements, voir ce billet de blog de stuffedcow.net : Store-to-Load Forwarding et désambiguïsation de la mémoire dans les processeurs x86.

Le passage des données par la mémoire est un cas d'utilisation important, et le désalignement + les divisions de lignes de cache peuvent interférer avec le store-forwarding sur certains CPU. Pour tester correctement cela, assurez-vous de tester différents désalignements, pas seulement 1:15 (vecteur) ou 1:3 (entier). (Vous ne testez actuellement qu'un décalage de +1 par rapport à l'alignement 16B).

J'oublie si c'est juste pour le store-forwarding, ou pour les charges régulières, mais il peut y avoir moins de pénalité lorsqu'une charge est divisée uniformément sur une limite de ligne de cache (un vecteur 8:8, et peut-être aussi des divisions entières 4:4 ou 2:2). Vous devriez tester cela. (Je pense peut-être au P4 lddqu ou Core 2 movqdu)

Le manuel d'optimisation d'Intel a de grands tableaux de désalignement par rapport au store-forwarding à partir d'un store large vers des rechargements étroits qui sont entièrement contenus dans celui-ci. Sur certains processeurs, cela fonctionne dans plus de cas lorsque le magasin large était naturellement aligné, même s'il ne traverse aucune limite de ligne de cache. (Peut-être sur SnB/IvB, puisqu'ils utilisent un cache L1 banked avec des banques de 16B, et que les divisions à travers celles-ci peuvent affecter le store forwarding.
Je n'ai pas revérifié le manuel, mais si vous voulez vraiment tester cela expérimentalement, c'est quelque chose que vous devriez rechercher).


Ce qui me rappelle que les chargements mal alignés sont plus susceptibles de provoquer des conflits cache-banque sur SnB/IvB (car un chargement peut toucher deux banques). Mais vous ne verrez pas ce chargement à partir d'un seul flux, parce que l'accès à la même banque dans le... même deux fois dans un cycle est bien. C'est seulement l'accès à la même banque dans la ligne différentes qui ne peut pas se produire dans le même cycle. (Par exemple, lorsque deux accès à la mémoire sont séparés par un multiple de 128B).

Vous ne faites aucune tentative pour tester les fractionnements de pages 4k. Ils sont plus lents que les splits de lignes de cache réguliers, car ils nécessitent également deux vérifications TLB. (Skylake les a améliorés de ~100 cycles de pénalité à ~5 cycles de pénalité au-delà de la latence normale d'utilisation de la charge, cependant).

Vous ne testez pas movups sur les adresses alignées, donc vous ne détecteriez pas que movups est plus lent que movaps sur Core2 et plus tôt, même lorsque la mémoire est alignée au moment de l'exécution. (Je pense que la mémoire non alignée mov Les chargements allant jusqu'à 8 octets ne posaient pas de problème, même dans le Core2, tant qu'ils ne franchissaient pas la limite d'une ligne de cache. Je ne sais pas de quand date un processeur que vous devriez examiner pour trouver un problème avec des charges non vectorielles dans une ligne de cache. Il s'agirait d'un processeur 32 bits uniquement, mais vous pourriez toujours tester les charges 8B avec MMX ou SSE, ou même x87. Les Pentium P5 et ultérieurs garantissent que les charges/stores 8B alignés sont atomiques, mais les P6 et plus récents garantissent que les charges/stores 8B en cache sont atomiques tant qu'aucune limite de ligne de cache n'est franchie. Contrairement à AMD où les limites de 8B ont de l'importance pour les garanties d'atomicité même dans une mémoire cache. Pourquoi l'affectation d'un entier sur une variable naturellement alignée est atomique sur x86).

Allez voir les trucs d'Agner Fog pour en savoir plus sur la façon dont les charges non alignées peuvent être plus lentes, et cuisinez des tests pour exercer ces cas. En fait, Agner n'est peut-être pas la meilleure ressource pour cela, puisque son guide microarch se concentre principalement sur l'obtention d'uops à travers le pipeline. Juste une brève mention du coût des divisions de lignes de cache, rien d'approfondi sur le débit par rapport à la latence.

Voir aussi : Cacheline splits, take two, du blog de Dark Shikari (développeur principal de x264), parlant des stratégies de chargement non alignées sur Core2 : cela valait la peine de vérifier l'alignement et d'utiliser une stratégie différente pour le bloc.


Notes de bas de page :

  1. 64B lignes de cache est une hypothèse sûre de nos jours. Les Pentium 3 et antérieurs avaient 32B lignes. Le P4 avait 64B lignes mais elles étaient souvent transférées par paires alignées sur 128B. Je pensais me souvenir avoir lu que le P4 avait en fait 128B lignes dans L2 ou L3, mais peut-être n'était-ce qu'une déformation de 64B lignes transférées par paires. 7-CPU dit définitivement 64B lignes dans les deux niveaux de cache pour un P4 130nm.

Voir aussi les résultats de uarch-bench pour Skylake.. Apparemment, quelqu'un a déjà écrit un testeur qui vérifie tous les désalignements possibles par rapport à une limite de ligne de cache.


Mes tests sur un ordinateur de bureau Skylake (i7-6700k) :

Le mode d'adressage affecte la latence d'utilisation de la charge, exactement comme Intel le documente dans son manuel d'optimisation. J'ai testé avec des entiers mov rax, [rax+...], et avec movzx/sx (dans ce cas, en utilisant la valeur chargée comme un index, car elle est trop étroite pour être un pointeur).

;;;  Linux x86-64 NASM/YASM source.  Assemble into a static binary
;; public domain, originally written by [email protected]
;; Share and enjoy.  If it breaks, you get to keep both pieces.

;;; This kind of grew while I was testing and thinking of things to test
;;; I left in some of the comments, but took out most of them and summarized the results outside this code block
;;; When I thought of something new to test, I'd edit, save, and up-arrow my assemble-and-run shell command
;;; Then edit the result into a comment in the source.

section .bss

ALIGN   2 * 1<<20   ; 2MB = 4096*512.  Uses hugepages in .bss but not in .data.  I checked in /proc//smaps
buf:    resb 16 * 1<<20

section .text
global _start
_start:
    mov     esi, 128

;   mov             edx, 64*123 + 8
;   mov             edx, 64*123 + 0
;   mov             edx, 64*64 + 0
    xor             edx,edx
   ;; RAX points into buf, 16B into the last 4k page of a 2M hugepage

    mov             eax, buf + (2<<20)*0 + 4096*511 + 64*0 + 16
    mov             ecx, 25000000

%define ADDR(x)  x                     ; SKL: 4c
;%define ADDR(x)  x + rdx              ; SKL: 5c
;%define ADDR(x)  128+60 + x + rdx*2   ; SKL: 11c cache-line split
;%define ADDR(x)  x-8                 ; SKL: 5c
;%define ADDR(x)  x-7                 ; SKL: 12c for 4k-split (even if it's in the middle of a hugepage)
; ... many more things and a block of other result-recording comments taken out

%define dst rax

        mov             [ADDR(rax)], dst
align 32
.loop:
        mov             dst, [ADDR(rax)]
        mov             dst, [ADDR(rax)]
        mov             dst, [ADDR(rax)]
        mov             dst, [ADDR(rax)]
    dec         ecx
    jnz .loop

        xor edi,edi
        mov eax,231
    syscall

Ensuite, exécutez avec

asm-link load-use-latency.asm && disas load-use-latency && 
    perf stat -etask-clock,cycles,L1-dcache-loads,instructions,branches -r4 ./load-use-latency

+ yasm -felf64 -Worphan-labels -gdwarf2 load-use-latency.asm
+ ld -o load-use-latency load-use-latency.o
 (disassembly output so my terminal history has the asm with the perf results)

 Performance counter stats for './load-use-latency' (4 runs):

     91.422838      task-clock:u (msec)       #    0.990 CPUs utilized            ( +-  0.09% )
   400,105,802      cycles:u                  #    4.376 GHz                      ( +-  0.00% )
   100,000,013      L1-dcache-loads:u         # 1093.819 M/sec                    ( +-  0.00% )
   150,000,039      instructions:u            #    0.37  insn per cycle           ( +-  0.00% )
    25,000,031      branches:u                #  273.455 M/sec                    ( +-  0.00% )

   0.092365514 seconds time elapsed                                          ( +-  0.52% )

Dans ce cas, je testais mov rax, [rax], naturellement alignés, donc cycles = 4*L1-dcache-loads. 4c latence. Je n'ai pas désactivé le turbo ou quelque chose comme ça. Puisque rien ne sort du noyau, les cycles d'horloge du noyau sont la meilleure façon de mesurer.

  • [base + 0..2047]: 4c latence d'utilisation de charge, 11c division de ligne de cache, 11c division de page 4k (même si à l'intérieur de la même hugepage). Voir Is there a penalty when base+offset is in a different page than the base ? pour plus de détails : if base+disp se trouve dans une page différente de basel'opération de chargement doit être rejouée.
  • tout autre mode d'adressage : 5c latence, 11c division de la ligne de cache, 12c division de 4k (même à l'intérieur d'une hugepage). Cela comprend [rax - 16]. Ce n'est pas disp8 vs disp32 qui fait la différence.

Donc : les hugepages ne permettent pas d'éviter les pénalités liées à la division de la page (du moins pas lorsque les deux pages sont chaudes dans la TLB). Un split de ligne de cache rend le mode d'adressage non pertinent, mais les modes d'adressage "rapides" ont une latence inférieure de 1c pour les chargements normaux et les chargements par fractionnement de page.

La gestion du fractionnement 4k est fantastiquement meilleure qu'avant, voir les chiffres de @harold où Haswell a ~32c de latence pour un fractionnement 4k. (Et les anciens processeurs peuvent être encore pires que cela. Je pensais qu'avant SKL, c'était censé être une pénalité de ~100 cycles).

Débit (indépendamment du mode d'adressage), mesuré en utilisant une destination autre que rax pour que les charges soient indépendantes :

  • pas de fractionnement : 0,5c.
  • CL-split : 1c.
  • 4k-split : ~3,8 à 3,9c (beaucoup plus mieux que les CPU pré-Skylake)

Même débit/latence pour movzx/movsx (y compris les splits WORD), comme prévu car ils sont gérés dans le port de charge (contrairement à certains CPU AMD, où il y a aussi une uop ALU).

Les charges de fractionnement de lignes de cache sont rejouées depuis la RS (Reservation Station). Les compteurs pour uops_dispatched_port.port_2 + port_3 = 2x le nombre de mov rdi, [rdi]Dans un autre test utilisant essentiellement la même boucle. (C'était un cas de charge dépendante, pas limité par le débit.) On ne peut pas détecter une charge divisée avant l'AGU.

Vraisemblablement, lorsqu'une uop de chargement découvre qu'elle a besoin des données d'une 2e ligne, elle recherche un registre de fractionnement (le tampon que les CPU Intel utilisent pour gérer les charges fractionnées), et met la partie nécessaire des données de la première ligne dans ce registre de fractionnement. Il signale également au RS qu'il doit être rejoué. (C'est une conjecture.)

Je pense que même si aucune ligne de cache n'est présente sur un split, le replay split-load devrait se produire dans quelques cycles (peut-être dès que le port de chargement signale au RS que c'était un split, c'est-à-dire après la génération d'adresse). Ainsi, les requêtes de demande et de charge des deux côtés du fractionnement peuvent être en vol en même temps.


Voir aussi Effets bizarres sur les performances de magasins dépendants proches dans une boucle de chasse aux pointeurs sur IvyBridge. L'ajout d'une charge supplémentaire l'accélère ? pour en savoir plus sur les répétitions d'uop. (Mais notez que c'est pour les uops dépendant de une charge, pas l'uop de charge elle-même. Dans cette Q&A, les uops dépendantes sont aussi pour la plupart des charges).

Une charge cache-miss n'est pas elle-même besoin d'être rejoué pour "accepter" les données entrantes quand il est prêt, seulement les uops dépendants.. Voir la discussion en ligne sur les load ops sont-elles désallouées du RS lorsqu'elles sont distribuées, terminées ou à un autre moment ? Ce cas de test https://godbolt.org/z/HJF3BN NASM sur i7-6700k montre le même nombre d'uops de charge distribués indépendamment des hits L1d ou L3. Mais le nombre d'uops ALU distribués (sans compter les surcharges de boucle) passe de 1 par charge à ~8,75 par charge. Le planificateur planifie agressivement les uops consommant les données à distribuer dans le cycle où les données de charge pourraient arriver du cache L2 (et ensuite très agressivement après cela, il semble), au lieu d'attendre un cycle supplémentaire pour voir si c'est le cas ou non.

Nous n'avons pas testé à quel point le rejeu est agressif quand il y a un autre travail indépendant mais plus jeune qui pourrait être fait sur le même port dont les entrées sont définitivement prêtes.


SKL a deux unités matérielles de page-walk, ce qui est probablement lié à l'amélioration massive des performances de 4k-split.. Même lorsqu'il n'y a pas de ratés de TLB, les anciens processeurs devaient vraisemblablement tenir compte du fait qu'il pourrait y en avoir.

Il est intéressant que le débit de 4k-split soit non entier. Je pense que mes mesures avaient suffisamment de précision et de répétabilité pour dire cela. Rappelez-vous que c'est avec chaque étant un 4k-split, et aucun autre travail en cours (sauf pour être à l'intérieur d'une petite boucle dec/jnz). Si jamais vous avez cela dans un code réel, vous faites quelque chose de vraiment mauvais.

Je n'ai pas de suppositions solides sur la raison pour laquelle il pourrait être non entier, mais il y a clairement beaucoup de choses qui doivent se produire microarchitecturalement pour un 4k-split. C'est toujours un split de ligne de cache, et il doit vérifier la TLB deux fois.

En testant les charges 64bit pour différents décalages (code ci-dessous), mes résultats bruts sur Haswell sont :

aligned L: 4.01115 T: 0.500003
ofs1 L: 4.00919 T: 0.500003
ofs2 L: 4.01494 T: 0.500003
ofs3 L: 4.01403 T: 0.500003
ofs7 L: 4.01073 T: 0.500003
ofs15 L: 4.01937 T: 0.500003
ofs31 L: 4.02107 T: 0.500002
ofs60 L: 9.01482 T: 1
ofs62 L: 9.03644 T: 1
ofs4092 L: 32.3014 T: 31.1967

appliquer l'arrondi comme vous le jugez bon, la plupart d'entre eux devraient évidemment être arrondis vers le bas, mais .3 et .2 (du passage de la limite de la page) sont peut-être trop significatifs pour être du bruit. Cela n'a testé que des charges avec des adresses simples, et seulement des "charges pures", sans transfert.

Je conclus que l'alignement à l'intérieur d'une ligne de cache n'est pas pertinent pour les charges scalaires, seul le franchissement des limites de la ligne de cache et (surtout, et pour des raisons évidentes) le franchissement des limites de la page importe. Il ne semble pas y avoir de différence entre traverser une frontière de ligne de cache exactement au milieu ou ailleurs dans ce cas.

AMD a occasionnellement des effets bizarres avec les limites de 16 octets, mais je ne peux pas le tester.

Et voici les résultats bruts( !) du vecteur xmm qui incluent les effets de... pextrq, donc soustrayez 2 cycles de latence :

aligned L: 8.05247 T: 0.500003
ofs1 L: 8.03223 T: 0.500003
ofs2 L: 8.02899 T: 0.500003
ofs3 L: 8.05598 T: 0.500003
ofs7 L: 8.03579 T: 0.500002
ofs15 L: 8.02787 T: 0.500003
ofs31 L: 8.05002 T: 0.500003
ofs58 L: 13.0404 T: 1
ofs60 L: 13.0825 T: 1
ofs62 L: 13.0935 T: 1
ofs4092 L: 36.345 T: 31.2357

Le code de test était

global test_unaligned_l
proc_frame test_unaligned_l
    alloc_stack 8
[endprolog]
    mov r9, rcx
    rdtscp
    mov r8d, eax

    mov ecx, -10000000
    mov rdx, r9
.loop:
    mov rdx, [rdx]
    mov rdx, [rdx]
    add ecx, 1
    jnc .loop

    rdtscp
    sub eax, r8d

    add rsp, 8
    ret
endproc_frame

global test_unaligned_tp
proc_frame test_unaligned_tp
    alloc_stack 8
[endprolog]
    mov r9, rcx
    rdtscp
    mov r8d, eax

    mov ecx, -10000000
    mov rdx, r9
.loop:
    mov rax, [rdx]
    mov rax, [rdx]
    add ecx, 1
    jnc .loop

    rdtscp
    sub eax, r8d

    add rsp, 8
    ret
endproc_frame

Pour des vecteurs largement similaires mais avec pextrq dans le test de latence.

Avec quelques données préparées à divers décalages, par exemple :

align 64
%rep 31
db 0
%endrep
unaligned31: dq unaligned31
align 4096
%rep 60
db 0
%endrep
unaligned60: dq unaligned60
align 4096
%rep 4092
db 0
%endrep
unaligned4092: dq unaligned4092

Pour se concentrer un peu plus sur le nouveau titre, je vais décrire ce que cela essaie de faire et pourquoi.

Tout d'abord, il y a un test de latence. En chargeant un million de choses dans eax à partir d'un pointeur qui n'est pas dans eax (comme le fait le code dans la question) teste le débit, qui n'est que la moitié de l'image. Pour les charges scalaires, c'est trivial, pour les charges vectorielles, j'ai utilisé des paires de :

movdqu xmm0, [rdx]
pextrq rdx, xmm0, 0

La latence de pextrq est de 2, c'est pourquoi les chiffres de latence pour les charges vectorielles sont tous 2 trop élevés comme indiqué.

Afin de faciliter ce test de latence, les données sont un pointeur autoréférentiel. C'est un scénario assez atypique, mais cela ne devrait pas affecter les caractéristiques de temps des charges.

Le test de débit a deux charges par boucle au lieu d'une pour éviter d'être embouteillé par l'overhead de la boucle. Plus de charges pourraient être utilisées, mais ce n'est pas nécessaire sur Haswell (ou tout ce à quoi je peux penser, mais en théorie un µarch avec un débit de branche inférieur ou un débit de charge supérieur pourrait exister).

Je ne suis pas super prudent en ce qui concerne le fencing dans la lecture TSC ou la compensation de son overhead (ou d'autres overheads). Je n'ai pas non plus désactivé le turbo, je l'ai juste laissé tourner à la fréquence du turbo et divisé par le rapport entre le taux de TSC et la fréquence du turbo, ce qui pourrait affecter un peu les timings. Tous ces effets sont tous minuscules par rapport à un benchmark de l'ordre de 1E7, et les résultats peuvent être arrondis de toute façon.

Tous les temps sont les meilleurs sur 30, des choses comme la moyenne et la variance sont inutiles sur ces micro benchmarks puisque la vérité terrain n'est pas un processus aléatoire avec des paramètres que nous voulons estimer mais un certain nombre entier fixe.[1] (ou un multiple entier d'une fraction, pour le débit). Presque tous les bruits sont positifs, sauf le cas (relativement théorique) des instructions du benchmark qui "fuient" devant la première lecture TSC (on pourrait même l'éviter si nécessaire), donc prendre le minimum est approprié.

Note 1 : sauf le franchissement d'une frontière de 4k apparemment, quelque chose d'étrange se passe là.

Je mets mon benchmark un peu amélioré ici. Il mesure toujours le débit uniquement (et seulement le décalage non aligné 1). Sur la base des autres réponses, j'ai ajouté la mesure des fractionnements de 64 et 4096 octets.

Pour les fractionnements de 4k, il y a une énorme différence ! Mais si les données ne traversent pas la frontière de 64 octets, il n'y a pas du tout de perte de vitesse (du moins pour ces 2 processeurs que j'ai testés).

En regardant ces chiffres (et les chiffres des autres réponses), ma conclusion est que l'accès non aligné est rapide en moyenne (à la fois le débit et la latence), mais il y a des cas où il peut être beaucoup plus lent. Mais cela ne signifie pas que leur utilisation est découragée.

Les chiffres bruts produits par mon benchmark doivent être pris avec un grain de sel (il est fort probable qu'un code asm correctement écrit le surpasse), mais ces résultats sont pour la plupart en accord avec la réponse de harold pour Haswell (colonne différence).

Haswell:

Full:
 32-bit, cache: aligned:  33.2901 GB/sec unaligned:  29.5063 GB/sec, difference: 1.128x
 32-bit,   mem: aligned:  12.1597 GB/sec unaligned:  12.0659 GB/sec, difference: 1.008x
 64-bit, cache: aligned:  66.0368 GB/sec unaligned:  52.8914 GB/sec, difference: 1.249x
 64-bit,   mem: aligned:  16.1317 GB/sec unaligned:  16.0568 GB/sec, difference: 1.005x
128-bit, cache: aligned: 129.8730 GB/sec unaligned:  87.9791 GB/sec, difference: 1.476x
128-bit,   mem: aligned:  16.8150 GB/sec unaligned:  16.8151 GB/sec, difference: 1.000x

JustBoundary64:
 32-bit, cache: aligned:  32.5555 GB/sec unaligned:  16.0175 GB/sec, difference: 2.032x
 32-bit,   mem: aligned:   1.0044 GB/sec unaligned:   1.0001 GB/sec, difference: 1.004x
 64-bit, cache: aligned:  65.2707 GB/sec unaligned:  32.0431 GB/sec, difference: 2.037x
 64-bit,   mem: aligned:   2.0093 GB/sec unaligned:   2.0007 GB/sec, difference: 1.004x
128-bit, cache: aligned: 130.6789 GB/sec unaligned:  64.0851 GB/sec, difference: 2.039x
128-bit,   mem: aligned:   4.0180 GB/sec unaligned:   3.9994 GB/sec, difference: 1.005x

WithoutBoundary64:
 32-bit, cache: aligned:  33.2911 GB/sec unaligned:  33.2916 GB/sec, difference: 1.000x
 32-bit,   mem: aligned:  11.6156 GB/sec unaligned:  11.6223 GB/sec, difference: 0.999x
 64-bit, cache: aligned:  65.9117 GB/sec unaligned:  65.9548 GB/sec, difference: 0.999x
 64-bit,   mem: aligned:  14.3200 GB/sec unaligned:  14.3027 GB/sec, difference: 1.001x
128-bit, cache: aligned: 128.2605 GB/sec unaligned: 128.3342 GB/sec, difference: 0.999x
128-bit,   mem: aligned:  12.6352 GB/sec unaligned:  12.6218 GB/sec, difference: 1.001x

JustBoundary4096:
 32-bit, cache: aligned:  33.5500 GB/sec unaligned:   0.5415 GB/sec, difference: 61.953x
 32-bit,   mem: aligned:   0.4527 GB/sec unaligned:   0.0431 GB/sec, difference: 10.515x
 64-bit, cache: aligned:  67.1141 GB/sec unaligned:   1.0836 GB/sec, difference: 61.937x
 64-bit,   mem: aligned:   0.9112 GB/sec unaligned:   0.0861 GB/sec, difference: 10.582x
128-bit, cache: aligned: 134.2000 GB/sec unaligned:   2.1668 GB/sec, difference: 61.936x
128-bit,   mem: aligned:   1.8165 GB/sec unaligned:   0.1700 GB/sec, difference: 10.687x

Sandy Bridge (processor from 2011)

Full:
 32-bit, cache: aligned:  30.0302 GB/sec unaligned:  26.2587 GB/sec, difference: 1.144x
 32-bit,   mem: aligned:  11.0317 GB/sec unaligned:  10.9358 GB/sec, difference: 1.009x
 64-bit, cache: aligned:  59.2220 GB/sec unaligned:  41.5515 GB/sec, difference: 1.425x
 64-bit,   mem: aligned:  14.5985 GB/sec unaligned:  14.3760 GB/sec, difference: 1.015x
128-bit, cache: aligned: 115.7643 GB/sec unaligned:  45.0905 GB/sec, difference: 2.567x
128-bit,   mem: aligned:  14.8561 GB/sec unaligned:  14.8220 GB/sec, difference: 1.002x

JustBoundary64:
 32-bit, cache: aligned:  15.2127 GB/sec unaligned:   3.1037 GB/sec, difference: 4.902x
 32-bit,   mem: aligned:   0.9870 GB/sec unaligned:   0.6110 GB/sec, difference: 1.615x
 64-bit, cache: aligned:  30.2074 GB/sec unaligned:   6.2258 GB/sec, difference: 4.852x
 64-bit,   mem: aligned:   1.9739 GB/sec unaligned:   1.2194 GB/sec, difference: 1.619x
128-bit, cache: aligned:  60.7265 GB/sec unaligned:  12.4007 GB/sec, difference: 4.897x
128-bit,   mem: aligned:   3.9443 GB/sec unaligned:   2.4460 GB/sec, difference: 1.613x

WithoutBoundary64:
 32-bit, cache: aligned:  30.0348 GB/sec unaligned:  29.9801 GB/sec, difference: 1.002x
 32-bit,   mem: aligned:  10.7067 GB/sec unaligned:  10.6755 GB/sec, difference: 1.003x
 64-bit, cache: aligned:  59.1895 GB/sec unaligned:  59.1925 GB/sec, difference: 1.000x
 64-bit,   mem: aligned:  12.9404 GB/sec unaligned:  12.9307 GB/sec, difference: 1.001x
128-bit, cache: aligned: 116.4629 GB/sec unaligned: 116.0778 GB/sec, difference: 1.003x
128-bit,   mem: aligned:  11.2963 GB/sec unaligned:  11.3533 GB/sec, difference: 0.995x

JustBoundary4096:
 32-bit, cache: aligned:  30.2457 GB/sec unaligned:   0.5626 GB/sec, difference: 53.760x
 32-bit,   mem: aligned:   0.4055 GB/sec unaligned:   0.0275 GB/sec, difference: 14.726x
 64-bit, cache: aligned:  60.6175 GB/sec unaligned:   1.1257 GB/sec, difference: 53.851x
 64-bit,   mem: aligned:   0.8150 GB/sec unaligned:   0.0551 GB/sec, difference: 14.798x
128-bit, cache: aligned: 121.2121 GB/sec unaligned:   2.2455 GB/sec, difference: 53.979x
128-bit,   mem: aligned:   1.6255 GB/sec unaligned:   0.1103 GB/sec, difference: 14.744x

Voici le code :

#include 
#include 

__attribute__((always_inline))
void load32(const char *v) {
    __asm__ ("mov     %0, %%eax" : : "m"(*v) :"eax");
}

__attribute__((always_inline))
void load64(const char *v) {
    __asm__ ("mov     %0, %%rax" : : "m"(*v) :"rax");
}

__attribute__((always_inline))
void load128a(const char *v) {
    __asm__ ("movaps     %0, %%xmm0" : : "m"(*v) :"xmm0");
}

__attribute__((always_inline))
void load128u(const char *v) {
    __asm__ ("movups     %0, %%xmm0" : : "m"(*v) :"xmm0");
}

struct Full {
    template 
    static float factor() {
        return 1.0f;
    }
    template 
    static void loop(const char *v) {
        for (int i=0; i
    static float factor() {
        return S/64.0f;
    }
    template 
    static void loop(const char *v) {
        static_assert(N%(64*16)==0);
        for (int i=0; i
    static float factor() {
        return (64-S)/64.0f;
    }
    template 
    static void loop(const char *v) {
        for (int i=0; i
    static float factor() {
        return S/4096.0f;
    }
    template 
    static void loop(const char *v) {
        static_assert(N%(4096*4)==0);
        for (int i=0; i
void bench(const char *data, int iter, const char *name) {
    long long int t0 = t();
    for (int i=0; i(data);
    }
    long long int t1 = t();
    for (int i=0; i(data+1);
    }
    long long int t2 = t();
    for (int i=0; i(data);
    }
    long long int t3 = t();
    for (int i=0; i(data+1);
    }
    long long int t4 = t();

    printf("%s-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fxn", name, (double)N*iter/(t1-t0)/1000*TYPE::template factor(), (double)N*iter/(t2-t1)/1000*TYPE::template factor(), (float)(t2-t1)/(t1-t0));
    printf("%s-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fxn", name, (double)N*iter/(t3-t2)/1000*TYPE::template factor(), (double)N*iter/(t4-t3)/1000*TYPE::template factor(), (float)(t4-t3)/(t3-t2));
}

int main() {
    const int ITER = 10;
    const int N = 1638400000;

    char *data = reinterpret_cast(((reinterpret_cast(new char[N+8192])+4095)&~4095));
    for (int i=0; i(data, ITER, " 32");
    bench(data, ITER, " 64");
    bench(data, ITER, "128");

    printf("nJustBoundary64:n");
    bench(data, ITER, " 32");
    bench(data, ITER, " 64");
    bench(data, ITER, "128");

    printf("nWithoutBoundary64:n");
    bench(data, ITER, " 32");
    bench(data, ITER, " 64");
    bench(data, ITER, "128");

    printf("nJustBoundary4096:n");
    bench(data, ITER*10, " 32");
    bench(data, ITER*10, " 64");
    bench(data, ITER*10, "128");
}

Si vous faites défiler, vous pouvez trouver les annotations d'autres chefs de projet, vous avez également la possibilité de montrer la vôtre si vous en avez envie.



Utilisez notre moteur de recherche

Ricerca
Generic filters

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.