Apakah ada cuplikan C yang menghitung penambahan yang aman-overflow secara efisien tanpa menggunakan kompiler bawaan?

11

Berikut adalah fungsi C yang menambahkan sebuah intke yang lain, gagal jika terjadi 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;
}

Sayangnya itu tidak dioptimalkan dengan baik oleh GCC atau Dentang:

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

Versi ini dengan __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;
        }
}

adalah dioptimalkan lebih baik :

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

tapi saya ingin tahu apakah ada cara tanpa menggunakan builtin yang akan mendapatkan kecocokan pola oleh GCC atau Dentang.

Barnes Tavian
sumber
1
Saya melihat ada gcc.gnu.org/bugzilla/show_bug.cgi?id=48580 dalam konteks multiplikasi. Tetapi penambahan harus jauh lebih mudah untuk mencocokkan pola. Saya akan melaporkannya.
Tavian Barnes
Dilaporkan: gcc.gnu.org/bugzilla/show_bug.cgi?id=93742
Tavian Barnes

Jawaban:

6

Yang terbaik yang saya buat, jika Anda tidak memiliki akses ke bendera arsitektur yang meluap, adalah melakukan sesuatu unsigned. Pikirkan semua aritmatika bit di sini karena kami hanya tertarik pada bit tertinggi, yang merupakan bit tanda ketika ditafsirkan sebagai nilai yang ditandatangani.

(Semua kesalahan tanda modulo, saya tidak memeriksa ini dengan baik, tapi saya harap idenya jelas)

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

Jika Anda menemukan versi tambahan yang bebas dari UB, seperti versi atom, assembler bahkan tanpa cabang (tetapi dengan awalan kunci)

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

Jadi jika kita melakukan operasi seperti itu, tetapi bahkan lebih "santai" ini dapat memperbaiki situasi lebih jauh.

Take3: Jika kami menggunakan "pemeran" khusus dari hasil yang tidak ditandatangani ke yang ditandatangani, sekarang ini bebas cabang:

#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
sumber
2
Bukan DV, tapi saya percaya bahwa XOR kedua tidak boleh dinegasikan. Lihat misalnya upaya ini untuk menguji semua proposal.
Bob__
Saya mencoba sesuatu seperti ini tetapi tidak berhasil. Terlihat menjanjikan tetapi saya berharap GCC mengoptimalkan kode idiomatik.
R .. GitHub BERHENTI MEMBANTU ICE
1
@PSkocik, tidak ini tidak tergantung pada representasi tanda, perhitungan seluruhnya dilakukan sebagai unsigned. Tapi itu tergantung pada fakta bahwa tipe unsigned bukan hanya sedikit saja yang disembunyikan. (Keduanya sekarang dijamin dalam C2x, yaitu, tahan untuk semua lengkungan yang bisa kita temukan). Kemudian, Anda tidak dapat mengembalikan unsignedhasilnya jika lebih besar dari itu INT_MAX, yang akan menjadi implementasi yang ditentukan dan dapat meningkatkan sinyal.
Jens Gustedt
1
@ PSkik, sayangnya tidak, yang tampaknya revolusioner untuk komite. Tapi di sini ada "Take3" yang sebenarnya keluar tanpa cabang di mesin saya.
Jens Gustedt
1
Maaf mengganggu Anda lagi, tapi saya pikir Anda harus mengubah Take3 menjadi sesuatu seperti ini untuk mendapatkan hasil yang benar. Tapi sepertinya ini menjanjikan .
Bob__
2

Situasi dengan operasi yang ditandatangani jauh lebih buruk daripada yang tidak ditandatangani, dan saya hanya melihat satu pola untuk penambahan yang ditandatangani, hanya untuk dentang dan hanya ketika jenis yang lebih luas tersedia:

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

dentang memberikan asm persis sama dengan __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

Kalau tidak, solusi paling sederhana yang dapat saya pikirkan adalah ini (dengan antarmuka seperti yang digunakan 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 dan dentang menghasilkan asm sangat mirip . gcc memberikan ini:

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

Kami ingin menghitung jumlahnya unsigned , jadi unsignedharus dapat mewakili semua nilai inttanpa ada yang menyatu. Untuk dengan mudah mengkonversi hasil dari unsignedke int, yang sebaliknya juga berguna. Secara keseluruhan, komplemen dua diasumsikan.

Pada semua platform populer, saya pikir kita dapat mengkonversi dari unsigned ke intdengan penugasan sederhana seperti int sum = u;tetapi, seperti yang disebutkan Jens, bahkan varian terbaru dari standar C2x memungkinkannya untuk menaikkan sinyal. Cara paling alami berikutnya adalah melakukan sesuatu seperti itu: *(unsigned *)&sum = u;tetapi varian padding non-trap tampaknya dapat berbeda untuk tipe yang ditandatangani dan tidak ditandatangani. Jadi contoh di atas berjalan dengan susah payah. Untungnya, baik gcc dan dentang mengoptimalkan konversi rumit ini.

PS Dua varian di atas tidak dapat dibandingkan secara langsung karena mereka memiliki perilaku yang berbeda. Yang pertama mengikuti pertanyaan awal dan tidak mengalahkan *valuejika terjadi overflow. Yang kedua mengikuti jawaban dari Jens dan selalu clobbers variabel yang ditunjukkan oleh parameter pertama tetapi itu tidak memiliki cabang.

Alexander Cherepanov
sumber
Bisakah Anda menunjukkan asm yang dihasilkan?
R .. GitHub BERHENTI MEMBANTU ICE
Mengganti kesetaraan dengan xor dalam pemeriksaan overflow untuk mendapatkan asm yang lebih baik dengan gcc. Ditambahkan asm.
Alexander Cherepanov
1

versi terbaik yang bisa saya dapatkan adalah:

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

yang menghasilkan:

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
sumber
Saya terkejut bahkan itu tidak menggunakan flag overflow. Masih jauh lebih baik daripada pemeriksaan rentang eksplisit, tetapi tidak digeneralisasikan untuk menambahkan rindu panjang.
Tavian Barnes
@TavianBarnes Anda benar, sayangnya tidak ada cara yang baik untuk menggunakan flag overflow di c (kecuali kompiler khusus builtin)
Iłya Bursov
1
Kode ini menderita overflow yang ditandatangani, yang merupakan Perilaku Tidak Terdefinisi.
emacs membuatku gila
@emacsdrivesmenuts, Anda benar, para pemain di komparator dapat meluap.
Jens Gustedt
@emacsdrivesmenuts Para pemeran tidak ditentukan. Ketika berada di luar jangkauan int, gips dari tipe yang lebih luas akan menghasilkan nilai yang ditentukan implementasi atau menaikkan sinyal. Semua implementasi yang saya pedulikan mendefinisikannya untuk mempertahankan pola bit yang melakukan hal yang benar.
Tavian Barnes
0

Saya bisa mendapatkan kompiler untuk menggunakan tanda bendera dengan mengasumsikan (dan menyatakan) representasi komplemen dua tanpa padding byte. Implementasi seperti itu harus menghasilkan perilaku yang diperlukan dalam baris yang dijelaskan oleh komentar, walaupun saya tidak dapat menemukan konfirmasi formal positif dari persyaratan ini dalam standar (dan mungkin tidak ada).

Perhatikan bahwa kode berikut hanya menangani penambahan bilangan bulat positif, tetapi dapat diperpanjang.

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

Ini menghasilkan clang dan 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
sumber
Saya pikir para pemain dalam perbandingan tidak terdefinisi. Tapi Anda bisa lolos dengan ini seperti yang saya lakukan dalam jawaban saya. Tapi kemudian juga, semua kesenangan adalah bisa menutupi semua kasus. _Static_assertTujuan Anda tidak banyak, karena ini sepele pada arsitektur saat ini, dan bahkan akan dikenakan untuk C2x.
Jens Gustedt
2
@ Jens Sebenarnya sepertinya para pemainnya adalah implementasi-didefinisikan, tidak didefinisikan, jika saya membaca (ISO / IEC 9899: 2011) 6.3.1.3/3 dengan benar. Bisakah Anda memeriksanya? (Namun, memperluas ini ke argumen negatif membuat semuanya agak berbelit-belit dan akhirnya mirip dengan solusi Anda.)
Konrad Rudolph
Anda benar, ini adalah iplementasi yang ditentukan, tetapi juga dapat meningkatkan sinyal :(
Jens Gustedt
@ Jens Ya, secara teknis saya kira implementasi pelengkap dua mungkin masih berisi padding byte. Mungkin kode harus menguji untuk ini dengan membandingkan rentang teoritis INT_MAX. Saya akan mengedit posting. Tapi sekali lagi saya tidak berpikir kode ini harus digunakan dalam praktiknya.
Konrad Rudolph