Tracce
/
x86-64 Assembly
x86-64 Assembly
/
Programma
/
Codice senza rami
Co

Codice senza rami in x86-64 Assembly

1 esercizio

Informazioni su Codice senza rami

Il concetto di condizionali introduce jcc, la famiglia di istruzioni per eseguire salti in base a una condizione. Un jcc trasferisce l'esecuzione a un altro punto del programma solo se la sua condizione è soddisfatta.

Tuttavia, le CPU moderne sono veloci e capaci di eseguire molte istruzioni in parallelo. Sarebbe troppo costoso bloccare tutto il lavoro mentre la CPU aspetta di verificare una condizione a runtime.

Ecco perché, al suo interno, la CPU non aspetta. Usa un predittore di salti per indovinare l'esito di una condizione e inizia a eseguire speculativamente il percorso previsto.

Se la previsione è corretta, l'esecuzione procede come se il salto non ci fosse. Se invece la previsione è sbagliata, la CPU deve scartare il lavoro svolto sul percorso previsto e ricominciare da quello corretto.

Questo fenomeno si chiama previsione errata del salto ed è costoso, perché introduce un ritardo.

Quando un salto è difficile da prevedere, queste previsioni errate si accumulano. In questi casi, a volte è preferibile evitare del tutto il salto, così che vengano eseguite le stesse istruzioni indipendentemente dalla condizione. Il codice che sceglie tra valori senza un jcc si dice senza salti.

Ci sono diversi modi per strutturare il codice in modo che esegua meno salti, o che i salti che esegue siano più prevedibili. In particolare, x86-64 fornisce due famiglie di istruzioni comunemente usate per scrivere codice senza salti.

Spostamenti condizionali

La famiglia di istruzioni cmovcc copia l'operando sorgente nella destinazione solo se una condizione specifica è soddisfatta. Altrimenti, la destinazione resta invariata.

Il suffisso cc segue la stessa nomenclatura di jcc, con lo stesso significato:

istruzione sposta se
cmove A == B dopo cmp A, B
cmovne A != B dopo cmp A, B
cmovl A < B (con segno) dopo cmp A, B
cmovb A < B (senza segno) dopo cmp A, B
cmovg A > B (con segno) dopo cmp A, B
cmova A > B (senza segno) dopo cmp A, B

Le regole sono simili a quelle di jcc:

  1. l e g si usano per i confronti con segno; b e a per quelli senza segno.
  2. Aggiungere e a un suffisso include l'uguaglianza.
  3. Aggiungere n nega la condizione.
  4. Esistono anche varianti che si riferiscono al flag impostato. Per esempio, cmovz e cmovc.

Consideriamo il valore assoluto di un intero con segno in rdi, restituito in rax. Scritta con jcc, la funzione sceglie uno di due percorsi:

abs_branch:
    mov rax, rdi
    cmp rax, 0
    jge .done
    neg rax
.done:
    ret

Scritta con cmovcc, entrambi i valori candidati vengono calcolati incondizionatamente e lo spostamento condizionale ne sceglie uno:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi
    cmp rdi, 0
    cmovge rax, rdi    ; if rdi >= 0, rax = rdi
                       ; otherwise rax stays = -rdi
    ; now rax = abs(rdi)
    ret

La versione senza salti esegue sempre le stesse istruzioni. Non c'è alcun jcc che il predittore possa indovinare.

La destinazione di un cmovcc deve essere un registro. La sorgente può essere un registro o una locazione di memoria, ma non un valore immediato.

Note

Nel codice qui sopra, l'istruzione neg imposta diversi flag in base al risultato, incluso il flag di segno (SF). Questo significa che il cmp rdi, 0 può essere eliminato del tutto:

abs_branchless:
    mov rax, rdi
    neg rax            ; rax = -rdi. `neg` sets SF = 1 if the result is negative
    cmovs rax, rdi     ; if SF = 1 (i.e., rax < 0), replace rax with rdi
                       ; otherwise rax stays = -rdi, which is non-negative
    ; now rax = abs(rdi)
    ret

Per rdi positivo, neg produce un risultato negativo e SF = 1, quindi cmovs ripristina rdi. Per rdi uguale a zero o negativo, neg produce un risultato non negativo e SF = 0, quindi rax mantiene il valore negato (che è il valore assoluto corretto).

Anche se cmp è il modo principale per confrontare valori nel codice assembly, molte istruzioni impostano i flag in base al loro risultato. L'elenco completo dei flag che un'istruzione influisce è di solito riportato nella sua documentazione di riferimento. Per neg in particolare, sono SF, ZF, CF, OF e PF.

Impostazione condizionale

La famiglia di istruzioni setcc imposta l'operando di destinazione a 1 se una condizione specifica è soddisfatta, e a 0 altrimenti. La destinazione è sempre un operando a 8 bit.

Il suffisso cc segue la stessa nomenclatura di jcc e cmovcc. Per esempio, setz imposta la destinazione a 1 se ZF == 1 e a 0 altrimenti.

Dato che la destinazione è a 8 bit, è comune far seguire setcc da un movzx quando serve un valore più ampio:

cmp rdi, rsi
setg al             ; al = 1 if rdi > rsi (signed), 0 otherwise
movzx eax, al       ; eax (and rax) = 1 or 0, with the upper bits cleared

È un modo di scrivere comune per trasformare il risultato di un confronto in un intero 0 o 1.

Quando usare il codice senza salti

Il codice senza salti non è sempre più veloce di quello con salti. Quando un salto è altamente prevedibile, il predittore ha ragione quasi sempre e un jcc non costa praticamente nulla. Il codice senza salti che sostituisce un salto di questo tipo è più lento, perché esegue sempre entrambi i calcoli candidati.

Un contatore di ciclo è l'esempio canonico. Consideriamo una funzione che somma rsi interi a 64 bit a partire da rdi:

sum_array:
    xor rax, rax
.loop:
    add rax, [rdi]
    add rdi, 8
    dec rsi
    jnz .loop          ; taken on every iteration except the last
    ret

Questo è esattamente il tipo di schema che i predittori di salti gestiscono bene, quindi il salto non costa praticamente nulla, indipendentemente da quanti elementi elabora sum_array.

Il codice senza salti conviene quando il salto è difficile da prevedere, o quando serve una temporizzazione costante indipendentemente dagli input.

Advanced

Un riferimento approfondito sulla previsione dei salti per le CPU x86-64 si trova nel terzo capitolo di questo libro di riferimento.

Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Codice senza rami