Có đoạn mã C nào tính toán bổ sung an toàn tràn hiệu quả mà không cần sử dụng nội dung trình biên dịch không?


11

Đây là một hàm C thêm một inthàm khác, không thành công nếu tràn sẽ xảy ra:

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

Thật không may, nó không được tối ưu hóa tốt bởi GCC hoặc 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

Phiên bản này với __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;
        }
}

được tối ưu hóa tốt hơn :

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

nhưng tôi tò mò liệu có cách nào mà không sử dụng các nội trang sẽ được khớp với mẫu của GCC hoặc Clang không.


1
Tôi thấy có gcc.gnu.org/ormszilla/show_orms.cgi?id=48580 trong bối cảnh nhân. Nhưng bổ sung sẽ dễ dàng hơn nhiều để phù hợp với mô hình. Tôi sẽ báo cáo nó.
Tavian Barnes

Câu trả lời:


6

Điều tốt nhất tôi nghĩ ra, nếu bạn không có quyền truy cập vào cờ tràn của kiến ​​trúc, là thực hiện mọi thứ unsigned. Chỉ cần nghĩ về tất cả số học bit ở đây là chúng ta chỉ quan tâm đến bit cao nhất, đó là bit dấu khi được hiểu là các giá trị đã ký.

(Tất cả các lỗi ký hiệu modulo đó, tôi đã không kiểm tra điều này một cách rõ ràng, nhưng tôi hy vọng ý tưởng này rõ ràng)

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

Nếu bạn tìm thấy một phiên bản bổ sung không có UB, chẳng hạn như phiên bản nguyên tử, trình biên dịch thậm chí không có nhánh (nhưng có tiền tố khóa)

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

Vì vậy, nếu chúng tôi có một hoạt động như vậy, nhưng thậm chí "thoải mái" hơn, điều này có thể cải thiện tình hình hơn nữa.

Take3: Nếu chúng ta sử dụng một "cast" đặc biệt từ kết quả không dấu đến kết quả đã ký, thì đây là nhánh miễn phí:

#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
Không phải DV, nhưng tôi tin rằng XOR thứ hai không nên bị từ chối. Xem ví dụ như nỗ lực này để kiểm tra tất cả các đề xuất.
Bob__

Tôi đã thử một cái gì đó như thế này nhưng không thể làm cho nó hoạt động. Có vẻ đầy hứa hẹn nhưng tôi muốn GCC tối ưu hóa mã thành ngữ.
R .. GitHub DỪNG GIÚP ICE

1
@PSkocik, không điều này không phụ thuộc vào biểu diễn dấu hiệu, việc tính toán hoàn toàn được thực hiện như unsigned. Nhưng nó phụ thuộc vào thực tế là loại không dấu không chỉ là dấu bit bị che đi. (Cả hai hiện được đảm bảo trong C2x, nghĩa là giữ cho tất cả các vòm mà chúng ta có thể tìm thấy). Sau đó, bạn không thể bỏ unsignedkết quả trở lại nếu nó lớn hơn INT_MAX, đó sẽ là việc thực hiện được xác định và có thể tăng tín hiệu.
Jens Gustyt

1
@PSkocik, không may là không, điều đó dường như mang tính cách mạng cho ủy ban. Nhưng đây là một "Take3" thực tế xuất hiện mà không có chi nhánh trên máy của tôi.
Jens Gustyt

1
Xin lỗi đã làm phiền bạn một lần nữa, nhưng tôi nghĩ bạn nên thay đổi Take3 thành một cái gì đó như thế này để có được kết quả chính xác. Nó có vẻ đầy hứa hẹn , mặc dù.
Bob__

2

Tình huống với các hoạt động đã ký còn tồi tệ hơn nhiều so với các hoạt động không dấu và tôi chỉ thấy một mẫu cho bổ sung đã ký, chỉ cho tiếng kêu và chỉ khi có sẵn một loại rộng hơn:

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 cung cấp chính xác asm như với __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

Mặt khác, giải pháp đơn giản nhất tôi có thể nghĩ đến là đây (với giao diện như Jens đã sử dụng):

_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 và clang tạo ra asm rất giống nhau . gcc cho điều này:

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

Chúng tôi muốn tính tổng unsigned, vì vậy unsignedphải có thể biểu diễn tất cả các giá trị intmà không có bất kỳ giá trị nào dính vào nhau. Để dễ dàng chuyển đổi kết quả từ unsignedsang int, ngược lại cũng hữu ích. Nhìn chung, hai bổ sung được giả định.

Trên tất cả các nền tảng phổ biến, tôi nghĩ rằng chúng ta có thể chuyển đổi từ unsignedsang intmột nhiệm vụ đơn giản như int sum = u;, nhưng như Jens đã đề cập, ngay cả biến thể mới nhất của tiêu chuẩn C2x cũng cho phép nó tăng tín hiệu. Cách tự nhiên nhất tiếp theo là làm một cái gì đó tương tự: *(unsigned *)&sum = u;nhưng các biến thể đệm không bẫy rõ ràng có thể khác nhau đối với các loại đã ký và không dấu. Vì vậy, ví dụ trên đi một cách khó khăn. May mắn thay, cả gcc và clang đều tối ưu hóa việc chuyển đổi khó khăn này.

PS Hai biến thể ở trên không thể so sánh trực tiếp vì chúng có hành vi khác nhau. Câu hỏi đầu tiên tuân theo câu hỏi ban đầu và không ghi đè *valuetrong trường hợp tràn. Cái thứ hai theo câu trả lời từ Jens và luôn ghi lại biến được chỉ ra bởi tham số đầu tiên nhưng nó không có nhánh.


Bạn có thể hiển thị asm được tạo ra?
R .. GitHub DỪNG GIÚP ICE

Thay thế đẳng thức bằng xor trong kiểm tra tràn để có được asm tốt hơn với gcc. Đã thêm asm.
Alexander Cherepanov

1

phiên bản tốt nhất tôi có thể nghĩ ra là:

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

sản xuất:

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

Tôi ngạc nhiên ngay cả khi không sử dụng cờ tràn. Vẫn tốt hơn nhiều so với kiểm tra phạm vi rõ ràng, nhưng nó không khái quát để thêm thời gian dài.
Tavian Barnes

@TavianBarnes bạn nói đúng, thật không may, không có cách nào tốt để sử dụng cờ tràn trong c (ngoại trừ phần xây dựng cụ thể của trình biên dịch)
Iłya Bursov

1
Mã này bị tràn tràn đã ký, đó là Hành vi không xác định.
emacs khiến tôi phát điên vào

@emacsdrivemenuts, bạn nói đúng, dàn diễn viên trong so sánh có thể tràn.
Jens Gustyt

@emacsdrivemenuts Dàn diễn viên không được xác định. Khi nằm ngoài phạm vi int, một vật đúc từ loại rộng hơn sẽ tạo ra giá trị do xác định thực hiện hoặc tăng tín hiệu. Tất cả các triển khai tôi quan tâm đến việc định nghĩa nó để bảo toàn mô hình bit thực hiện đúng.
Tavian Barnes

0

Tôi có thể yêu cầu trình biên dịch sử dụng cờ dấu bằng cách giả sử (và khẳng định) một biểu diễn bổ sung của hai mà không cần đệm byte. Việc triển khai như vậy sẽ mang lại hành vi cần thiết trong dòng được chú thích bởi một nhận xét, mặc dù tôi không thể tìm thấy xác nhận chính thức tích cực về yêu cầu này trong tiêu chuẩn (và có lẽ không có gì).

Lưu ý rằng đoạn mã sau chỉ xử lý phép cộng số nguyên dương, nhưng có thể được mở rộng.

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

Điều này mang lại cả tiếng kêu và GCC:

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

Tôi nghĩ rằng các diễn viên trong so sánh là không xác định. Nhưng bạn có thể thoát khỏi điều này như tôi làm trong câu trả lời của tôi. Nhưng sau đó, tất cả những niềm vui là trong việc có thể bao gồm tất cả các trường hợp. Việc _Static_assertphục vụ của bạn không có nhiều mục đích, bởi vì điều này là đúng đối với bất kỳ kiến ​​trúc hiện tại nào, và thậm chí sẽ được áp đặt cho C2x.
Jens Gustyt

2
@Jens Trên thực tế, có vẻ như dàn diễn viên được xác định theo triển khai, không được xác định, nếu tôi đang đọc (ISO / IEC 9899: 2011) 6.3.1.3/3. Bạn có thể kiểm tra lại không? (Tuy nhiên, việc mở rộng điều này thành các lập luận tiêu cực làm cho toàn bộ sự việc trở nên phức tạp và cuối cùng tương tự như giải pháp của bạn.)
Konrad Rudolph

Bạn nói đúng, đó là việc thực hiện được xác định, nhưng cũng có thể đưa ra tín hiệu :(
Jens Gustyt

@Jens Vâng, về mặt kỹ thuật tôi đoán việc triển khai bổ sung hai có thể vẫn chứa các byte đệm. Có lẽ mã nên kiểm tra điều này bằng cách so sánh phạm vi lý thuyết với INT_MAX. Tôi sẽ chỉnh sửa bài viết. Nhưng sau đó một lần nữa tôi không nghĩ rằng mã này nên được sử dụng trong thực tế.
Konrad Rudolph
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.