Esiste uno snippet C che calcola l'aggiunta sicura da tracimazione in modo efficiente senza utilizzare i builtin del compilatore?


11

Ecco una funzione C che aggiunge una inta un'altra, non riuscendo in caso di overflow:

int safe_add(int *value, int delta) {
        if (*value >= 0) {
                if (delta > INT_MAX - *value) {
                        return -1;
                }
        } else {
                if (delta < INT_MIN - *value) {
                        return -1;
                }
        }

        *value += delta;
        return 0;
}

Purtroppo non è ottimizzato bene da GCC o Clang:

safe_add(int*, int):
        movl    (%rdi), %eax
        testl   %eax, %eax
        js      .L2
        movl    $2147483647, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jl      .L6
.L4:
        addl    %esi, %eax
        movl    %eax, (%rdi)
        xorl    %eax, %eax
        ret
.L2:
        movl    $-2147483648, %edx
        subl    %eax, %edx
        cmpl    %esi, %edx
        jle     .L4
.L6:
        movl    $-1, %eax
        ret

Questa versione con __builtin_add_overflow()

int safe_add(int *value, int delta) {
        int result;
        if (__builtin_add_overflow(*value, delta, &result)) {
                return -1;
        } else {
                *value = result;
                return 0;
        }
}

è ottimizzato meglio :

safe_add(int*, int):
        xorl    %eax, %eax
        addl    (%rdi), %esi
        seto    %al
        jo      .L5
        movl    %esi, (%rdi)
        ret
.L5:
        movl    $-1, %eax
        ret

ma sono curioso di sapere se esiste un modo senza usare i builtin che verranno adattati da GCC o Clang.


1
Vedo che c'è gcc.gnu.org/bugzilla/show_bug.cgi?id=48580 nel contesto della moltiplicazione. Ma l'aggiunta dovrebbe essere molto più facile da abbinare al modello. Lo segnalerò.
Tavian Barnes,

Risposte:


6

Il migliore che mi è venuto in mente, se non si ha accesso alla bandiera di overflow dell'architettura, è fare le cose unsigned. Basti pensare a tutta l'aritmetica dei bit qui in quanto siamo interessati solo al bit più alto, che è il bit di segno quando interpretato come valori con segno.

(Tutti quegli errori di segno del modulo, non l'ho verificato minuziosamente, ma spero che l'idea sia chiara)

#include <stdbool.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;

  if (!ret) a[0] += b;
  return ret;
}

Se trovi una versione dell'aggiunta che è libera da UB, come una atomica, l'assemblatore è anche senza ramo (ma con un prefisso di blocco)

#include <stdbool.h>
#include <stdatomic.h>
bool overadd(_Atomic(int) a[static 1], int b) {
  unsigned A = a[0];
  atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  bool ret = (AeB & AuAB) > M;
  return ret;
}

Quindi, se avessimo avuto un'operazione del genere, ma ancora più "rilassata", ciò potrebbe migliorare ulteriormente la situazione.

Take3: se utilizziamo un "cast" speciale dal risultato non firmato a quello firmato, questo ora è privo di diramazioni:

#include <stdbool.h>
#include <stdatomic.h>

bool overadd(int a[static 1], int b) {
  unsigned A = a[0];
  //atomic_fetch_add_explicit(a, b, memory_order_relaxed);
  unsigned B = b;
  // This computation will be done anyhow
  unsigned AB = A + B;
  // See if the sign bits are equal
  unsigned AeB = ~(A^B);
  unsigned AuAB = (A^AB);
  // The function result according to these should be:
  //
  // AeB \ AuAB | false | true
  //------------+-------+------
  // false      | false | false
  // true       | false | true
  //
  // So the expression to compute from the sign bits is (AeB & AuAB)

  // This is INT_MAX
  unsigned M = -1U/2;
  unsigned res = (AeB & AuAB);
  signed N = M-1;
  N = -N - 1;
  a[0] =  ((AB > M) ? -(int)(-AB) : ((AB != M) ? (int)AB : N));
  return res > M;
}

2
Non il DV, ma credo che il secondo XOR non debba essere negato. Vedi ad esempio questo tentativo di testare tutte le proposte.
Bob__

Ho provato qualcosa del genere ma non sono riuscito a farlo funzionare. Sembra promettente ma vorrei che GCC ottimizzasse il codice idiomatico.
R .. GitHub smette di aiutare ICE il

1
@PSkocik, no, questo non dipende dalla rappresentazione del segno, il calcolo è interamente fatto come unsigned. Ma dipende dal fatto che il tipo senza segno non ha solo il bit di segno mascherato. (Entrambi sono ora garantiti in C2x, vale a dire, vale per tutti gli archi che potremmo trovare). Quindi, non è possibile unsignedrestituire il risultato se è maggiore di INT_MAX, che sarebbe definito dall'implementazione e potrebbe generare un segnale.
Jens Gustedt,

1
@PSkocik, no, sfortunatamente no, che sembrava rivoluzionare il comitato. Ma qui c'è un "Take3" che effettivamente esce senza rami sulla mia macchina.
Jens Gustedt,

1
Scusa se ti disturbo ancora una volta, ma penso che si dovrebbe cambiare Take3 in qualcosa come questo per ottenere risultati corretti. Sembra promettente , però.
Bob__

2

La situazione con le operazioni firmate è molto peggiore rispetto a quelle non firmate e vedo solo un modello per l'aggiunta firmata, solo per clang e solo quando è disponibile un tipo più ampio:

int safe_add(int *value, int delta)
{
    long long result = (long long)*value + delta;

    if (result > INT_MAX || result < INT_MIN) {
        return -1;
    } else {
        *value = result;
        return 0;
    }
}

clang dà esattamente lo stesso asm di __builtin_add_overflow:

safe_add:                               # @safe_add
        addl    (%rdi), %esi
        movl    $-1, %eax
        jo      .LBB1_2
        movl    %esi, (%rdi)
        xorl    %eax, %eax
.LBB1_2:
        retq

Altrimenti, la soluzione più semplice che mi viene in mente è questa (con l'interfaccia utilizzata da Jens):

_Bool overadd(int a[static 1], int b)
{
    // compute the unsigned sum
    unsigned u = (unsigned)a[0] + b;

    // convert it to signed
    int sum = u <= -1u / 2 ? (int)u : -1 - (int)(-1 - u);

    // see if it overflowed or not
    _Bool overflowed = (b > 0) != (sum > a[0]);

    // return the results
    a[0] = sum;
    return overflowed;
}

gcc e clang generano asm molto simili . gcc dà questo:

overadd:
        movl    (%rdi), %ecx
        testl   %esi, %esi
        setg    %al
        leal    (%rcx,%rsi), %edx
        cmpl    %edx, %ecx
        movl    %edx, (%rdi)
        setl    %dl
        xorl    %edx, %eax
        ret

Vogliamo calcolare la somma in unsigned, quindi unsigneddobbiamo essere in grado di rappresentare tutti i valori intsenza che nessuno di loro si attacchino. Per convertire facilmente il risultato da unsignedin int, è utile anche il contrario. Nel complesso, si assume il complemento a due.

Su tutte le piattaforme popolari penso che possiamo convertirci da unsignedin intun semplice compito come int sum = u;, ma, come menzionato da Jens, anche l'ultima variante dello standard C2x gli consente di aumentare il segnale. Il prossimo modo più naturale è fare qualcosa del genere: *(unsigned *)&sum = u;ma le varianti non trap di imbottitura apparentemente potrebbero differire per tipi firmati e non firmati. Quindi l'esempio sopra va nel modo più duro. Fortunatamente, sia gcc che clang ottimizzano questa difficile conversione.

PS Le due varianti precedenti non possono essere confrontate direttamente in quanto hanno un comportamento diverso. Il primo segue la domanda originale e non ostruisce il *valuecaso di overflow. Il secondo segue la risposta di Jens e blocca sempre la variabile indicata dal primo parametro ma è priva di diramazioni.


Potresti mostrare l'asm generato?
R .. GitHub smette di aiutare ICE il

Sostituita l'uguaglianza con xor nel controllo di overflow per migliorare asm con gcc. Aggiunto asm.
Alexander Cherepanov,

1

la migliore versione che posso trovare è:

int safe_add(int *value, int delta) {
    long long t = *value + (long long)delta;
    if (t != ((int)t))
        return -1;
    *value = (int) t;
    return 0;
}

che produce:

safe_add(int*, int):
    movslq  %esi, %rax
    movslq  (%rdi), %rsi
    addq    %rax, %rsi
    movslq  %esi, %rax
    cmpq    %rsi, %rax
    jne     .L3
    movl    %eax, (%rdi)
    xorl    %eax, %eax
    ret
.L3:
    movl    $-1, %eax
    ret

Sono sorpreso anche che non usi la bandiera di overflow. Ancora molto meglio dei controlli di intervallo espliciti, ma non si generalizza all'aggiunta di long long.
Tavian Barnes,

@TavianBarnes hai ragione, sfortunatamente non esiste un buon modo per usare i flag di overflow in c (tranne i builtin specifici del compilatore)
Iłya Bursov

1
Questo codice soffre di overflow firmato, che è un comportamento indefinito.
Emacs mi fa impazzire il

@emacsdrivesmenuts, hai ragione, il cast nel comparisson può traboccare.
Jens Gustedt,

@emacsdrivesmenuts Il cast non è indefinito. Se non compreso nell'intervallo di int, un cast di un tipo più ampio produrrà un valore definito dall'implementazione o genererà un segnale. Tutte le implementazioni a cui tengo lo definiscono per preservare il modello di bit che fa la cosa giusta.
Tavian Barnes,

0

Potrei convincere il compilatore a usare il flag di segno assumendo (e affermando) una rappresentazione di complemento a due senza riempimento di byte. Tali implementazioni dovrebbero produrre il comportamento richiesto nella riga annotata da un commento, anche se non riesco a trovare una conferma formale positiva di questo requisito nello standard (e probabilmente non ce ne sono).

Si noti che il codice seguente gestisce solo l'aggiunta di numeri interi positivi, ma può essere esteso.

int safe_add(int* lhs, int rhs) {
    _Static_assert(-1 == ~0, "integers are not two's complement");
    _Static_assert(
        1u << (sizeof(int) * CHAR_BIT - 1) == (unsigned) INT_MIN,
        "integers have padding bytes"
    );
    unsigned value = *lhs;
    value += rhs;
    if ((int) value < 0) return -1; // impl. def., 6.3.1.3/3
    *lhs = value;
    return 0;
}

Questo produce sia clang che GCC:

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret

Penso che il cast nel confronto non sia definito. Ma potresti cavartela come faccio nella mia risposta. Ma poi, tutto il divertimento è riuscire a coprire tutti i casi. Il tuo _Static_assertnon serve molto a uno scopo, perché questo è banalmente vero su qualsiasi architettura attuale e verrà persino imposto per C2x.
Jens Gustedt,

2
@Jens In realtà sembra che il cast sia definito dall'implementazione, non indefinito, se sto leggendo (ISO / IEC 9899: 2011) 6.3.1.3/3 correttamente. Puoi ricontrollarlo? (Tuttavia, estendere questo ad argomenti negativi rende il tutto piuttosto contorto e in definitiva simile alla tua soluzione.)
Konrad Rudolph

Hai ragione, è definita l'plementazione, ma può anche generare un segnale :(
Jens Gustedt

@Jens Sì, tecnicamente immagino che l'implementazione del complemento a due potrebbe contenere ancora byte di riempimento. Forse il codice dovrebbe verificarlo confrontando l'intervallo teorico con INT_MAX. Modifica il post. Ma poi non credo che questo codice dovrebbe essere usato in pratica comunque.
Konrad Rudolph,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.