2011-12-16 71 views
15

Có cách nào hiệu quả và di động để kiểm tra khi phép nhân với phép toán int64_t hoặc uint64_t tràn trong C không?phát hiện phép nhân uint64_t số nguyên tràn với C

Ví dụ, đối với việc bổ sung các uint64_t tôi có thể làm:

if (UINT64_MAX - a < b) overflow_detected(); 
else sum = a + b; 

Nhưng tôi không thể có được một biểu thức đơn giản tương tự cho phép nhân.

Tất cả những gì xảy ra với tôi là phá vỡ các toán hạng thành các phần uint32_t cao và thấp và thực hiện phép nhân của những phần đó trong khi kiểm tra tràn, cái gì đó thực sự xấu xí và có thể không hiệu quả.

Cập nhật 1: Một số mã chuẩn thực hiện một số phương pháp tiếp cận thêm

Cập nhật 2: Jens Gustedt phương pháp bổ sung

chương trình chuẩn:

#include <stdio.h> 
#include <stdint.h> 
#include <stdlib.h> 

#define N 100000000 

int d = 2; 

#define POW_2_64 ((double)(1 << 31) * (double)(1 << 31) * 4) 

#define calc_b (a + c) 
// #define calc_b (a + d) 

int main(int argc, char *argv[]) { 
    uint64_t a; 
    uint64_t c = 0; 
    int o = 0; 
    int opt; 

    if (argc != 2) exit(1); 

    opt = atoi(argv[1]); 

    switch (opt) { 

    case 1: /* faked check, just for timing */ 
     for (a = 0; a < N; a++) { 
      uint64_t b = a + c; 
      if (c > a) o++; 
      c += b * a; 
     } 
     break; 

    case 2: /* using division */ 
     for (a = 0; a < N; a++) { 
      uint64_t b = a + c; 
      if (b && (a > UINT64_MAX/b)) o++; 
      c += b * a; 
     } 
     break; 

    case 3: /* using floating point, unreliable */ 
     for (a = 0; a < N; a++) { 
      uint64_t b = a + c; 
      if ((double)UINT64_MAX < (double)a * (double)b) o++; 
      c += b * a; 
     } 
     break; 

    case 4: /* using floating point and division for difficult cases */ 
     for (a = 0; a < N; a++) { 
      uint64_t b = a + c; 
      double m = (double)a * (double)b; 
      if (((double)(~(uint64_t)(0xffffffff)) < m) && 
       ((POW_2_64 < m) || 
        (b && 
        (a > UINT64_MAX/b)))) o++; 
      c += b * a; 
     } 
     break; 

    case 5: /* Jens Gustedt method */ 
     for (a = 0; a < N; a++) { 
      uint64_t b = a + c; 
      uint64_t a1, b1; 
      if (a > b) { a1 = a; b1 = b; } 
      else  { a1 = b; b1 = a; } 
      if (b1 > 0xffffffff) o++; 
      else { 
       uint64_t a1l = (a1 & 0xffffffff) * b1; 
       uint64_t a1h = (a1 >> 32) * b1 + (a1l >> 32); 
       if (a1h >> 32) o++; 
      } 
      c += b1 * a1; 
     } 
     break; 

    default: 
     exit(2); 
    } 
    printf("c: %lu, o: %u\n", c, o); 
} 

Cho đến nay, trường hợp 4 sử dụng điểm nổi để lọc hầu hết các trường hợp là nhanh nhất khi nó được giả định rằng các luồng quá bất thường, ít nhất là trên máy tính của tôi, nơi nó chỉ là hai t imes chậm hơn so với trường hợp không có gì.

Trường hợp 5, là chậm hơn so với 4 30%, nhưng nó luôn luôn thực hiện như nhau, không có bất kỳ số trường hợp đặc biệt mà yêu cầu xử lý chậm như xảy ra với 4.

+0

Làm thế nào về việc sử dụng dấu phẩy động và, nếu kết quả rất gần với 2^64, làm số nguyên nhân? Nếu kết quả dấu phẩy động trên 9.223370E + 18 thì sản phẩm chính xác chắc chắn sẽ lớn hơn 2^63, và nếu kết quả dấu phẩy động nhỏ hơn 9.223374E + 18 thì sản phẩm chính xác chắc chắn sẽ nhỏ hơn 3^63. Vì vậy, nếu kết quả dấu phẩy động là gần và số nguyên không dấu nhân năng suất lớn hơn 1ULL << 63, kết quả số nguyên sẽ không đại diện cho tràn. – supercat

+0

@supercat: đó là trường hợp chủ yếu là trường hợp 4. – salva

+0

Trường hợp bốn dường như sử dụng phân chia để kiểm tra xem kết quả có phù hợp hay không. Trên nhiều bộ vi xử lý, làm số nguyên không dấu nhân sẽ tự nó nhanh hơn nhiều (phân chia số nguyên thường là một trong các hướng dẫn chậm nhất). – supercat

Trả lời

5

Nếu bạn muốn tránh phân chia như trong Ambrož' câu trả lời:

Trước tiên, bạn phải thấy rằng nhỏ hơn của hai số, nói a, là ít hơn 2 , nếu không kết quả sẽ tràn bất kỳ cách nào. Để b được phân tách thành hai từ 32 bit là b = + d.

Các tính toán thì không phải là quá khó khăn, tôi thấy:

uint64_t mult_with_overflow_check(uint64_t a, uint64_t b) { 
    if (a > b) return mult_with_overflow_check(b, a); 
    if (a > UINT32_MAX) overflow(); 
    uint32_t c = b >> 32; 
    uint32_t d = UINT32_MAX & b; 
    uint64_t r = a * c; 
    uint64_t s = a * d; 
    if (r > UINT32_MAX) overflow(); 
    r <<= 32; 
    return addition_with_overflow_check(s, r); 
} 

vì vậy đây là hai phép nhân, hai ca, một số bổ sung và kiểm tra điều kiện. Điều này có thể hiệu quả hơn phân chia vì ví dụ, hai phép nhân có thể được pipelined trong paralle. Bạn sẽ phải điểm chuẩn để xem những gì hiệu quả hơn cho bạn.

+1

tôi nghĩ rằng sự thay đổi chính xác nên là: 'if (r> UINT32_MAX) tràn(); r << = 32; 'vì số được dịch chuyển trước là c và r = a * c! vì vậy chúng tôi phải quay lại r. điều này có sai không? –

+0

Thật vậy, các dòng 'if (s> UINT32_MAX) overflow();' và 's << = 32;' là không chính xác, chúng nên sử dụng 'r' thay vì' s'. –

+0

@AdamBowen, phải, cảm ơn. Đã sửa. –

9

Trên thực tế, nguyên tắc tương tự có thể được sử dụng đối với phép nhân:

uint64_t a; 
uint64_t b; 
... 
if (b != 0 && a > UINT64_MAX/b) { // if you multiply by b, you get: a * b > UINT64_MAX 
    <error> 
} 
uint64_t c = a * b; 

Đối với các số nguyên có dấu tương tự có thể được thực hiện, bạn có thể cần một trường hợp cho mỗi tổ hợp biển báo.

+1

Phân chia hoạt động rất chậm. Tôi đã chạy một số điểm chuẩn trên máy tính của mình và tốc độ này chậm hơn 10 lần. – salva

1
case 6: 
    for (a = 0; a < N; a++) { 
     uint64_t b = a + c; 
     uint64_t a1, b1; 
     if (a > b) { a1 = a; b1 = b; } 
     else  { a1 = b; b1 = a; } 
     uint64_t cc = b1 * a1; 
     c += cc; 
     if (b1 > 0xffffffff) o++; 
     else { 
      uint64_t a1l = (a1 & 0xffffffff) + (a1 >> 32); 
      a1l = (a1 + (a1 >> 32)) & 0xffffffff; 
      uint64_t ab1l = a1l * b1; 
      ab1l = (ab1l & 0xffffffff) + (ab1l >> 32); 
      ab1l += (ab1l >> 32); 
      uint64_t ccl = (cc & 0xffffffff) + (cc >> 32); 
      ccl += (ccl >> 32); 
      uint32_t ab32 = ab1l; if (ab32 == 0xffffffff) ab32 = 0; 
      uint32_t cc32 = ccl; if (cc32 == 0xffffffff) cc32 = 0; 
      if (ab32 != cc32) o++; 
     } 
    } 
    break; 

Phương pháp này so sánh (có thể tràn) kết quả phép nhân bình thường với kết quả nhân, không thể tràn. Tất cả các phép tính là modulo (2^32 - 1).

Điều này phức tạp hơn và (rất có thể) không nhanh hơn phương pháp của Jens Gustedt.

Sau một số sửa đổi nhỏ, nó có thể nhân với độ chính xác 96 bit (nhưng không có kiểm soát tràn). Điều gì có thể thú vị hơn, ý tưởng của phương pháp này có thể được sử dụng để kiểm tra tràn cho một loạt các phép toán số học (phép nhân, phép cộng, phép trừ).

Một số câu hỏi đã trả lời

Trước hết, về "your code is not portable". Có, mã không di động vì nó sử dụng uint64_t, được yêu cầu trong câu hỏi ban đầu. Nói đúng ra, bạn không thể nhận được bất kỳ câu trả lời di động nào với (u)int64_t vì nó không được yêu cầu theo tiêu chuẩn.

Giới thiệu "once some overflow happens, you can not assume the result value to be anything". Tiêu chuẩn nói rằng những mảnh đất chưa được ký không thể tràn. Chương 6.2.5, mục 9:

Một tính toán liên quan đến toán hạng unsigned thể không bao giờ tràn, vì một kết quả mà không thể được đại diện bởi các kết quả kiểu dữ liệu integer unsigned được giảm modulo số đó là một lớn hơn lớn nhất giá trị có thể là được biểu thị bằng loại kết quả.

Vì vậy, phép nhân 64 bit chưa ký được thực hiện modulo 2^64, không tràn.

Bây giờ, về số "logic behind". "Hàm băm" không phải là từ đúng ở đây. Tôi chỉ sử dụng phép tính modulo (2^32 - 1). Kết quả của phép nhân có thể được biểu diễn là n*2^64 + m, trong đó m là kết quả hiển thị và n có nghĩa là chúng ta đã tràn ra bao nhiêu. Kể từ 2^64 = 1 (mod 2^32 - 1), chúng tôi có thể tính [true value] - [visible value] = (n*2^64 + m) - m = n*2^64 = n (mod 2^32 - 1). Nếu giá trị được tính toán của n không phải là 0, thì sẽ có tràn. Nếu nó bằng không, không có tràn. Bất kỳ va chạm nào cũng chỉ có thể sau n >= 2^32 - 1. Điều này sẽ không bao giờ xảy ra vì chúng tôi kiểm tra xem một trong các phép nhân có nhỏ hơn 2^32 hay không.

+0

Bạn có thể giải thích logic đằng sau? Tôi thấy bạn có một hàm băm bạn đang tính toán theo hai cách sẽ tạo ra kết quả tương tự trừ khi tràn chúng ta muốn bắt xảy ra. Nhưng, có thể có một số va chạm trên băm mà có thể cho một số tràn qua không bị phát hiện? Bên cạnh đó, tiêu chuẩn C cho biết khi xảy ra tình trạng tràn, bạn không thể giả định giá trị kết quả là bất kỳ điều gì. Cụ thể, gcc thực hiện tối ưu hóa cho rằng tràn không xảy ra. Và vì vậy mã của bạn không phải là xách tay. – salva

+0

@salva Tôi đồng ý, có những lý do để xử lý mã này không phải di động. Tiêu chuẩn không phải là rất rõ ràng về số nguyên tràn (theo như tôi hiểu nó). Chúng tôi chỉ có các máy tính nhị phân và chúng thực hiện phép nhân số nguyên theo cách thông thường, cắt ngắn kết quả. Vì vậy, vấn đề về tính di động này chủ yếu là lý thuyết. Xem thêm giải thích trong câu trả lời. –

+0

Vấn đề này không phải là lý thuyết, nó là rất thực tế! Ít nhất gcc giả định rằng tràn không bao giờ xảy ra khi nó tối ưu hóa mã của bạn. Ví dụ, nó có thể loại bỏ mã như 'if ((uint32_t) 12 + some_uint <10) {do_something(); } ' – salva

1

Nó có thể không phát hiện tràn chính xác, nhưng nói chung bạn có thể kiểm tra kết quả của phép nhân của bạn trên một quy mô lôgarít:

if (log(UINT64_MAX-1) - log(a) - log(b) < 0) overflow_detected(); // subtracting 1 to allow some tolerance when the numbers are converted to double 
else prod = a * b; 

Nó phụ thuộc nếu bạn thực sự cần phải làm nhân lên đến chính xác UINT64_MAX, nếu không này một cách rất thuận tiện và di động để kiểm tra phép nhân số lớn.

+0

không cần làm nhật ký chậm. Chỉ cần tìm chiều dài (trên cùng 1 bit) của a và b, nếu chiều dài (a) + chiều dài (b)> 64 thì phép nhân sẽ tràn –

Các vấn đề liên quan