Чи є фрагмент C, який ефективно розраховує безпечне доповнення без використання компілятора?


11

Ось функція 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;
}

На жаль, це не оптимізовано GCC або 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

але мені цікаво, чи є спосіб без використання вбудованих файлів, які отримають узгоджені GCC або Clang.


1
Я бачу, є gcc.gnu.org/bugzilla/show_bug.cgi?id=48580 в контексті множення. Але доповнення має бути набагато простішим для узгодження зразком. Я повідомлю про це.
Тавіан Барнс

Відповіді:


6

Найкраще, що я придумав, якщо у вас немає доступу до переповненого прапора архітектури, - це робити справи unsigned. Просто подумайте про всю бітну арифметику тут у тому, що нас цікавить лише найвищий біт, який є бітовим знаком, коли його трактують як підписані значення.

(Усі помилки підписуючи модуль, я не перевіряв це ретельно, але сподіваюся, що ідея зрозуміла)

#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, наприклад, атомну, асемблер навіть без гілки (але з префіксом блокування)

#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
Не DV, але я вважаю, що другий XOR не слід заперечувати. Дивіться, наприклад, цю спробу перевірити всі пропозиції.
Боб__

Я спробував щось подібне, але не міг змусити його працювати. Це виглядає перспективно, але я хотів би, щоб GCC оптимізував ідіоматичний код.
R .. GitHub СТОП ДОПОМОГАТИ

1
@PSkocik, ні це не залежить від представлення знаків, обчислення повністю виконано як unsigned. Але це залежить від того, що неподписаний тип не просто маскував біт знака. (Обидва тепер гарантуються в C2x, тобто утримуйте всі арки, які ми могли б знайти). Тоді ви не можете unsignedповернути результат назад, якщо він більший за INT_MAX, це буде визначено реалізацією і може підняти сигнал.
Єнс Гуведт

1
@PSkocik, ні, на жаль, ні, це здавалося революційним для комітету. Але ось "Take3", який фактично виходить без гілок на моїй машині.
Йенс Гуведт

1
Вибачте , що турбую вас знову, але я думаю , ви повинні змінити Take3 в щось на зразок цього , щоб отримати правильні результати. Це здається перспективним .
Боб__

2

Ситуація з підписаними операціями набагато гірша, ніж із непідписаними, і я бачу лише один зразок для підписаного додавання, лише для 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

Інакше найпростішим рішенням, про який я можу придумати, є таке (з інтерфейсом, як використовується 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 і clang генерують дуже схожу asm . gcc дає це:

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;але, як згадував Jens, навіть останній варіант стандарту C2x дозволяє йому піднімати сигнал. Наступний найприродніший спосіб - це зробити щось подібне: *(unsigned *)&sum = u;але варіанти, що не вловлюють пастку, мабуть, можуть відрізнятися для підписаних і неподписаних типів. Тож наведений вище приклад йде нелегким шляхом. На щастя, і gcc, і clang оптимізують цю складну конверсію.

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


Не могли б ви показати сформований зом?
R .. GitHub СТОП ДОПОМОГАТИ

Замінено рівність на xor у перевірці на переповнення, щоб покращити asm з gcc. Додано асм.
Олександр Черепанов

1

найкраща версія, яку я можу придумати, - це:

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

Я навіть здивований, що не використовується прапор переповнення. Ще набагато краще, ніж явні перевірки діапазону, але це не узагальнює додавання довгих довгих.
Тавіан Барнс

@TavianBarnes Ви маєте рацію, на жаль, немає хорошого способу використання переповнених прапорів у c (крім конкретних вбудованих компіляторів)
Iłya Bursov

1
Цей код страждає від підписаного переповнення, яке є Невизначена поведінка.
Emacs змушує мене горіхів

@emacsdrivesmenuts, ти маєш рацію, акторський склад у порівнянні може переповнитися.
Єнс Гуведт

@emacsdrivesmenuts Акторський склад не визначений. Коли виходить за межі діапазону int, трансляція з більш широкого типу буде або видавати значення, визначене реалізацією, або піднімати сигнал. Усі реалізації, які я дбаю про те, щоб визначити це, щоб зберегти бітовий зразок, який робить правильно.
Тавіан Барнс

0

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

Зауважте, що наступний код обробляє лише додатне ціле додавання, але його можна продовжити.

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

Це дає вихід як на клакс, так і на GCC:

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

Я думаю, що акторський склад у порівнянні не визначений. Але ти міг би уникнути цього, як і я у своїй відповіді. Але тоді також все задоволення полягає в тому, щоб мати можливість висвітлювати всі справи. Ваша _Static_assertпослуга не має великої мети, оскільки це тривіально вірно в будь-якій сучасній архітектурі і навіть буде нав'язуватися для C2x.
Йенс Гуведт

2
@Jens Насправді здається, що актерський склад визначено реалізацією, а не визначено, якщо я читаю (ISO / IEC 9899: 2011) 6.3.1.3/3 правильно. Чи можете ви двічі перевірити це? (Однак, поширюючи це на негативні аргументи, ця річ робить доволі викривленою і, зрештою, подібною до вашого рішення.)
Конрад Рудольф,

Ви маєте рацію, це визначено іплементацією, але може також подати сигнал :(
Jens Gustedt

@Jens Так, технічно я здогадуюсь, що реалізація доповнення двох може все ще містити байти. Можливо, код повинен перевірити це, порівнявши теоретичний діапазон з INT_MAX. Я відредагую публікацію. Але знову ж таки, я не думаю, що цей код у будь-якому разі не слід застосовувати на практиці.
Конрад Рудольф
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.