컴파일러 내장을 사용하지 않고 오버플로 안전 추가를 효율적으로 계산하는 C 스 니펫이 있습니까?


11

다음은 int오버플로가 발생하면 실패하는 다른 함수를 추가하는 C 함수입니다 .

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 곱셈의 맥락에서가. 그러나 패턴 일치는 추가하기가 훨씬 쉬워야합니다. 보고하겠습니다.
Tavian Barnes

답변:


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을 부정해서는 안된다고 생각합니다. 예를 들어 모든 제안을 테스트 하려는 시도 를 참조하십시오 .
Bob__

나는 이것과 같은 것을 시도했지만 그것을 작동시키지 못했습니다. 유망 해 보이지만 GCC가 관용어 코드를 최적화하기를 바랍니다.
R .. GitHub 중지 지원 얼음

1
@ PSkocik, 이것은 부호 표현에 의존하지 않으며 계산은 전적으로로 수행됩니다 unsigned. 그러나 부호없는 유형에 부호 비트가 마스킹되어 있지 않다는 사실에 달려 있습니다. (이제 C2x에서 모두 보장됩니다. 즉, 찾을 수있는 모든 아치를 보유합니다.) 그런 다음 unsigned이보다 크면 결과를 캐스트 할 수 없습니다. INT_MAX구현이 정의되어 신호를 발생시킬 수 있습니다.
Jens Gustedt

1
@PSkocik은 안타깝게도위원회에 혁명적 인 것처럼 보였습니다. 그러나 여기에 실제로 내 기계에 가지없이 나오는 "Take3"이 있습니다.
Jens Gustedt

1
죄송합니다 다시 당신을 귀찮게,하지만 난 당신이 뭔가에 Take3을 변경해야합니다 생각하는 올바른 결과를 얻을 수 있습니다. 그래도 유망한 것 같습니다 .
Bob__

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은 __builtin_add_overflow와 동일한 asm 을 제공합니다.

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서로 달라 붙지 않고 모든 값을 나타낼 수 있어야 합니다. 쉽게에서 결과를 변환하려면 unsignedint, 반대도 유용하다. 전반적으로 2의 보수가 가정됩니다.

모든 인기있는 플랫폼 우리가에서 변환 할 수 있습니다 생각 unsignedint같은 간단한 할당에 의해 int sum = u;옌스가 언급 한 바와 같이, C2x 표준도 최신 변종은 신호를 인상 할 수 있지만. 다음으로 가장 자연스러운 방법은 다음과 같은 작업을 수행하는 것입니다. *(unsigned *)&sum = u;패딩의 비 트랩 변형은 부호있는 유형과 부호없는 유형에 따라 다를 수 있습니다. 따라서 위의 예는 어렵습니다. 다행히도 gcc와 clang은이 까다로운 변환을 최적화합니다.

PS 위의 두 변형은 동작이 다르기 때문에 직접 비교할 수 없습니다. 첫 번째 질문은 원래의 질문을 따르며 *value오버플로의 경우를 방해하지 않습니다 . 두 번째 것은 Jens의 답변을 따르고 항상 첫 번째 매개 변수가 가리키는 변수를 클로버하지만 분기는 없습니다.


생성 된 asm을 보여줄 수 있습니까?
R .. GitHub 중지 지원 얼음

gcc로 더 나은 asm을 얻기 위해 오버플로 검사에서 동일성을 xor로 대체했습니다. asm을 추가했습니다.
Alexander Cherepanov

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

오버플로 플래그를 사용하지 않아도 놀랍습니다. 명시 적 범위 검사보다 훨씬 낫지 만 더 이상 길게 추가하는 것은 아닙니다.
Tavian Barnes

@TavianBarnes 당신이 맞습니다, 불행히도 c에서 오버플로 플래그를 사용하는 좋은 방법은 없습니다 (컴파일러 특정 내장 제외)
Iłya Bursov

1
이 코드에는 정의되지 않은 동작 인 부호있는 오버플로가 발생합니다.
emacs는

@emacsdrivesmenuts, 맞습니다. 비교의 캐스트가 오버플로 될 수 있습니다.
Jens Gustedt

@emacsdrivesmenuts 캐스트가 정의되지 않았습니다. 의 범위를 int벗어나면 더 넓은 유형의 캐스트가 구현 정의 값을 생성하거나 신호를 발생시킵니다. 내가 관심있는 모든 구현은 올바른 일을하는 비트 패턴을 유지하도록 정의합니다.
Tavian Barnes

0

패딩 바이트없이 2의 보수 표현을 가정 (및 주장)하여 컴파일러가 부호 플래그를 사용하도록 할 수 있습니다. 이러한 구현 주석으로 주석이 달린 행에서 필요한 동작을 산출 해야 하지만 표준 에서이 요구 사항에 대한 긍정적 인 공식 확인을 찾을 수는 없지만 (아마도 없습니다).

다음 코드는 양의 정수 추가 만 처리하지만 확장 할 수 있습니다.

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과 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 부과되기 때문.
Jens Gustedt

2
@Jens 실제로 (ISO / IEC 9899 : 2011) 6.3.1.3/3을 올바르게 읽으면 캐스트가 구현 정의되어 있고 정의되지 않은 것 같습니다. 다시 확인할 수 있습니까? (그러나 이것을 부정적 논증으로 확장하면 모든 것이 다소 복잡하고 궁극적으로 솔루션과 유사합니다.)
Konrad Rudolph

당신은 맞습니다, 그것은 구현이 정의되었지만 신호를 일으킬 수도 있습니다 :(
Jens Gustedt

@Jens 예, 기술적으로 2의 보수 구현에는 여전히 패딩 바이트가 포함될 수 있습니다. 이론적 범위를와 비교하여 코드에서이를 테스트해야 할 수도 INT_MAX있습니다. 글을 수정하겠습니다. 그러나 다시 한 번 나는이 코드가 실제로 사용되어야한다고 생각하지 않습니다.
Konrad Rudolph
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.