Se non hai bisogno di casualità di altissima qualità e la distribuzione quasi uniforme è abbastanza buona, puoi andare molto veloce, specialmente su una CPU moderna con efficienti vettori interi SIMD come x86 con SSE2 o AVX2.
Questa è la risposta di @NominalAnimal poiché entrambi abbiamo avuto la stessa idea, ma vettorializzati manualmente per x86. (E con numeri casuali di qualità peggiore, ma probabilmente abbastanza buono per molti casi d'uso.) Funziona circa 15 o 30 volte più veloce del codice di @ Nominal, a ~ 13 GB / s di output ASCII su un Intel Haswell a 2,5 GHz CPU con AVX2. Questo è ancora inferiore alla larghezza di banda massima teorica della memoria principale (DDR3-1600 a doppio canale è circa 25,6 GB / s), ma stavo programmando la scrittura su / dev / null, quindi in realtà è solo riscrittura di un buffer che rimane caldo nella cache. Skylake dovrebbe eseguire lo stesso codice in modo significativamente più veloce di Haswell (vedere il fondo di questa risposta).
Supponendo che in realtà si verifichino colli di bottiglia sull'I / O su disco o esegua il piping da qualche parte, un'implementazione rapida significa che la CPU non deve nemmeno avere un clock superiore al minimo. Utilizza molta meno energia totale per produrre il risultato. (Durata batteria / riscaldamento / riscaldamento globale.)
È così veloce che probabilmente non vuoi scriverlo su disco. Rigenerare appena necessario (dallo stesso seme se si desidera ripetere gli stessi dati). Anche se vuoi alimentarlo a un processo multi-thread che può usare tutte le CPU, eseguirlo per reindirizzare i dati a esso lascerà caldo nella cache L3 (e cache L2 sul core che lo ha scritto), e usa così tanto poco tempo della CPU. (Ma nota che il piping aggiunge un sacco di overhead rispetto alla scrittura /dev/null
. Su uno Sky7ke i7-6700k, il piping su wc -c
o un altro programma che legge + scarta il suo input, è circa 8 volte più lento rispetto alla scrittura/dev/null
e usa solo il 70% di un CPU, ma è ancora 4,0 GB / s su una CPU da 3,9 GHz.
Rigenerarlo è più veloce che rileggerlo anche da un veloce SSD collegato a PCIe, ma IDK se è più efficiente dal punto di vista energetico (il moltiplicatore di vettore intero è abbastanza impegnato e probabilmente ha molta fame, insieme ad altri AVX2 256b ALU vettoriali). OTOH, non so quanto tempo di lettura della CPU dal disco richiederebbe da qualcosa che stava esaurendo tutti i core che elaborano questo input. Immagino che un cambio di contesto per la rigenerazione in blocchi di 128k potrebbe essere competitivo con l'esecuzione di codice filesystem / pagecache e l'allocazione di pagine per leggere i dati dal disco. Ovviamente, se è già caldo nel pagecache, è semplicemente memcpy. OTOH, scriviamo già velocemente come memcpy! (che deve dividere la larghezza di banda della memoria principale tra lettura e scrittura). (Nota anche che scrivere in memoria che 'rep movsb
(memcpy ottimizzato e memset nel microcodice, che evita la RFO, poiché l'implementazione di Andy Glew in P6 (Pentium Pro) )).
Finora questa è solo una prova di concetto e la gestione di newline è solo approssimativamente corretta. È sbagliato attorno alle estremità di un buffer di potenza di 2. Con più tempo di sviluppo. Sono fiducioso di poter trovare un modo più efficiente per inserire nuove righe che sia anche esattamente corretto, con spese generali almeno così basse (rispetto alla produzione di soli spazi). Penso che questo sia tra il 10 e il 20%. Sono solo interessato a sapere quanto velocemente potremmo farcela, non in realtà averne una versione raffinata, quindi lascerò quella parte come esercizio per il lettore, con commenti che descrivono alcune idee.
Su una Haswell i5 al suo turbo massimo di 2,5 GHz, con RAM DDR3-1600 MHz , temporizzata producendo 100 GiB ma ridotta. (Cronometrato su cygwin64 su Win10 con gcc5.4 -O3 -march=native
, omesso -funroll-loops
dal momento che non riuscivo a ottenere tempi decenti su questo laptop preso in prestito. Avrei dovuto avviare Linux su una USB).
scrivendo a / dev / null se non diversamente specificato.
- James Hollis's: (non testato)
- La versione fwrite di Nominal: ~ 2.21s
- questo (SSE2): ~ 0.142s (tempi non scalati = reale = 14.232s, utente = 13.999s, sys = 0.187s).
- questo (AVX-128): ~ 0.140s
- questo (AVX2): ~ 0.073s (non scalato : reale = 0m7.291s, utente = 0m7.125s, sys = 0m0.155s).
- questo (AVX2) piping cygwin a
wc -c
, con dimensioni del buffer di 128 kB: 0,32 s con CPU a 2,38 GHz (turbo dual-core massimo). (tempi non scalati: reale = 32.466s utente = 11.468s sys = 41.092s, inclusi sia questo che wc
). Tuttavia, solo la metà dei dati è stata effettivamente copiata, poiché il mio programma stupido presuppone che write esegua il buffer completo, anche se non è così e cygwin write () esegue solo 64k per chiamata in una pipe.
Quindi con SSE2 questo è circa 15 volte più veloce del codice scalare di @Nominal Animal. Con AVX2, è circa 30 volte più veloce. Non ho provato una versione del codice di Nominal che utilizza solo write()
invece di fwrite()
, ma presumibilmente per buffer di grandi dimensioni stdio rimane per lo più fuori dai piedi. Se sta copiando i dati, ciò comporterebbe molto rallentamento.
Tempi per la produzione di 1 GB di dati su un Core2Duo E6600 (Merom 2.4GHz, L1 privato da 32 kB, cache L2 condivisa da 4MiB), DDR2-533MHz in Linux 4.2 a 64 bit (Ubuntu 15.10). Usando ancora una dimensione del buffer di 128 kB per write (), non ho esplorato quella dimensione.
scrivendo a / dev / null se non diversamente specificato.
- (SSE2) questo con gestione newline e 4 vettori di cifre da ciascun vettore di byte casuali: 0,183s (temporizzato facendo 100GiB in 18,3s, ma risultati simili per esecuzioni da 1GiB). 1,85 istruzioni per ciclo.
- (SSE2) questo, piping a
wc -c
: 0,593s (non scalato: reale = 59.266s user = 20.148s sys = 1m6.548s, incluso il tempo di CPU del wc). Stesso numero di chiamate di sistema write () come con cygwin, ma in realtà esegue il piping di tutti i dati perché Linux gestisce tutti i 128k di un write () su una pipe.
- NominalAnimal's
fwrite()
version (gcc5.2 -O3 -march=native
), eseguito con ./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3.19s +/- 0.1%, con 1.40 istruzioni per ciclo. -funroll-loop ha fatto forse una piccola differenza. clang-3.8 -O3 -march=native
: 3,42s +/- 0,1%
fwrite
Tubazioni nominali a wc -c
: real = 3.980s user = 3.176s sys = 2.080s
- La versione line-at-a-time di James Hollis (
clang++-3.8 -O3 -march=native
): 22,885s +/- 0,07%, con 0,84 istruzioni per ciclo. (g ++ 5.2 era leggermente più lento: 22.98s). Scrivere solo una riga alla volta probabilmente fa molto male.
- Stéphane Chazelas's
tr < /dev/urandom | ...
: real = 41.430s user = 26.832s sys = 40.120s. tr
stava facendo tutto da solo un core della CPU per la maggior parte del tempo, impiegando quasi tutto il suo tempo nel driver del kernel generando byte casuali e copiandoli in una pipe. L'altro core su questa macchina dual core stava eseguendo il resto della pipeline.
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: cioè solo leggendo tanta casualità senza piping: real = 35.018s user = 0.036s sys = 34.940s.
- Programma perl di Lưu Vĩnh Phúc (perl v5.20.2 da 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. Ancora molto lento.
- (SSE2) questo senza gestione di newline , e 3 o 4 vettori di cifre da ciascun vettore di byte casuali (quasi esattamente la stessa velocità: il
dig3 = v%10
passo riguarda il pareggio su questo HW): 0,166s (1,82 istruzioni per ciclo) . Questo è fondamentalmente il limite inferiore per ciò a cui possiamo avvicinarci con una gestione newline perfettamente efficiente.
- (SSE2) Vecchia versione di questo senza gestione newline, ma ottenendo solo una cifra per elemento uint16_t utilizzando
v%10
, 0,222 secondi +/- 0,4%, 2,12 istruzioni per ciclo. (Compilato con gcc5.2,. I -march=native -O3 -funroll-loops
cicli di srotolamento possono aiutare per questo codice su questo hardware. Non utilizzarlo ciecamente, specialmente per programmi di grandi dimensioni).
- (SSE2) Vecchia versione di questo, scrivendo su un file (su un RAID10f2 di 3 dischi rigidi magnetici veloci, non molto ottimizzato per le scritture): ~ 4 secondi. Potrebbe andare più veloce modificando le impostazioni del buffer I / O del kernel per consentire molti più dati sporchi prima dei blocchi write (). Il tempo "Sistema" è ancora ~ 1,0 secondi, molto più lungo del tempo "utente". Su questo vecchio sistema con RAM DDR2-533 lenta, il kernel impiega circa 4 volte più tempo a memorizzare i dati nella pagecache ed eseguire le funzioni XFS rispetto al mio ciclo per continuare a riscriverli sul posto in un buffer che rimane caldo in cache.
Come è fatto
Un PRNG veloce è ovviamente essenziale. xorshift128 + può essere vettorializzato, in modo da avere due o quattro generatori a 64 bit in parallelo, in elementi di un vettore SIMD. Ogni passaggio produce un vettore completo di byte casuali. ( Implementazione AVX2 256b qui con Intel intrinsics ). L'ho scelto per la scelta di xorshift di Nominal *, perché la moltiplicazione di numeri interi a 64 bit è possibile solo in SSE2 / AVX2 con tecniche di precisione estesa .
Dato un vettore di byte casuali, possiamo tagliare ogni elemento a 16 bit in più cifre decimali. Produciamo più vettori di elementi a 16 bit che sono ciascuno una cifra ASCII + spazio ASCII . Lo memorizziamo direttamente nel nostro buffer di output.
La mia versione originale ha usato solo x / 6554
per ottenere una cifra casuale da ogni elemento uint16_t di un vettore. È sempre compreso tra 0 e 9, inclusi. È distorto 9
, perché (2^16 -1 ) / 6554
è solo 9.99923. (6554 = ceil ((2 ^ 16-1) / 10), che assicura che il quoziente sia sempre <10.)
x/6554
può essere calcolato con un moltiplicatore per una costante "magica" ( il reciproco a punto fisso ) e uno spostamento a destra del risultato della metà alta. Questo è il caso migliore per la divisione per una costante; alcuni divisori prendono più operazioni e la divisione firmata richiede un lavoro extra. x % 10
ha una propensione simile e non è così economico da calcolare. (L'output asm di gcc equivale a x - 10*(x/10)
, cioè un moltiplicare e sottrarre in cima alla divisione usando un inverso moltiplicativo modulare.) Inoltre, il bit più basso di xorshift128 + non è così di alta qualità , quindi è meglio dividere l'entropia dai bit alti ( per qualità e velocità) rispetto al modulo per prendere l'entropia dai bit bassi.
Tuttavia, possiamo usare più entropia in ogni uint16_t osservando le cifre decimali basse, come la digit()
funzione di @ Nominal . Per le massime prestazioni, ho deciso di prendere le 3 cifre decimali basse e x/6554
, di salvare un PMULLW e PSUBW (e probabilmente un po 'di MOVDQA) rispetto all'opzione di qualità superiore di prendere le 4 cifre decimali basse. x / 6554 è leggermente influenzato dalle 3 cifre decimali basse, quindi esiste una certa correlazione tra le cifre dello stesso elemento (separazione di 8 o 16 cifre nell'uscita ASCII, a seconda della larghezza del vettore).
Penso che gcc si stia dividendo per 100 e per 1000, piuttosto che una catena più lunga che si divide successivamente per 10, quindi probabilmente non sta accorciando significativamente la lunghezza della catena di dipendenze non a ciclo che produce 4 risultati da ogni uscita PRNG. port0 (moltiplicare e spostare vettore) è il collo di bottiglia a causa delle inversioni moltiplicative modulari e degli spostamenti in xorshift +, quindi è sicuramente utile salvare una moltiplicazione vettoriale.
xorshift + è così veloce che anche usando solo ~ 3,3 bit di casualità ogni 16 (ovvero un'efficienza del 20%) non è molto più lento di tagliarlo in più cifre decimali. Approssimiamo solo la distribuzione uniforme, perché questa risposta è focalizzata sulla velocità purché la qualità non sia troppo male.
Qualsiasi tipo di comportamento condizionale che mantiene un numero variabile di elementi richiederebbe molto più lavoro. (Ma potrebbe ancora essere fatto in qualche modo in modo efficiente utilizzando le tecniche di imballaggio a sinistra SIMD . Tuttavia, questo diventa meno efficiente per elementi di piccole dimensioni; le tabelle di ricerca shuffle-mask giganti non sono vitali e non c'è shuffle di attraversamento di corsia AVX2 con dimensioni inferiori a 32- elementi bit. Una versione PSHUFB a 128b potrebbe ancora essere in grado di generare una maschera al volo con BMI2 PEXT / PDEP, come puoi fare per AVX2 con elementi più grandi , ma è difficile perché un numero intero a 64 bit contiene solo 8 byte. su quella risposta ha un codice che potrebbe funzionare per conteggi di elementi più elevati.)
Se la latenza dell'RNG è un collo di bottiglia, potremmo andare ancora più veloci eseguendo due vettori di generatori in parallelo, alternando quello che usiamo. Il compilatore può ancora facilmente tenere tutto nei registri in un ciclo non srotolato e ciò consente alle due catene di dipendenze di funzionare in parallelo.
Nella versione attuale, tagliando l'output del PRNG, in realtà abbiamo colli di bottiglia sul throughput della porta 0, non sulla latenza del PRNG, quindi non è necessario.
Il codice: versione AVX2
Versione completa con altri commenti sull'esploratore del compilatore Godbolt .
Non molto ordinato, mi dispiace che devo dormire e voglio che questo sia pubblicato.
Per ottenere la versione SSE2, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
, e il cambiamento vector_size(32)
a 16. cambiare anche l'incremento di nuova riga da 4 * 16-4 * 8. (Come ho detto, il codice è disordinato e non ben impostato per la compilazione di due versioni. Inizialmente non avevo intenzione di creare una versione AVX2, ma poi volevo davvero testare su una CPU Haswell a cui avevo accesso.)
#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);
}
}
Compilare con gcc, clang o ICC (o, auspicabilmente, qualsiasi altro compilatore che comprenda il dialetto C GNU C di C99 e le caratteristiche intrinseche di Intel). Le estensioni vettoriali GNU C sono molto utili per far sì che il compilatore generi i numeri magici per divisione / modulo usando inversioni moltiplicative modulari __attribute__
e sono utili occasionali .
Questo potrebbe essere scritto in modo portabile, ma richiederebbe più codice.
Note sulle prestazioni:
Il negozio sovrapposto per l'inserimento di nuove righe ha un notevole sovraccarico per decidere dove posizionarlo (previsioni errate delle filiali e colli di bottiglia del frontend su Core2), ma il negozio stesso non ha alcun impatto sulle prestazioni. Commentare solo le istruzioni del negozio nell'asm del compilatore (lasciando tutte le ramificazioni uguali) ha lasciato le prestazioni su Core2 completamente invariate, con corse ripetute che danno lo stesso tempo a +/- meno dell'1%. Quindi concludo che il buffer / cache del negozio lo gestisce bene.
Tuttavia, l'utilizzo di una sorta di finestra rotante ascii_digitspace
con un elemento con una nuova linea potrebbe essere ancora più veloce, se srotoliamo abbastanza da far scomparire eventuali contatori / ramificazioni.
Scrivere su / dev / null è sostanzialmente un no-op, quindi il buffer probabilmente rimane caldo nella cache L2 (256 kB per core su Haswell). È previsto lo speedup perfetto da vettori 128b a vettori 256b: non ci sono istruzioni aggiuntive e tutto (compresi i negozi) avviene con una larghezza doppia. Tuttavia, il ramo di inserimento della nuova riga viene eseguito due volte più spesso. Sfortunatamente non ho avuto il tempo di installare Cygwin Haswell con quella parte #ifdef
.
2,5 GHz * 32 B / 13,7 GB / s = 5,84 cicli per negozio AVX2 su Haswell. È abbastanza buono, ma potrebbe essere più veloce. Forse c'è qualche sovraccarico nelle chiamate di sistema di Cygwin di quanto pensassi. Non ho provato a commentare quelli nell'output asm del compilatore (il che garantirebbe che nulla fosse ottimizzato).
La cache L1 può supportare un archivio di 32 B per clock e L2 non ha una larghezza di banda molto inferiore (latenza più elevata, tuttavia).
Quando ho guardato IACA alcune versioni fa (senza la diramazione per le nuove linee, ma ottenendo solo un vettore ASCII per vettore RNG), stava predicendo qualcosa come un archivio vettoriale da 32 B per 4 o 5 orologi.
Speravo di ottenere una maggiore accelerazione dall'estrazione di più dati da ogni risultato RNG, basato sull'osservazione di me stesso, considerando le guide di Agner Fog e altre risorse di ottimizzazione per cui ho aggiunto collegamenti nel wiki SO x86 .)
Probabilmente sarebbe significativamente più veloce su Skylake , dove moltiplicare e spostare i numeri interi di vettore può funzionare su un numero di porte doppio (p0 / p1) rispetto a Haswell (solo p0). xorshift e l'estrazione delle cifre utilizzano entrambi molti turni e moltiplicazioni. ( Aggiornamento: Skylake lo esegue a 3.02 IPC, dandoci 3,77 cicli per archivio AVX2 a 32 byte , temporizzato a 0,030 secondi per iterazione da 1 GB, scrivendo /dev/null
su Linux 4.15 su i7-6700k a 3,9 GHz.
Non richiede la modalità a 64 bit per funzionare bene . La versione SSE2 è altrettanto veloce quando compilata -m32
, perché non ha bisogno di molti registri vettoriali e tutta la matematica a 64 bit viene eseguita in vettori, non in registri di uso generale.
In realtà è leggermente più veloce in modalità 32-bit su Core2, perché la macro-fusione comparativa / diramazione funziona solo in modalità 32-bit, quindi ci sono meno uops per il core fuori servizio (18,3 s (1,85 istruzioni per clock) vs 16.9s (2.0 IPC)). La dimensione del codice inferiore rispetto alla mancanza di prefissi REX aiuta anche i decodificatori di Core2.
Inoltre, alcuni movimenti di vettori reg-reg vengono sostituiti con carichi, poiché non tutte le costanti si fissano più nei registri vettori. Poiché il throughput di caricamento dalla cache L1 non è un collo di bottiglia, questo in realtà aiuta. (ad es. moltiplicando per un vettore costante di set1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
trasforma in movdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) Poiché il reg-reg MOVDQA richiede una porta ALU, compete con il lavoro reale svolto, ma un carico MOVDQA compete solo per la larghezza di banda della decodifica front-end. (Avere un indirizzo di 4 byte all'interno di molte istruzioni annulla gran parte del guadagno derivante dal salvataggio dei prefissi REX.
Non sarei sorpreso se il salvataggio di ALU MOVDQA uops è da dove provengono i guadagni reali, dal momento che il frontend dovrebbe tenere il passo con la media di 2.0 IPC abbastanza bene.
Tutte queste differenze scompaiono su Haswell, dove l'intera cosa dovrebbe essere eseguita dalla cache decoded-uop, se non dal buffer di loopback. La macro-fusione ALU + branch funziona in entrambe le modalità da Nehalem.