Perché la rottura della “dipendenza di uscita” di LZCNT è importante?

Durante il benchmark ho misurato un throughput molto più basso di quello che avevo calcolato, che ho ristretto all’istruzione LZCNT (succede anche con TZCNT), come dimostrato nei seguenti benchmark:

xor ecx, ecx _benchloop: lzcnt eax, edx add ecx, 1 jnz _benchloop 

E:

  xor ecx, ecx _benchloop: xor eax, eax ; this shouldn't help, but it does lzcnt eax, edx add ecx, 1 jnz _benchloop 

La seconda versione è molto più veloce. Non dovrebbe essere. Non c’è motivo per cui LZCNT debba avere una dipendenza di input dal suo output. A differenza di BSR / BSF, le istruzioni xZCNT sovrascrivono sempre il loro output.

Sto eseguendo questo su un 4770K, quindi LZCNT e TZCNT non vengono eseguiti come BSR / BSF.

Cosa sta succedendo qui?

Questa è semplicemente una limitazione della microarchitettura della CPU Intel Haswell e di alcune precedenti 1 CPU. È stato corretto per tzcnt e lzcnt partire da Skylake, ma il problema rimane per popcnt .

Su quelle micro-architetture l’operando di destinazione per tzcnt , lzcnt e popcnt viene trattato come una dipendenza di input anche se, semanticamente, non lo è. Ora dubito che questo sia davvero un “bug”: se fosse semplicemente una svista, mi aspetto che sarebbe stato corretto in una delle tante nuove micro-architetture che sono state rilasciate da quando è stata introdotta.

È più probabile che si tratti di un compromesso di progettazione basato su uno o entrambi i seguenti due fattori:

  • L’hardware per popcnt , lzcnt e tzcnt è probabilmente condiviso con le istruzioni bsf e bsr esistenti. Ora bsf e bsr avevano una dipendenza dal precedente valore di destinazione nella pratica 2 per il caso speciale di input tutto-bit-zero, poiché i chip Intel lasciavano la destinazione non modificata in quel caso. Quindi è del tutto ansible che il progetto più semplice per l’hardware combinato abbia come risultato altre istruzioni simili eseguite sulla stessa unità che ereditano la stessa dipendenza.

  • La maggior parte delle istruzioni ALU a due operandi x86 ha una dipendenza dall’operando di destinazione, poiché viene utilizzata anche come sorgente. Le tre istruzioni interessate sono in qualche modo univoche in quanto sono operatori unari , ma a differenza degli operatori unari esistenti come not e neg che hanno un singolo operando utilizzato come sorgente e destinazione, hanno distinti operandi di origine e destinazione, rendendoli superficialmente simili alla maggior parte istruzioni di input. Forse la circuiteria di renamer / scheduler semplicemente non distingue il caso speciale di questi unando-con-due-registro-operando contro la stragrande maggioranza delle semplici istruzioni di input / destinazione a 2 ingressi / destinazioni che non hanno questa dipendenza.

In effetti, per il caso di popcnt Intel ha emesso vari errori che coprono il falso problema di dipendenza come HSD146 per Haswell Desktop e SKL029 per Skylake , che recita:

L’istruzione POPCNT può richiedere più tempo dell’esecuzione prevista

Problema L’esecuzione dell’istruzione POPCNT con un operando a 32 o 64 bit può essere ritardata fino a quando non sono state eseguite istruzioni non dipendenti precedenti.

Il software di implicazione che utilizza l’istruzione POPCNT può presentare prestazioni inferiori a quelle previste.

Soluzione alternativa Nessuna identificata

Ho sempre trovato questo errato insolito poiché non identifica in realtà alcun tipo di difetto funzionale o di non conformità alle specifiche, che è il caso essenzialmente di tutti gli altri errori. Intel non documenta realmente uno specifico modello di prestazioni per il motore di esecuzione di OoO e ci sono un sacco di altre “trucchi” di performance che sono apparse e scomparse nel corso degli anni (molte con un impatto molto più grande che questo problema molto minore) che don ‘ t vengono documentati in errata. Tuttavia, questo forse fornisce alcune prove che può essere considerato un bug. Stranamente, l’erratum non fu mai esteso per includere tzcnt o lzcnt che avevano lo stesso problema quando furono introdotti.


1 Well tzcnt e lzcnt sono apparsi solo in Haswell, ma il problema esiste anche per popcnt che è stato introdotto in Nehalem – ma il problema della falsa dipendenza forse esiste solo per Sandy Bridge o popcnt successive.

2 In pratica , sebbene non documentato nei documenti ISA, dal momento che il risultato per l’input tutto-zero era indefinito nei manuali Intel. La maggior parte o tutti i chip Intel hanno implementato il comportamento lasciando invariato il registro di destinazione in questo caso.

Sulla falsariga di quanto suggerito da @BrettHale, è ansible (se dispari) che si stia verificando un errore di aggiornamento parziale delle bandiere del caso d’angolo. In teoria, lo stato di flag dovrebbe semplicemente essere rinominato perché la seguente aggiunta aggiorna tutti i flag, ma se non lo fosse per qualche ragione, introdurrebbe una dipendenza trasportata dal ciclo e l’inserimento di xor interromperà tale dipendenza.

È difficile sapere con certezza se questo è ciò che sta accadendo, ma sembra essere la spiegazione più probabile da un’occhiata casuale; è ansible verificare l’ipotesi sostituendo l’ xor con il test (che interrompe anche la dipendenza dei flag ma non ha alcun effetto sulle dipendenze dei registri).