¿Existe un fragmento de C que calcule la adición segura contra desbordamiento de manera eficiente sin usar compiladores integrados?

11

Aquí hay una función C que agrega una inta otra, fallando si se produce un desbordamiento:

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;
}

Lamentablemente, no está optimizado por 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

Esta versión 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;
        }
}

está optimizado mejor :

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

pero tengo curiosidad por saber si hay una manera sin usar incorporados que GCC o Clang coincidan con los patrones.

Tavian Barnes
fuente
1
Veo que hay gcc.gnu.org/bugzilla/show_bug.cgi?id=48580 en el contexto de la multiplicación. Pero la suma debería ser mucho más fácil de combinar. Lo reportaré.
Tavian Barnes

Respuestas:

6

Lo mejor que se me ocurrió, si no tienes acceso a la bandera de desbordamiento de la arquitectura, es hacer cosas unsigned. Solo piense en toda la aritmética de bits aquí, ya que solo estamos interesados ​​en el bit más alto, que es el bit de signo cuando se interpreta como valores con signo.

(Todos esos errores de signo de módulo, no lo revisé a fondo, pero espero que la idea sea clara)

#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;
}

Si encuentra una versión de la adición que esté libre de UB, como una atómica, el ensamblador es incluso sin ramificación (pero con un prefijo de bloqueo)

#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;
}

Entonces, si tuviéramos tal operación, pero aún más "relajada", esto podría mejorar aún más la situación.

Take3: si usamos un "reparto" especial del resultado sin firmar al firmado, esto ahora no tiene ramificación:

#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;
}
Jens Gustedt
fuente
2
No es el DV, pero creo que el segundo XOR no debería ser negado. Ver, por ejemplo, este intento de probar todas las propuestas.
Bob__
Intenté algo como esto pero no pude hacerlo funcionar. Parece prometedor, pero deseo que GCC optimice el código idiomático.
R .. GitHub DEJA DE AYUDAR A ICE
1
@PSkocik, no, esto no depende de la representación del signo, el cálculo se realiza completamente como unsigned. Pero depende del hecho de que el tipo sin signo no tiene solo el bit de signo enmascarado. (Ambos están ahora garantizados en C2x, es decir, mantén todos los arcos que podamos encontrar). Entonces, no puede unsigneddevolver el resultado si es mayor que INT_MAX, eso sería una implementación definida y puede generar una señal.
Jens Gustedt
1
@PSkocik, no, desafortunadamente no, eso parecía revolucionario para el comité. Pero aquí hay un "Take3" que sale sin ramas en mi máquina.
Jens Gustedt
1
Lamento molestarte nuevamente, pero creo que deberías cambiar Take3 en algo como esto para obtener resultados correctos. Sin embargo, parece prometedor .
Bob__
2

La situación con las operaciones firmadas es mucho peor que con las no firmadas, y solo veo un patrón para la adición firmada, solo para el sonido metálico y solo cuando hay un tipo más amplio disponible:

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 da exactamente el mismo asm que con __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

De lo contrario, la solución más simple que se me ocurre es esta (con la interfaz que usó 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 y clang generan asm muy similares . gcc da esto:

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

Queremos calcular la suma unsigned, por lo unsignedque debemos ser capaces de representar todos los valores intsin que ninguno se pegue. Para convertir fácilmente el resultado de unsigneda int, lo contrario también es útil. En general, se supone el complemento de dos.

En todas las plataformas populares, creo que podemos convertir de unsigneda intmediante una asignación simple como int sum = u;, pero, como mencionó Jens, incluso la última variante del estándar C2x le permite generar una señal. La siguiente forma más natural es hacer algo así: *(unsigned *)&sum = u;pero las variantes de relleno sin trampa aparentemente podrían diferir para los tipos con y sin signo. Entonces, el ejemplo anterior va por el camino difícil. Afortunadamente, tanto gcc como clang optimizan esta difícil conversión.

PD Las dos variantes anteriores no se pudieron comparar directamente ya que tienen un comportamiento diferente. El primero sigue la pregunta original y no golpea *valueen caso de desbordamiento. El segundo sigue la respuesta de Jens y siempre marca la variable señalada por el primer parámetro, pero no tiene ramificaciones.

Alexander Cherepanov
fuente
¿Podrías mostrar el asm generado?
R .. GitHub DEJA DE AYUDAR AL HIELO
Se reemplazó la igualdad por xor en la verificación de desbordamiento para obtener una mejor asm con gcc. Añadido asm.
Alexander Cherepanov
1

La mejor versión que se me ocurre es:

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;
}

que 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
Iłya Bursov
fuente
Me sorprende que incluso eso no use la bandera de desbordamiento. Todavía es mucho mejor que las comprobaciones explícitas de rango, pero no se generaliza a la adición de largos largos.
Tavian Barnes
@TavianBarnes tienes razón, desafortunadamente no hay una buena manera de usar banderas de desbordamiento en c (excepto las compilaciones específicas del compilador)
Iłya Bursov
1
Este código sufre de desbordamiento firmado, que es Comportamiento indefinido.
Emacs me vuelve loco
@emacsdrivesmenuts, tiene razón, el elenco en la comparación puede desbordarse.
Jens Gustedt
@emacsdrivesmenuts El elenco no está indefinido. Cuando está fuera del rango de int, una conversión de un tipo más amplio producirá un valor definido por la implementación o generará una señal. Todas las implementaciones que me interesan lo definen para preservar el patrón de bits que hace lo correcto.
Tavian Barnes
0

Podría hacer que el compilador use el indicador de signo asumiendo (y afirmando) una representación de complemento a dos sin bytes de relleno. Dichas implementaciones deberían producir el comportamiento requerido en la línea anotada por un comentario, aunque no puedo encontrar una confirmación formal positiva de este requisito en el estándar (y probablemente no haya ninguna).

Tenga en cuenta que el siguiente código solo maneja la suma de enteros positivos, pero puede ampliarse.

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;
}

Esto produce tanto en clang como en GCC:

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret
Konrad Rudolph
fuente
Creo que el reparto en la comparación no está definido. Pero podrías salirte con la tuya como lo hago yo en mi respuesta. Pero también, toda la diversión está en poder cubrir todos los casos. Su _Static_assertobjetivo no tiene mucho propósito, porque esto es trivialmente cierto en cualquier arquitectura actual, e incluso se impondrá para C2x.
Jens Gustedt
2
@Jens En realidad, parece que el elenco está definido en la implementación, no está indefinido, si estoy leyendo (ISO / IEC 9899: 2011) 6.3.1.3/3 correctamente. ¿Puedes verificar eso dos veces? (Sin embargo, extender esto a argumentos negativos hace que todo sea bastante complicado y, en última instancia, similar a su solución.)
Konrad Rudolph,
Tienes razón, se define la multiplicación, pero también puede generar una señal :(
Jens Gustedt
@Jens Sí, técnicamente supongo que la implementación del complemento de dos podría contener bytes de relleno. Tal vez el código debería probar esto comparando el rango teórico con INT_MAX. Editaré la publicación. Pero, de nuevo, no creo que este código deba usarse en la práctica de todos modos.
Konrad Rudolph