Derleyici yerleşiklerini kullanmadan taşmaya karşı güvenli eki verimli bir şekilde hesaplayan bir C snippet'i var mı?


11

intTaşma gerçekleşirse başarısız bir diğerine ekleyen bir C işlevi :

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

Ne yazık ki GCC veya Clang tarafından iyi optimize edilmemiştir :

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

Bu sürüm ile __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;
        }
}

olduğu daha iyi optimize :

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

ancak GCC veya Clang ile desen eşleşecek yerleşikler kullanmanın bir yolu olup olmadığını merak ediyorum.


1
Çarpma bağlamında gcc.gnu.org/bugzilla/show_bug.cgi?id=48580 olduğunu görüyorum . Ancak, desen eşleşmesi için ekleme çok daha kolay olmalıdır. Rapor edeceğim.
Tavian Barnes

Yanıtlar:


6

Geldiğim en iyi şey, eğer mimarinin taşma bayrağına erişiminiz yoksa, bir şeyler yapmaktır unsigned. Sadece tüm bit aritmetiğini düşünün, çünkü sadece en yüksek bitle ilgileniyoruz, bu da işaretli değerler olarak yorumlandığında işaret bitidir.

(Tüm bu modulo işareti hataları, bunu iyice kontrol etmedim, ama umarım fikir açıktır)

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

Eklemenin atomik gibi UB içermeyen bir sürümünü bulursanız, montajcı da dalsızdır (ancak bir kilit önekiyle)

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

Eğer böyle bir operasyonumuz olsaydı, ama daha da "rahat" olsaydı bu durum daha da gelişebilirdi.

Take3: İmzasız sonuçtan imzalı sonuca özel bir "oyuncu" kullanırsak, şu anda şube ücretsizdir:

#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 değil, ama ikinci XOR'un reddedilmemesi gerektiğine inanıyorum. Bkz. Örneğin, tüm teklifleri test etme girişimi .
Bob__

Ben böyle bir şey denedim ama işe yaramadı. Umut verici görünüyor ama keşke GCC deyimsel kodu optimize.
R .. GitHub DURDURMA BUZA YARDIM

1
@PSkocik, hayır bu işaret temsiline bağlı değildir, hesaplama tamamen olarak yapılır unsigned. Ancak imzasız tipin sadece işaret bitinin maskelenmediğine bağlıdır. (Her ikisi de artık C2x'te garanti edilmektedir, yani bulabildiğimiz tüm arkları tutun). Daha sonra, unsignedsonuç daha büyükse INT_MAX, uygulama tanımlı olacak ve bir sinyal oluşturabilecek şekilde geri veremezsiniz .
Jens Gustedt

1
@PSkocik, ne yazık ki hayır, bu komiteye devrimci görünüyordu. Ama işte benim makinemde dallar olmadan ortaya çıkan bir "Take3".
Jens Gustedt

1
Yine rahatsız, ama böyle bir şey içine Take3 değişmelidir düşünmek bu doğru sonuçlar elde etmek. Yine de umut verici görünüyor .
Bob__

2

İmzalı işlemlerle ilgili durum, imzasız olanlardan çok daha kötüdür ve imzalı ekleme için yalnızca bir kalıp görüyorum, sadece clang için ve sadece daha geniş bir tip mevcut olduğunda:

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

çınlama tamamen aynı veren asm __builtin_add_overflow olduğu gibi:

safe_add:                               # @safe_add
        addl    (%rdi), %esi
        movl    $-1, %eax
        jo      .LBB1_2
        movl    %esi, (%rdi)
        xorl    %eax, %eax
.LBB1_2:
        retq

Aksi takdirde, aklıma gelen en basit çözüm şudur (kullanılan Jens arayüzü ile):

_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 ve clang çok benzer bir asm oluşturur . gcc bunu verir:

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

Toplamı hesaplamak istiyoruz unsigned, bu yüzden unsignedhiçbiri intbirbirine yapışmadan tüm değerlerini temsil edebilmeliyiz . Kolayca sonucunu dönüştürmek için unsignediçin int, tersi de yararlıdır. Genel olarak, ikisinin tamamlayıcısı varsayılmaktadır.

Tüm popüler platformlarda biz dönüştürebilirsiniz düşünmek unsignediçin intgibi basit bir atama tarafından int sum = u;Jens belirtildiği gibi, c2x standardının hatta en son varyantı bir sinyal yükseltmek izin verir ama. Bir sonraki en doğal yol böyle bir şey yapmaktır: *(unsigned *)&sum = u;ancak kapanın tuzak olmayan varyantları imzalı ve imzasız tipler için farklı olabilir. Yani yukarıdaki örnek zor yoldan gidiyor. Neyse ki, hem gcc hem de clang bu zor dönüşümü optimize ediyor.

Not: Yukarıdaki iki değişken, farklı davranışları olduğu için doğrudan karşılaştırılamadı. Birincisi orijinal soruyu takip eder ve *valuetaşma durumunda tıkanmaz . İkincisi Jens'in cevabını takip eder ve her zaman ilk parametrenin işaret ettiği değişkeni toplar, ancak dalsızdır.


Oluşturulan grubu gösterebilir misiniz?
R .. GitHub BUZA YARDIMCI DURDUR

Gcc ile daha iyi bir çözüm elde etmek için taşma kontrolünde eşitlik xor ile değiştirildi. Asm eklendi.
Alexander Cherepanov

1

gelip en iyi sürümü:

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

hangi üretir:

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

Taşma bayrağını kullanmasa bile şaşırdım. Açık aralık kontrollerinden hala çok daha iyi, ancak uzun uzun eklemeler için genelleme yok.
Tavian Barnes

@TavianBarnes haklısın, maalesef c'de taşma bayrakları kullanmanın iyi bir yolu yok (derleyiciye özel yerleşikler hariç)
Iłya Bursov

1
Bu kod, Tanımlanmamış Davranış olan imzalı taşma sorunu yaşıyor.
emacs beni deli ediyor

@ emacsdrivesmenuts, haklısın, karşılaştırmada oyuncu kadrosu taşabilir.
Jens Gustedt

@emacsdrivesmenuts Oyuncular undefined değil. Kapsama alanı dışındayken, intdaha geniş tipte bir döküm ya uygulama tarafından tanımlanan bir değer üretir veya bir sinyal yükseltir. Önem verdiğim tüm uygulamalar, doğru olanı yapan bit desenini korumak için onu tanımlar.
Tavian Barnes

0

Derleyicinin, bayt doldurmadan iki tamamlayıcı temsilini varsayarak (ve iddia ederek) işaret bayrağını kullanmasını sağlayabilirim. Standartta bu gereksinimin olumlu bir resmi doğrulamasını bulamasam da (ve muhtemelen hiç yok), bu tür uygulamalar bir yorumla açıklanan satırda gerekli davranışı vermelidir .

Aşağıdaki kodun yalnızca pozitif tamsayı toplamayı işlediğini, ancak genişletilebileceğini unutmayın.

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

Bu hem clang hem de GCC'yi verir:

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

Bence karşılaştırma kadrosu tanımsız. Ama cevabımda yaptığım gibi bundan kurtulabilirsiniz. Ama sonra, tüm eğlence tüm vakaları kapsayabilmektir. Sizin _Static_assertherhangi bir amaca hizmet etmez, çünkü bu mevcut herhangi bir mimari için önemsizdir ve C2x için bile uygulanacaktır.
Jens Gustedt

2
@Jens Aslında, doğru okuyorsam, oyuncu seçimi tanımlı, tanımsız değil gibi görünüyor (ISO / IEC 9899: 2011) 6.3.1.3/3. Bunu iki kez kontrol edebilir misin? (Ancak, bunu olumsuz argümanlara genişletmek her şeyi oldukça kıvrımlı ve sonuç olarak çözümünüze benzer hale getirir.)
Konrad Rudolph

Haklısınız, bu uygulama tanımlandı, ancak bir sinyal de oluşturabilir :(
Jens Gustedt

@Jens Evet, teknik olarak ikisinin tamamlayıcı uygulaması hala dolgu baytları içerebilir. Belki de kod, teorik aralığı ile karşılaştırarak bunu test etmelidir INT_MAX. Gönderiyi düzenleyeceğim. Ama sonra yine de bu kodun pratikte yine de kullanılması gerektiğini düşünmüyorum.
Konrad Rudolph
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.