Si vous n'avez pas besoin d'aléatoire de très haute qualité et que la distribution presque uniforme convient, vous pouvez aller très vite, en particulier sur un processeur moderne doté de vecteurs entiers SIMD efficaces tels que x86 avec SSE2 ou AVX2.
Cela ressemble à la réponse de @ NominalAnimal puisque nous avions tous deux la même idée, mais vectorisés manuellement pour x86. (Et avec des nombres aléatoires de qualité médiocre, mais toujours suffisants pour de nombreux cas d'utilisation.) Cela s'exécute environ 15 ou 30 fois plus rapidement que le code de @ Nominal, à environ 13 Go / s de sortie ASCII sur un Intel Haswell cadencé à 2,5 GHz. CPU avec AVX2. C’est toujours moins que la bande passante maximale théorique de la mémoire principale (la DDR3-1600 à double canal est d’environ 25,6 Go / s), mais j’étais en train d’écrire sur / dev / null; Skylake devrait exécuter ce même code considérablement plus rapidement que Haswell (voir le bas de cette réponse).
En supposant que vous ayez un goulot d’étranglement sur les entrées / sorties sur le disque ou sur une tuyauterie quelque part, une implémentation rapide signifie que votre processeur n’a même pas besoin d’être plus rapide qu’au ralenti. Il utilise beaucoup moins d'énergie totale pour produire le résultat. (Vie de la batterie / chaleur / réchauffement climatique.)
C'est tellement rapide que vous ne voulez probablement pas l'écrire sur le disque. Il suffit de générer de nouveau au besoin (à partir de la même graine si vous voulez à nouveau les mêmes données). Même si vous souhaitez le transmettre à un processus multithread pouvant utiliser tous les processeurs, son exécution pour y diriger les données le laissera chaud dans le cache L3 (et le cache L2 du noyau qui l'a écrit), et peu de temps processeur. (Mais notez que la tuyauterie ajoute beaucoup de frais généraux par rapport à l’écriture /dev/null
. Sur un Skylake i7-6700k, la tuyauterie vers wc -c
ou un autre programme qui lit juste + supprime son entrée, c’est environ 8 fois plus lent que l’écriture/dev/null
, et n’utilise que 70% CPU, mais cela reste 4,0 Go / s sur un processeur 3.9GHz.
La régénérer est plus rapide que de le relire même à partir d'un SSD rapide connecté par PCIe, mais un IDK s'il est plus économe en énergie (le multiplicateur de vecteur entier est assez occupé et il est probablement très gourmand en énergie, avec d'autres AVX2 256b ALU vectorielles). OTOH, je ne sais pas combien de temps CPU lire sur un disque enlèverait quelque chose qui dépassait tous les cœurs traitant cette entrée. J'imagine qu'un changement de contexte pour générer de nouveau des morceaux de 128 Ko pourrait être concurrentiel avec le code de système de fichiers / pagecache en cours d'exécution et l'allocation de pages pour la lecture de données à partir du disque. Bien sûr, si il fait déjà chaud dans la pagecache, c’est tout simplement mémorable. OTOH, nous écrivons déjà à peu près aussi vite que memcpy! (qui doit partager la bande passante de la mémoire principale entre la lecture et l’écriture). (Notez également que l'écriture en mémoire que 'rep movsb
(optimisé memcpy et memset dans le microcode, ce qui évite les RFO, puisque Andy Glew l’a implémenté dans P6 (Pentium Pro )).
Jusqu'à présent, il ne s'agit que d'une preuve de concept et la gestion de la nouvelle ligne n'est qu'approximativement correcte. C'est faux autour des extrémités d'un tampon de puissance de 2. Avec plus de temps de développement. Je suis convaincu que je pourrais trouver un moyen plus efficace d'insérer des nouvelles lignes tout à fait corrects, avec des frais généraux au moins aussi faibles que cela (par rapport à la sortie d'espaces uniquement). Je pense que c'est quelque chose comme 10 à 20%. Je voudrais seulement savoir à quelle vitesse nous pourrions faire cela, et non pas en avoir une version perfectionnée. Je laisserai donc cette partie comme un exercice pour le lecteur, avec des commentaires décrivant certaines idées.
Sur un Haswell i5 à son turbo maxi de 2,5 GHz, avec une RAM DDR3-1600 MHz , chronométré à 100 GiB mais réduit. (Chronométré sur cygwin64 sur Win10 avec gcc5.4 -O3 -march=native
, omis -funroll-loops
car j'avais assez de mal à obtenir un timing correct sur cet ordinateur portable emprunté. Il aurait fallu démarrer Linux sur une clé USB).
écrire dans / dev / null sauf indication contraire.
- James Hollis's: (non testé)
- La version fwrite de Nominal: ~ 2.21s
- this (SSE2): ~ 0,142 (temps non mis à l'échelle = réel = 14,232s, utilisateur = 13,999s, sys = 0,187s).
- ceci (AVX-128): ~ 0.140s
- this (AVX2): ~ 0,073 s (non mis à l'échelle: réel = 0m7,291s, utilisateur = 0m7,125s, sys = 0m0,155s).
- cette tuyauterie cygwin (AVX2) vers
wc -c
, avec une taille de mémoire tampon de 128 Ko: 0,32 seconde avec un processeur à 2,38 GHz (turbo dual-core max). (Temps non mis à l'échelle: réel = 32.466s utilisateur = 11.468s sys = 41.092s, y compris ceci et wc
). Cependant, seulement la moitié des données ont été copiées, car mon programme idiot suppose que l’écriture remplit la totalité du tampon, même si ce n’est pas le cas et que cygwin write () ne produit que 64k par appel dans un tube.
Donc, avec SSE2, il est environ 15 fois plus rapide que le code scalaire de @Nominal Animal. Avec AVX2, c'est environ 30 fois plus rapide. Je n'ai pas essayé une version du code de Nominal qui utilise simplement write()
au lieu de fwrite()
, mais vraisemblablement pour les grands tampons, stdio reste généralement à l'écart. S'il s'agit de copier les données, cela entraînerait beaucoup de ralentissement.
Il est temps de produire 1 Go de données sur un Core2Duo E6600 (caches Merom 2,4 GHz, L1 privé 32 koB, caches L2 partagés 4 Mo), DDR2-533 MHz sous Linux 4.2 64 bits (Ubuntu 15.10). Toujours en utilisant une taille de tampon de 128kiB pour write (), je n’ai pas exploré cette dimension.
écrire dans / dev / null sauf indication contraire.
- (SSE2) cela avec une manipulation de nouvelle ligne et 4 vecteurs de chiffres de chaque vecteur d'octets aléatoires: 0,183s (chronométré en faisant 100GiB en 18.3s, mais des résultats similaires pour les exécutions 1GiB). 1,85 instructions par cycle.
- (SSE2) cela, la tuyauterie à
wc -c
: 0.593s (non mis à l'échelle: real = 59.266s utilisateur = 20.148s sys = 1m6.548s, temps de calcul de wc compris). Même nombre d'appels système write () que pour cygwin, mais en réalité, canalisation de toutes les données, car Linux gère tous les 128k d'un write () vers un canal.
- La
fwrite()
version de NominalAnimal (gcc5.2 -O3 -march=native
), fonctionnant avec ./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3.19s +/- 0.1%, avec 1,40 instruction par cycle. Les boucles de compression ont peut-être fait une petite différence. clang-3.8 -O3 -march=native
: 3.42s ± 0.1%
- Piping nominal
fwrite
à wc -c
: real = utilisateur 3.980 = 3.176s sys = 2.080s
- Version par ligne de James Hollis (
clang++-3.8 -O3 -march=native
): 22.885s +/- 0.07%, avec 0.84 instructions par cycle. (g ++ 5,2 était légèrement plus lent: 22,98s). Écrire une ligne à la fois a probablement fait très mal.
- Stéphane Chazelas's
tr < /dev/urandom | ...
: real = 41.430s user = 26.832s sys = 40.120s. tr
La plupart du temps, la totalité du cœur de la CPU était occupée par lui-même, passant presque tout son temps dans le pilote du noyau à générer des octets aléatoires et à les copier dans un canal. L'autre cœur de cette machine double cœur exécutait le reste du pipeline.
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: c'est- à- dire en train de lire autant d'aléas sans tuyauterie: real = 35.018s user = 0.036s sys = 34.940s.
- Programme perl de Lưu Vĩnh Phúc (perl v5.20.2 d'Ubuntu15.10)
LANG=en_CA.UTF-8
:: real = 4m32.634s user = 4m3.288s sys = 0m29.364.
LC_ALL=C LANG=C
: real = 4m18.637s user = 3m50.324s sys = 0m29.356s. Toujours très lent.
- (SSE2) ceci sans traitement de saut de ligne , ni avec 3 ou 4 vecteurs de chiffres de chaque vecteur d'octets aléatoires (presque exactement la même vitesse: l'
dig3 = v%10
étape concerne le seuil de rentabilité sur ce matériel): 0,166 (1,82 instructions par cycle) . C’est fondamentalement la limite inférieure de ce que nous pouvons approcher avec une manipulation de nouvelle ligne parfaitement efficace.
- (SSE2) Ancienne version de cette manipulation sans saut de ligne, mais seulement obtenir un chiffre par élément uint16_t à l' aide
v%10
, 0.222 secondes +/- 0,4%, 2.12 instructions par cycle. (Compilé avec gcc5.2, les -march=native -O3 -funroll-loops
boucles de déroulage peuvent aider pour ce code sur ce matériel. Ne l'utilisez pas aveuglément, en particulier pour les gros programmes).
- (SSE2) Ancienne version de cette opération, écriture dans un fichier (sur un disque RAID10f2 de 3 disques durs magnétiques rapides, pas très optimisé pour les écritures): ~ 4 secondes. Pourrait aller plus vite en modifiant les paramètres de la mémoire tampon d'E / S du noyau afin de permettre beaucoup plus de données altérées avant les blocs write (). Le temps "système" est toujours ~ 1,0 seconde, beaucoup plus élevé que le temps "utilisateur". Sur cet ancien système avec une mémoire vive DDR2-533 lente, il faut environ 4x plus de temps au noyau pour mémoriser les données dans la page de cache et exécuter les fonctions XFS plutôt que pour ma boucle afin de continuer à les réécrire sur place dans une mémoire tampon qui reste chaude. cache.
Comment c'est fait
Un PRNG rapide est évidemment essentiel. xorshift128 + peut être vectorisé, vous avez donc deux ou quatre générateurs 64 bits en parallèle, dans des éléments d’un vecteur SIMD. Chaque étape produit un vecteur complet d'octets aléatoires. ( Implémentation AVX2 256b ici avec les composants intrinsèques Intel ). Je l'ai choisi par rapport au choix de xorshift * de Nominal, car la multiplication vectorielle entière sur 64 bits n'est possible que dans SSE2 / AVX2 avec des techniques de précision étendue .
Avec un vecteur d'octets aléatoires, nous pouvons découper chaque élément de 16 bits en plusieurs chiffres décimaux. Nous produisons plusieurs vecteurs d'éléments 16 bits qui sont chacun un chiffre ASCII + un espace ASCII . Nous stockons cela directement dans notre tampon de sortie.
Ma version originale x / 6554
consistait simplement à obtenir un chiffre aléatoire de chaque élément uint16_t d'un vecteur. C'est toujours entre 0 et 9 inclus. C'est biaisé 9
, car ce (2^16 -1 ) / 6554
n'est que 9.99923. (6554 = ceil ((2 ^ 16-1) / 10), ce qui garantit que le quotient est toujours <10.)
x/6554
peut être calculé en multipliant par une constante "magique" ( la réciproque en virgule fixe ) et un décalage à droite du résultat supérieur. C'est le meilleur cas pour la division par une constante; certains diviseurs prennent plus d'opérations, et la division signée prend un travail supplémentaire. x % 10
a un biais similaire et n'est pas aussi bon marché pour calculer. (la sortie asm de gcc est équivalent à x - 10*(x/10)
, soit une multiplication et de soustraction supplémentaire sur le dessus de la division en utilisant un inverse multiplicatif modulaire.) En outre, le bit le plus bas de xorshift128 + ne sont pas aussi haute qualité , de sorte que la division de prendre l' entropie de bits de poids fort est meilleur ( pour la qualité ainsi que la vitesse) que modulo pour prendre l’entropie des bits bas.
Cependant, nous pouvons utiliser plus d'entropie dans chaque uint16_t en regardant les chiffres décimaux bas, comme la digit()
fonction de @ Nominal . Pour des performances maximales, j'ai décidé de prendre les 3 chiffres décimaux les plus bas et x/6554
, pour enregistrer un PMULLW et un PSUBW (et probablement du MOVDQA) par rapport à l'option de qualité supérieure consistant à prendre les 4 chiffres décimaux les plus bas. x / 6554 est légèrement affecté par les 3 chiffres décimaux, il existe donc une corrélation entre les chiffres du même élément (séparation de 8 ou 16 chiffres dans la sortie ASCII, en fonction de la largeur du vecteur).
Je pense que gcc divise par 100 et par 1000, plutôt que par une chaîne plus longue qui se divise successivement par 10, de sorte que cela ne raccourcit probablement pas de manière significative la longueur de la chaîne de dépendance non acheminée par boucle qui produit 4 résultats pour chaque sortie de PRNG. port0 (vecteur multiplier et déplacer) est le goulot d'étranglement en raison des inverses multiplicatifs modulaires et des changements dans xorshift +, il est donc certainement utile de sauvegarder un vecteur multiplier.
xorshift + est si rapide que même utiliser seulement ~ 3,3 bits d’aléatoire sur 16 (soit une efficacité de 20%) n’est pas beaucoup plus lent que de le découper en plusieurs chiffres décimaux. Nous ne faisons qu’approximer la distribution uniforme, car cette réponse est axée sur la rapidité tant que la qualité n’est pas mauvaise.
Tout type de comportement conditionnel qui conserve un nombre variable d'éléments nécessiterait beaucoup plus de travail. (Mais pourrait peut-être encore être fait assez efficacement en utilisant les techniques de gauchissement SIMD . Cependant, cela devient moins efficace pour les éléments de petite taille; les tables de recherche de masques de masques géants ne sont pas viables, et il n'y a pas de mélange de passages de voies AVX2 inférieur à 32 - Une version PSHUFB de 128b pourrait toujours générer un masque à la volée avec BMI2 PEXT / PDEP, comme vous pouvez le faire pour AVX2 avec des éléments plus volumineux , mais cela pose problème, car un entier de 64 bits ne contient que 8 octets. sur cette réponse a un code qui pourrait fonctionner pour un nombre plus élevé d'éléments.)
Si la latence du générateur de ressources aléatoires est un goulot d'étranglement, nous pourrions aller encore plus vite en exécutant deux vecteurs de générateurs en parallèle, en alternant celui que nous utilisons. Le compilateur peut toujours conserver facilement tous les registres dans une boucle déroulée, ce qui permet aux deux chaînes de dépendance de s'exécuter en parallèle.
Dans la version actuelle, en découpant la sortie du PRNG, nous avons en fait un goulot d'étranglement sur le débit du port 0, et non du temps de latence du PRNG. Ce n'est donc pas nécessaire.
Le code: version AVX2
Version complète avec plus de commentaires sur l'explorateur du compilateur Godbolt .
Pas très bien rangé, désolé je dois me coucher et je veux que ça soit posté.
Pour obtenir la version SSE2, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
et le changement vector_size(32)
à 16 changer également l'incrément de 4 * retour à la ligne 16-4 * 8. (Comme je l'ai dit, le code est complexe et mal configuré pour la compilation de deux versions. Je n'avais pas prévu au départ de créer une version AVX2, mais je voulais vraiment tester sur un processeur Haswell auquel j'avais accès.)
#include <immintrin.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
//#include <string.h>
// This would work equally fast 128b or 256b at a time (AVX2):
// https://stackoverflow.com/questions/24001930/avx-sse-version-of-xorshift128
struct rngstate256 {
__m256i state0;
__m256i state1;
};
static inline __m256i xorshift128plus_avx2(struct rngstate256 *sp)
{
__m256i s1 = sp->state0;
const __m256i s0 = sp->state1;
sp->state0 = s0;
s1 = _mm256_xor_si256(s1, _mm256_slli_epi64(s1, 23));
__m256i state1new = _mm256_xor_si256(_mm256_xor_si256(_mm256_xor_si256(s1, s0),
_mm256_srli_epi64(s1, 18)),
_mm256_srli_epi64(s0, 5));
sp->state1 = state1new;
return _mm256_add_epi64(state1new, s0);
}
// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));
__m256i* vec_store_digit_and_space(__m256i vec, __m256i *restrict p)
{
v16u v = (v16u)vec;
v16u ten = (v16u)_mm256_set1_epi16(10);
v16u divisor = (v16u)_mm256_set1_epi16(6554); // ceil((2^16-1) / 10.0)
v16u div6554 = v / divisor; // Basically the entropy from the upper two decimal digits: 0..65.
// Probably some correlation with the modulo-based values, especially dig3, but we do this instead of
// dig4 for more ILP and fewer instructions total.
v16u dig1 = v % ten;
v /= ten;
v16u dig2 = v % ten;
v /= ten;
v16u dig3 = v % ten;
// dig4 would overlap much of the randomness that div6554 gets
const v16u ascii_digitspace = (v16u)_mm256_set1_epi16( (' '<<8) | '0');
v16u *vecbuf = (v16u*)p;
vecbuf[0] = div6554 | ascii_digitspace;
vecbuf[1] = dig1 | ascii_digitspace;
vecbuf[2] = dig2 | ascii_digitspace;
vecbuf[3] = dig3 | ascii_digitspace;
return p + 4; // always a constant number of full vectors
}
void random_decimal_fill_buffer(char *restrict buf, size_t len, struct rngstate256 *restrict rngstate)
{
buf = __builtin_assume_aligned(buf, 32);
// copy to a local so clang can keep state in register, even in the non-inline version
// restrict works for gcc, but apparently clang still thinks that *buf might alias *rngstate
struct rngstate256 rng_local = *rngstate;
__m256i *restrict p = (__m256i*restrict)buf;
__m256i *restrict endbuf = (__m256i*)(buf+len);
static unsigned newline_pos = 0;
do {
__m256i rvec = xorshift128plus_avx2(&rng_local);
p = vec_store_digit_and_space(rvec, p); // stores multiple ASCII vectors from the entropy in rvec
#if 1
// this is buggy at the end or start of a power-of-2 buffer:
// usually there's a too-short line, sometimes a too-long line
const unsigned ncols = 100;
newline_pos += 4*16;
if (newline_pos >= ncols) {
newline_pos -= ncols;
char *cur_pos = (char*)p;
*(cur_pos - newline_pos*2 - 1) = '\n';
}
#endif
// Turning every 100th space into a newline.
// 1) With an overlapping 1B store to a location selected by a counter. A down-counter would be more efficient
// 2) Or by using a different constant for ascii_digitspace to put a newline in one element
// lcm(200, 16) is 400 bytes, so unrolling the loop enough to produce two full lines makes a pattern of full vectors repeat
// lcm(200, 32) is 800 bytes
// a power-of-2 buffer size doesn't hold a whole number of lines :/
// I'm pretty sure this can be solved with low overhead, like maybe 10% at worst.
} while(p <= endbuf-3);
*rngstate = rng_local;
}
#define BUFFER_SIZE (128 * 1024)
const static size_t bufsz = BUFFER_SIZE;
__attribute__((aligned(64))) static char static_buf[BUFFER_SIZE];
int main(int argc, char *argv[])
{
// TODO: choose a seed properly. (Doesn't affect the speed)
struct rngstate256 xorshift_state = {
_mm256_set_epi64x(123, 456, 0x123, 0x456),
_mm256_set_epi64x(789, 101112, 0x789, 0x101112)
};
for (int i=0; i < 1024ULL*1024*1024 / bufsz * 100; i++) {
random_decimal_fill_buffer(static_buf, bufsz, &xorshift_state);
size_t written = write(1, static_buf, bufsz);
(void)written;
//fprintf(stderr, "wrote %#lx of %#lx\n", written, bufsz);
}
}
Compilez avec gcc, clang ou ICC (ou, espérons-le, avec tout autre compilateur comprenant le dialecte GNU C de C99 et les éléments intrinsèques d’Intel). Les extensions de vecteur GNU C sont très pratiques pour permettre au compilateur de générer les nombres magiques pour division / modulo à l’aide d’inverses multiplicatifs modulaires, et les __attribute__
s occasionnels sont utiles.
Cela pourrait être écrit de manière portable, mais cela prendrait plus de code.
Notes de performance:
Le magasin qui se chevauche pour insérer des nouvelles lignes a un temps système important pour décider où le placer (erreurs de prédiction de succursale et goulots d'étranglement frontaux sur Core2), mais le magasin lui-même n'a aucun impact sur les performances. En commentant uniquement cette instruction de stockage dans l'asm du compilateur (en laissant toutes les branches identiques), les performances sur Core2 sont restées inchangées, les exécutions répétées donnant le même temps à +/- moins de 1%. Je conclus donc que le tampon de mémoire tampon / cache s’en occupe parfaitement.
Néanmoins, utiliser une sorte de fenêtre tournante ascii_digitspace
avec un élément ayant une nouvelle ligne pourrait être encore plus rapide, si nous déroulons suffisamment pour que les compteurs / ramifications disparaissent.
L'écriture dans / dev / null est fondamentalement une opération non-opérée, donc le tampon reste probablement chaud dans le cache L2 (256 ko par cœur sur Haswell). On attend une accélération parfaite des vecteurs 128b à 256b: il n’ya pas d’instructions supplémentaires et tout (y compris les magasins) se passe avec une largeur deux fois plus grande. La branche d'insertion de nouvelle ligne est prise deux fois plus souvent, cependant. Malheureusement, je n'ai pas eu le temps de ma configuration Haswell cygwin avec cette partie #ifdef
.
2,5 GHz * 32B / 13,7 Go / s = 5,84 cycles par magasin AVX2 sur Haswell. C'est très bien, mais pourrait être plus rapide. Peut-être qu'il y a des frais généraux dans les appels système cygwin que je ne le pensais. Je n'ai pas essayé de commenter ceux-ci dans la sortie asm du compilateur (ce qui ferait en sorte que rien ne soit optimisé.)
La mémoire cache N1 peut gérer une mémoire de 32B par horloge et L2 n’est pas beaucoup de bande passante inférieure (latence plus élevée, cependant).
Lorsque j'ai consulté IACA il y a quelques versions (sans ramification pour les nouvelles lignes, mais en obtenant seulement un vecteur ASCII par vecteur RNG), il prédit quelque chose comme un magasin de vecteurs 32B par 4 ou 5 horloges.
J'espérais obtenir plus de rapidité en extrayant plus de données de chaque résultat RNG, en regardant moi-même l'asm, en considérant les guides d'Agner Fog et d'autres ressources d'optimisation pour lesquelles j'ai ajouté des liens dans le wiki du tag SO x86 .)
Il est probable que ce serait beaucoup plus rapide sur Skylake , où le vecteur entier se multiplie et que shift peut s'exécuter sur deux fois plus de ports (p0 / p1) que Haswell (p0 uniquement). xorshift et l'extraction des chiffres utilisent beaucoup de décalages et de multiplications. ( Mise à jour: Skylake l’utilise à 3,02 IPC, nous donnant 3,77 cycles par magasin AVX2 sur 32 octets , chronométrés à 0,030 s par itération de 1 Go, en écrivant /dev/null
sur Linux 4.15 sur i7-6700k à 3,9 GHz.
Il ne nécessite pas le mode 64 bits pour bien fonctionner . La version SSE2 est aussi rapide quand elle est compilée -m32
, car elle n’a pas besoin de beaucoup de registres de vecteurs, et tous les calculs 64 bits sont effectués en vecteurs, et non en registres à usage général.
En réalité, il est légèrement plus rapide en mode 32 bits sur Core2, car la macro-fusion de comparaison / branche ne fonctionne qu'en mode 32 bits, de sorte qu'il y a moins d'ops pour le noyau en panne (18.3s (1.85 Instructions Per Clock) vs 16,9 s (2,0 IPC)). La taille de code plus petite de l'absence de préfixe REX aide également les décodeurs de Core2.
De plus, certains déplacements vectoriels reg-reg sont remplacés par des charges, car toutes les constantes ne sont plus corrigées dans les registres vectoriels. Étant donné que le débit de charge du cache N1 n'est pas un goulot d'étranglement, cela aide en réalité. (par exemple, la multiplication par un vecteur constant de set1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
se transforme en movdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) Comme le MOVDQA régulier nécessite un port ALU, il rivalise avec le travail réel en cours, mais une charge MOVDQA ne concourt que pour la bande passante de décodage frontale. (Le fait d'avoir une adresse de 4 octets dans de nombreuses instructions annule en grande partie l'avantage de la sauvegarde des préfixes REX.
Je ne serais pas surpris si ce sont les gains réels qui proviennent de l’ajout de ALU MOVDQA uops, car l’interface devrait plutôt bien suivre la moyenne de 2,0 IPC.
Toutes ces différences disparaissent sur Haswell, où tout devrait fonctionner à partir du cache décodé-uop, sinon du tampon de bouclage. La macro-fusion de branche ALU + fonctionne dans les deux modes depuis Nehalem.