Чи є фрагмент 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.