Come sono programmati gli x86, esattamente?

Le moderne CPU x86 suddividono il stream di istruzioni in entrata in micro-operazioni (uops 1 ) e schedano questi uops out-of-order man mano che i loro input diventano pronti. Sebbene l’idea di base sia chiara, mi piacerebbe conoscere i dettagli specifici di come sono programmate le istruzioni pronte, poiché influisce sulle decisioni di micro-ottimizzazione.

Ad esempio, prendi il seguente anello giocattolo 2 :

top: lea eax, [ecx + 5] popcnt eax, eax add edi, eax dec ecx jnz top 

questo in pratica implementa il ciclo (con la seguente corrispondenza: eax -> total, c -> ecx ):

 do { total += popcnt(c + 5); } while (--c > 0); 

Ho familiarità con il processo di ottimizzazione di ogni piccolo ciclo osservando l’interruzione di uop, le latenze della catena di dipendenza e così via. Nel ciclo precedente abbiamo solo una catena di dipendenze trasportata: dec ecx . Le prime tre istruzioni del ciclo ( lea , imul , add ) fanno parte di una catena di dipendenze che avvia ogni ciclo fresco.

L’ultimo dec e jne sono fusi. Quindi abbiamo un totale di 4 uops con dominio fuso e una sola catena di dipendenze trasportata da loop con una latenza di 1 ciclo. In base a tali criteri, sembra che il ciclo possa essere eseguito a 1 ciclo / iterazione.

Tuttavia, dovremmo guardare anche alla pressione del porto:

  • La lea può essere eseguita sulle porte 1 e 5
  • Il popcnt può essere eseguito sulla porta 1
  • L’ add può essere eseguita sulla porta 0, 1, 5 e 6
  • Il jnz preso in considerazione viene eseguito sulla porta 6

Quindi, per arrivare a 1 ciclo / iterazione, hai praticamente bisogno che succeda quanto segue:

  • Il popcnt deve essere eseguito sulla porta 1 (l’unica porta su cui può essere eseguita)
  • La lea deve essere eseguita sulla porta 5 (e mai sulla porta 1)
  • L’ add deve essere eseguita sulla porta 0 e mai su nessuna delle altre tre porte su cui può essere eseguita
  • jnz può comunque eseguire solo sulla porta 6

Sono molte condizioni! Se le istruzioni sono state pianificate in modo casuale, potresti ottenere un rendimento molto peggiore. Ad esempio, il 75% popcnt andrebbe alla porta 1, 5 o 6, che ritarderebbe il popcnt , lea o jnz di un ciclo. Allo stesso modo per la lea che può andare a 2 porte, una condivisa con popcnt .

D’altra parte, IACA riporta un risultato molto vicino a ottimale, 1,05 cicli per iterazione:

 Intel(R) Architecture Code Analyzer Version - 2.1 Analyzed File - lo Binary Format - 64Bit Architecture - HSW Analysis Type - Throughput Throughput Analysis Report -------------------------- Block Throughput: 1.05 Cycles Throughput Bottleneck: FrontEnd, Port0, Port1, Port5 Port Binding In Cycles Per Iteration: --------------------------------------------------------------------------------------- | Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | --------------------------------------------------------------------------------------- | Cycles | 1.0 0.0 | 1.0 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.0 | 0.9 | 0.0 | --------------------------------------------------------------------------------------- N - port number or number of cycles resource conflict caused delay, DV - Divider pipe (on port 0) D - Data fetch pipe (on ports 2 and 3), CP - on a critical path F - Macro Fusion with the previous instruction occurred * - instruction micro-ops not bound to a port ^ - Micro Fusion happened # - ESP Tracking sync uop was issued @ - SSE instruction followed an AVX256 instruction, dozens of cycles penalty is expected ! - instruction not supported, was not accounted in Analysis | Num Of | Ports pressure in cycles | | | Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | | --------------------------------------------------------------------------------- | 1 | | | | | | 1.0 | | | CP | lea eax, ptr [ecx+0x5] | 1 | | 1.0 | | | | | | | CP | popcnt eax, eax | 1 | 0.1 | | | | | 0.1 | 0.9 | | CP | add edi, eax | 1 | 0.9 | | | | | | 0.1 | | CP | dec ecx | 0F | | | | | | | | | | jnz 0xfffffffffffffff4 

Rispecchia in buona parte la necessaria pianificazione “ideale” che ho menzionato sopra, con una piccola deviazione: mostra l’ add ruba la porta 5 dalla lea su 1 su 10 cicli. Inoltre non sa che il ramo fuso andrà alla porta 6 poiché è previsto, quindi mette la maggior parte degli UOP per il ramo sulla porta 0 e la maggior parte degli UOP per l’ add sulla porta 6, piuttosto rispetto al contrario.

Non è chiaro se gli 0,05 cicli extra riportati da IACA rispetto all’ottimale siano il risultato di un’analisi approfondita e accurata o di una conseguenza meno profonda dell’algoritmo che utilizza, ad esempio, l’analisi del ciclo su un numero fisso di cicli o solo un bug o qualsiasi altra cosa Lo stesso vale per la frazione 0.1 di un uop che pensa andrà alla porta non ideale. Inoltre non è chiaro se uno spiega l’altro – penserei che assegnare in modo errato una porta 1 su 10 causerebbe un conteggio del ciclo di 11/10 = 1,1 cicli per iterazione, ma non ho elaborato l’attuale downstream risultati – forse l’impatto è inferiore in media. O potrebbe semplicemente arrotondare (da 0,05 == 0,1 a 1 decimale).

Quindi, come programmano le moderne CPU x86? In particolare:

  1. Quando più uops sono pronti nella stazione di prenotazione, in che ordine sono programmati per le porte?
  2. Quando un uop può andare su più porte (come add e lea nell’esempio sopra), come viene deciso quale porta viene scelta?
  3. Se una delle risposte coinvolge un concetto come il più vecchio tra cui scegliere, come viene definito? Età da quando è stata consegnata alla RS? Età da quando è stato pronto? Come si rompono i legami? L’ordine del programma è mai entrato in esso?

Risultati su Skylake

Misuriamo alcuni risultati effettivi su Skylake per verificare quali risposte spiegano le prove sperimentali, quindi ecco alcuni risultati misurati nel mondo reale (da perf ) sulla mia scatola Skylake. Confusamente, sto passando ad usare imul per la mia istruzione “only imul on one port”, dato che ha molte varianti, incluse le versioni a 3 argomenti che ti permettono di usare registri diversi per la (e) sorgente (i) e la destinazione. Questo è molto utile quando si tenta di build catene di dipendenze. Evita anche l’intera “dipendenza non corretta dalla destinazione” che popcnt ha.

Istruzioni indipendenti

Iniziamo osservando il semplice caso (?) Che le istruzioni sono relativamente indipendenti – senza catene di dipendenza diverse da quelle banali come il contatore di loop.

Ecco un loop di 4 uop (solo 3 uops eseguiti) con una leggera pressione. Tutte le istruzioni sono indipendenti (non condividere fonti o destinazioni). L’ add potrebbe in linea di principio rubare il p1 necessario per l’ imul o p6 necessario per il dec:

Esempio 1

 instr p0 p1 p5 p6 xor (elim) imul X add XXXX dec X top: xor r9, r9 add r8, rdx imul rax, rbx, 5 dec esi jnz top The results is that this executes with perfect scheduling at 1.00 cycles / iteration: 560,709,974 uops_dispatched_port_port_0 ( +- 0.38% ) 1,000,026,608 uops_dispatched_port_port_1 ( +- 0.00% ) 439,324,609 uops_dispatched_port_port_5 ( +- 0.49% ) 1,000,041,224 uops_dispatched_port_port_6 ( +- 0.00% ) 5,000,000,110 instructions:u # 5.00 insns per cycle ( +- 0.00% ) 1,000,281,902 cycles:u ( +- 0.00% ) 

Come atteso, p1 e p6 sono completamente utilizzati da imul e dec/jnz rispettivamente, e quindi l’ add circa la metà e la metà tra le restanti porte disponibili. Notare approssimativamente – il rapporto effettivo è 56% e 44%, e questo rapporto è abbastanza stabile su tutte le esecuzioni (notare la variazione +- 0.49% ). Se aggiusto l’allineamento del loop, i cambi di divisione (53/46 per l’allineamento 32B, più come 57/42 per l’allineamento 32B + 4). Ora, se non cambiamo nulla tranne la posizione di imul nel ciclo:

Esempio 2

 top: imul rax, rbx, 5 xor r9, r9 add r8, rdx dec esi jnz top 

Quindi improvvisamente la divisione p1 / p5 è esattamente del 50% / 50%, con una variazione dello 0,00%:

  500,025,758 uops_dispatched_port_port_0 ( +- 0.00% ) 1,000,044,901 uops_dispatched_port_port_1 ( +- 0.00% ) 500,038,070 uops_dispatched_port_port_5 ( +- 0.00% ) 1,000,066,733 uops_dispatched_port_port_6 ( +- 0.00% ) 5,000,000,439 instructions:u # 5.00 insns per cycle ( +- 0.00% ) 1,000,439,396 cycles:u ( +- 0.01% ) 

Quindi è già interessante, ma è difficile dire cosa sta succedendo. Forse il comportamento esatto dipende dalle condizioni iniziali all’entrata del ciclo ed è sensibile all’ordinamento all’interno del ciclo (ad es. Perché i contatori sono usati). Questo esempio mostra che qualcosa di più della programmazione “casuale” o “stupida” sta succedendo. In particolare, se si elimina l’istruzione imul dal ciclo, si ottiene quanto segue:

Esempio 3

  330,214,329 uops_dispatched_port_port_0 ( +- 0.40% ) 314,012,342 uops_dispatched_port_port_1 ( +- 1.77% ) 355,817,739 uops_dispatched_port_port_5 ( +- 1.21% ) 1,000,034,653 uops_dispatched_port_port_6 ( +- 0.00% ) 4,000,000,160 instructions:u # 4.00 insns per cycle ( +- 0.00% ) 1,000,235,522 cycles:u ( +- 0.00% ) 

Qui, l’ add è ora equamente distribuito tra p0 , p1 e p5 – quindi la presenza di imul ha influito sulla pianificazione imul : non era solo una conseguenza di qualche regola “evita la porta 1”.

Si noti qui che la pressione totale della porta è solo di 3 uop / ciclo, poiché l’ xor è un idioma di azzeramento ed è eliminato nel rinominatore. Proviamo con la massima pressione di 4 uop. Mi aspetto che il meccanismo sopra descritto sia in grado di pianificare perfettamente anche questo. Cambiamo solo xor r9, r9 a xor r9, r10 , quindi non è più un idioma di azzeramento. Otteniamo i seguenti risultati:

Esempio 4

 top: xor r9, r10 add r8, rdx imul rax, rbx, 5 dec esi jnz top 488,245,238 uops_dispatched_port_port_0 ( +- 0.50% ) 1,241,118,197 uops_dispatched_port_port_1 ( +- 0.03% ) 1,027,345,180 uops_dispatched_port_port_5 ( +- 0.28% ) 1,243,743,312 uops_dispatched_port_port_6 ( +- 0.04% ) 5,000,000,711 instructions:u # 2.66 insns per cycle ( +- 0.00% ) 1,880,606,080 cycles:u ( +- 0.08% ) 

Oops! Piuttosto che pianificare in modo uniforms tutto su p0156 , lo scheduler ha sottoutilizzato p0 (esegue solo qualcosa ~ 49% dei cicli), quindi p1 e p6 sono sovrascritti perché stanno eseguendo entrambe le operazioni necessarie di imul e dec/jnz . Questo comportamento, penso sia coerente con un indicatore di pressione basato sul contatore come hayesti indicato nella risposta, e con gli uops assegnati a un porto in tempo di rilascio, non al momento dell’esecuzione, come menzionato sia da Hayesti che da Peter Cordes. Quel comportamento 3 rende l’ esecuzione la più vecchia regola ready-up non altrettanto efficace. Se gli UOP non erano vincolati alle porte di esecuzione in questione, ma piuttosto all’esecuzione, allora questa “più vecchia” regola imul il problema sopra dopo una singola iterazione – una volta che uno imul e uno dec/jnz stati trattenuti per una singola iterazione, essi sempre essere più vecchio rispetto allo xor competizione e add istruzioni, quindi devi sempre essere programmato per primo. Uno che sto imparando, però, è che se le porte vengono assegnate al momento del rilascio, questa regola non aiuta perché le porte sono predeterminate al momento del problema. Immagino che aiuti ancora un po ‘a favorire le istruzioni che fanno parte di lunghe catene di dipendenza (poiché queste tenderanno a rimanere indietro), ma non è la panacea che pensavo fosse.

Ciò sembra anche spiegare i risultati sopra riportati: a p0 viene assegnata una pressione maggiore di quella che ha realmente perché la combinazione dec/jnz può in teoria essere eseguita su p06 . Infatti, poiché il ramo è previsto, si passa sempre solo a p6 , ma forse quell’informazione non può alimentare l’algoritmo di bilanciamento della pressione, quindi i contatori tendono a vedere uguale pressione su p016 , il che significa che l’ add e l’ xor si diffondono intorno in modo diverso da quello ottimale.

Probabilmente possiamo testarlo, srotolando un po ‘il ciclo in modo che il jnz sia meno di un fattore …


1 OK, è scritto correttamente μops , ma che uccide la capacità di ricerca e in realtà digita il carattere “μ” di solito sto ricorrendo al copia-incolla del personaggio da una pagina web.

2 In origine avevo usato imul invece di popcnt nel ciclo, ma, incredibilmente, IACA non lo supporta !

3 Si noti che non sto suggerendo che questo sia un design scarso o altro – ci sono probabilmente ottime ragioni hardware per cui lo scheduler non può facilmente prendere tutte le sue decisioni al momento dell’esecuzione.

Le tue domande sono difficili per un paio di motivi:

  1. La risposta dipende molto dalla microarchitettura del processore, che può variare significativamente da una generazione all’altra.
  2. Si tratta di dettagli a grana fine che Intel non rilascia generalmente al pubblico.

Tuttavia, proverò a rispondere …

Quando più uops sono pronti nella stazione di prenotazione, in che ordine sono programmati per le porte?

Dovrebbe essere il più vecchio [vedi sotto], ma il tuo chilometraggio può variare. La microarchitettura P6 (utilizzata in Pentium Pro, 2 e 3) utilizzava una stazione di prenotazione con cinque scheduler (uno per porta di esecuzione); gli scheduler utilizzavano un puntatore di priorità come punto di partenza per la scansione di copie da inviare. Era solo FIFO pseudo, quindi è del tutto ansible che l’istruzione più vecchia non fosse sempre programmata. Nella microarchitettura NetBurst (usata in Pentium 4), hanno abbandonato la stazione di prenotazione unificata e hanno usato invece due code uop. Si trattava di code di priorità che si chiudevano correttamente, quindi gli scheduler erano garantiti per ottenere le istruzioni più vecchie pronte. L’architettura Core è tornata a una stazione di prenotazione e azzarderei un’ipotesi plausibile sul fatto che abbiano usato la coda di priorità collassante, ma non riesco a trovare una fonte per confermarlo. Se qualcuno ha una risposta definitiva, sono tutto orecchie.

Quando un uop può andare su più porte (come add e lea nell’esempio sopra), come viene deciso quale porta viene scelta?

È difficile da sapere. Il meglio che ho potuto trovare è un brevetto di Intel che descrive un meccanismo del genere. Essenzialmente, mantengono un contatore per ogni porta che ha unità funzionali ridondanti. Quando gli uops lasciano il front-end alla stazione di prenotazione, gli viene assegnata una porta di spedizione. Se deve decidere tra più unità di esecuzione ridondanti, i contatori vengono utilizzati per distribuire uniformsmente il lavoro. I contatori vengono incrementati e decrementati quando gli utenti entrano e escono rispettivamente dalla stazione di prenotazione.

Naturalmente questo è solo un euristico e non garantisce un perfetto programma senza conflitti, tuttavia, potrei comunque vederlo funzionare con il tuo esempio di giocattolo. Le istruzioni che possono solo andare su una porta influenzerebbero in ultima analisi lo schedulatore per inviare gli Uop “meno ristretti” ad altre porte.

In ogni caso, la presenza di un brevetto non implica necessariamente che l’idea sia stata adottata (anche se detto questo, uno degli autori era anche un lead tecnologico del Pentium 4, quindi chi lo sa?)

Se una delle risposte coinvolge un concetto come il più vecchio tra cui scegliere, come viene definito? Età da quando è stata consegnata alla RS? Età da quando è stato pronto? Come si rompono i legami? L’ordine del programma è mai entrato in esso?

Poiché gli UOP vengono inseriti nella stazione di prenotazione in ordine, il più vecchio qui fa effettivamente riferimento al tempo in cui è entrato nella stazione di prenotazione, cioè più vecchio nell’ordine del programma.

A proposito, prenderei quei risultati IACA con un pizzico di sale in quanto potrebbero non riflettere le sfumature dell’hardware reale. Su Haswell, c’è un contatore hardware chiamato uops_executed_port che può dirti quanti cicli nel tuo thread hanno problemi di UOP alle porte 0-7. Forse potresti sfruttare questi per ottenere una migliore comprensione del tuo programma?

Ecco cosa ho trovato su Skylake, arrivando dall’angolo in cui gli UOP sono assegnati alle porte in fase di rilascio (cioè, quando vengono inviati alla RS), non al momento della spedizione (cioè, al momento in cui vengono inviati per l’esecuzione) . Prima ho capito che la decisione port è stata presa al momento della spedizione.

Ho fatto una serie di test che hanno cercato di isolare sequenze di operazioni di add che possono andare a p0156 e operazioni imul che vanno solo alla porta 0. Un tipico test va in questo modo:

 mov eax, [edi] mov eax, [edi] mov eax, [edi] mov eax, [edi] ... many more mov instructions mov eax, [edi] mov eax, [edi] mov eax, [edi] mov eax, [edi] imul ebx, ebx, 1 imul ebx, ebx, 1 imul ebx, ebx, 1 imul ebx, ebx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 mov eax, [edi] mov eax, [edi] mov eax, [edi] mov eax, [edi] ... many more mov instructions mov eax, [edi] mov eax, [edi] mov eax, [edi] mov eax, [edi] 

Fondamentalmente c’è una lunga mov eax, [edi] istruzioni mov eax, [edi] , che si verificano solo su p23 e quindi non intasano le porte usate dalle istruzioni (avrei potuto anche usare le istruzioni nop , ma il test sarebbe stato un un po ‘diverso dal momento che nop non rilascia la RS). Questa è seguita dalla sezione “payload”, qui composta da 4 imul e 12 add , e quindi una sezione di lead-out di più istruzioni fittizie.

Per prima cosa, diamo un’occhiata al brevetto che hayesti collegato sopra e che descrive l’idea di base su: contatori per ogni porta che tracciano il numero totale di uops assegnati alla porta, che sono usati per bilanciare il carico delle assegnazioni delle porte. Dai un’occhiata a questa tabella inclusa nella descrizione del brevetto:

inserisci la descrizione dell'immagine qui

Questa tabella è usata per scegliere tra p0 o p1 per i 3-uops in un gruppo di problemi per l’architettura a 3-larghe discussa nel brevetto. Si noti che il comportamento dipende dalla posizione di UOP nel gruppo e che ci sono 4 regole 1 basate sul conteggio, che distribuiscono gli stessi intorno in modo logico. In particolare, il conteggio deve essere di +/- 2 o maggiore prima che l’intero gruppo venga assegnato alla porta sottoutilizzata.

Vediamo se possiamo osservare la “posizione nel gruppo di emissione” che riguarda il comportamento su Sklake. Usiamo un carico utile di un singolo add come:

 add edx, 1 ; position 0 mov eax, [edi] mov eax, [edi] mov eax, [edi] 

… e lo facciamo scorrere all’interno del mandrino a 4 istruzioni come:

 mov eax, [edi] add edx, 1 ; position 1 mov eax, [edi] mov eax, [edi] 

… e così via, testando tutte e quattro le posizioni all’interno del gruppo di emissione 2 . Questo mostra quanto segue, quando la RS è piena (di istruzioni mov ) ma senza pressione di porta di nessuna delle porte rilevanti:

  • Le prime istruzioni di add vanno a p5 o p6 , con la porta selezionata di solito in alternanza mentre l’istruzione è rallentata (ad esempio, add istruzioni in posizioni pari andare a p5 e in posizioni dispari andare a p6 ).
  • Anche la seconda istruzione add va a p56 a p56 quale dei due il primo non sia andato.
  • Dopo di ciò, ulteriori istruzioni di add iniziano ad essere bilanciate attorno a p0156 , con p5 e p6 generalmente avanti ma con le cose abbastanza uniformsmente complessive (cioè, il divario tra p56 e le altre due porte non cresce).

Successivamente, ho dato un’occhiata a cosa succede se caricate p1 con operazioni imul , quindi prima in un gruppo di operazioni di add :

 imul ebx, ebx, 1 imul ebx, ebx, 1 imul ebx, ebx, 1 imul ebx, ebx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 add r9, 1 add r8, 1 add ecx, 1 add edx, 1 

I risultati mostrano che lo scheduler lo gestisce bene – tutto l’ imul è programmato su p1 (come previsto), e quindi nessuna delle successive istruzioni di add passata a p1 , essendo invece distribuita intorno a p056 . Quindi qui la programmazione funziona bene.

Naturalmente, quando la situazione è invertita, e la serie di imul arriva dopo l’ add s, p1 viene caricata con la sua quota di add prima che l’hit di imul s. Questo è il risultato dell’assegnazione delle porte in corso in ordine al momento della pubblicazione, poiché non esiste un meccanismo per “guardare avanti” e vedere l’ imul quando si pianificano le add .

Nel complesso, lo scheduler sembra fare un buon lavoro in questi casi di test.

Non spiega cosa succede nei circuiti più piccoli e più stretti come il seguente:

 sub r9, 1 sub r10, 1 imul ebx, edx, 1 dec ecx jnz top 

Proprio come l’ Esempio 4 nella mia domanda, questo ciclo riempie solo p0 su ~ 30% dei cicli, nonostante ci siano due istruzioni sub che dovrebbero essere in grado di andare a p0 ad ogni ciclo. p1 e p6 sono sovrascritti, ognuno dei quali esegue 1,24 UOP per ogni iterazione (1 è l’ideale). Non ero in grado di triangular la differenza tra gli esempi che funzionano bene in cima a questa risposta con i cattivi cicli – ma ci sono ancora molte idee da provare.

Ho notato che gli esempi senza differenze di latenza delle istruzioni non sembrano soffrire di questo problema. Ad esempio, ecco un altro ciclo di 4-uop con pressione di porta “complessa”:

 top: sub r8, 1 ror r11, 2 bswap eax dec ecx jnz top 

La mappa di uop è la seguente:

 instr p0 p1 p5 p6 sub XXXX ror XX bswap XX dec/jnz X 

Quindi il sub deve sempre andare a p15 , condiviso con bswap se le cose devono funzionare. Loro fanno:

Statistiche contatore prestazioni per ‘./sched-test2’ (2 esecuzioni):

  999,709,142 uops_dispatched_port_port_0 ( +- 0.00% ) 999,675,324 uops_dispatched_port_port_1 ( +- 0.00% ) 999,772,564 uops_dispatched_port_port_5 ( +- 0.00% ) 1,000,991,020 uops_dispatched_port_port_6 ( +- 0.00% ) 4,000,238,468 uops_issued_any ( +- 0.00% ) 5,000,000,117 instructions:u # 4.99 insns per cycle ( +- 0.00% ) 1,001,268,722 cycles:u ( +- 0.00% ) 

Quindi sembra che il problema possa essere correlato alle latenze delle istruzioni (certamente, ci sono altre differenze tra gli esempi). Questo è qualcosa che è emerso in questa domanda simile .


1 La tabella ha 5 regole, ma la regola per i conteggi 0 e -1 è identica.

2 Naturalmente, non posso essere sicuro di dove iniziano e si concludono i gruppi di problemi, ma a prescindere testiamo quattro diverse posizioni mentre scendiamo quattro istruzioni (ma le etichette potrebbero essere sbagliate). Inoltre, non sono sicuro che la dimensione massima del gruppo di problemi sia 4 – le parti precedenti della pipeline sono più ampie – ma credo che lo siano e alcuni test sembravano mostrare (loop con un multiplo di 4 uop hanno mostrato un comportamento di scheduling coerente). In ogni caso, le conclusioni valgono con diverse dimensioni del gruppo di pianificazione.