Существует ли фрагмент кода C, который эффективно вычисляет безопасное переполнение без использования встроенных компиляторов? - PullRequest
11 голосов
/ 13 февраля 2020

Вот функция C, которая добавляет int к другому, если она не произойдет, если произойдет переполнение:

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

К сожалению, не оптимизирована хорошо по G CC или 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

Эта версия с __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;
        }
}

оптимизирована лучше :

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

но я ' Интересно, есть ли способ без использования встроенных функций, которые будут сопоставлены с шаблоном G CC или Clang.

Ответы [ 4 ]

6 голосов
/ 14 февраля 2020

Лучшее, что я придумал, если у вас нет доступа к флагу переполнения архитектуры, это сделать что-то в unsigned. Просто подумайте обо всех арифметических битах c, здесь нас интересует только старший бит, который является знаковым битом, когда интерпретируется как знаковые значения.

(Все эти ошибки знака по модулю, я не проверял это тщательно, но я надеюсь, что идея ясна)

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

Если вы найдете версию дополнения, не содержащую UB, такую ​​как atomi c, ассемблер даже без ветвления ( но с префиксом блокировки)

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

Так что, если бы у нас была такая операция, но еще более «расслабленная», это могло бы еще больше улучшить ситуацию.

Take3: Если мы используем специальное «приведение» от неподписанного результата к подписанному, то теперь это без веток:

#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 голосов
/ 17 февраля 2020

Ситуация с подписанными операциями намного хуже, чем с беззнаковыми, и я вижу только один шаблон для добавления со знаком, только для clang и только когда доступен более широкий тип:

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 дает точно то же самое asm , что и с __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

В противном случае самое простое решение, о котором я могу подумать, это (с использованием интерфейса, используемого Йенсом):

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

g cc и clang генерируют очень похожие asm . g cc дает следующее:

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

Мы хотим вычислить сумму в unsigned, поэтому unsigned должна быть в состоянии представить все значения int без слипания между ними. Чтобы легко преобразовать результат из unsigned в int, полезно и обратное. В целом предполагается, что дополняют два.

На всех популярных платформах я думаю, что мы можем конвертировать из unsigned в int с помощью простого присваивания, такого как int sum = u;, но, как упоминал Йенс, даже самый последний вариант Стандарт C2x позволяет поднять сигнал. Следующий наиболее естественный способ - сделать что-то подобное: *(unsigned *)&sum = u;, но варианты дополнения без ловушек, очевидно, могут отличаться для типов со знаком и без знака. Таким образом, приведенный выше пример проходит сложный путь. К счастью, и g cc, и clang оптимизируют это сложное преобразование.

PS Два вышеописанных варианта нельзя сравнивать напрямую, поскольку они ведут себя по-разному. Первый следует исходному вопросу и не забивает *value в случае переполнения. Второй следует за ответом от Йенса и всегда забивает переменную, на которую указывает первый параметр, но он не имеет ответвлений.

1 голос
/ 13 февраля 2020

лучшая версия, которую я могу придумать:

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

, которая производит:

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
0 голосов
/ 14 февраля 2020

Я мог бы заставить компилятор использовать флаг знака, предполагая (и утверждая) представление дополнения до двух без дополнительных байтов. Такие реализации должны приводить к требуемому поведению в строке, аннотированной комментарием, хотя я не могу найти положительного формального подтверждения этого требования в стандарте (и, вероятно, его нет).

Обратите внимание, что следующий код обрабатывает только положительное целочисленное добавление, но может быть расширен.

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

Это дает как для clang, так и для G CC:

safe_add:
        add     esi, DWORD PTR [rdi]
        js      .L3
        mov     DWORD PTR [rdi], esi
        xor     eax, eax
        ret
.L3:
        mov     eax, -1
        ret
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...