Bão hòa trừ / cộng cho các byte không dấu


83

Hãy tưởng tượng tôi có hai byte không dấu bx. Tôi cần phải tính toán bsubnhư b - xbaddnhư b + x. Tuy nhiên, tôi không muốn tràn / tràn dòng xảy ra trong các hoạt động này. Ví dụ (mã giả):

b = 3; x = 5;
bsub = b - x; // bsub must be 0, not 254

b = 250; x = 10;
badd = b + x; // badd must be 255, not 4

Cách rõ ràng để làm điều này bao gồm phân nhánh:

bsub = b - min(b, x);
badd = b + min(255 - b, x);

Tôi chỉ tự hỏi nếu có bất kỳ cách nào tốt hơn để làm điều này, tức là bằng một số thao tác bit hacky?


13
y ^ ((x ^ y) & -(x < y))cho intcác loại đánh giá min(x, y)mà không phân nhánh. Điều này có thể là một phần của giải pháp cuối cùng, dựa trên những gì bạn có cho đến nay.
Bathsheba


8
Đây là câu hỏi C hay C ++? Hãy chọn một cái.
fuz

9
@AlanCampbell nó được gọi là Saturating Arithmetic .
Shafik Yaghmour

7
Bạn có cần nó để di động? Bởi vì nếu bạn đang xem xét một kiến ​​trúc cụ thể, thì có lẽ sẽ có một hướng dẫn duy nhất. Tôi biết ARM có phép cộng và trừ vectơ bão hòa cho các byte. Trên X86, _mm_adds_epi8nội tại sẽ thực hiện thêm 16 byte bão hòa trong một lệnh duy nhất.
porglezomp

Câu trả lời:


86

Bài báo Branchfree Saturating Arithmetic cung cấp các chiến lược cho việc này:

Giải pháp bổ sung của họ như sau:

u32b sat_addu32b(u32b x, u32b y)
{
    u32b res = x + y;
    res |= -(res < x);

    return res;
}

đã sửa đổi cho uint8_t:

uint8_t  sat_addu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x + y;
    res |= -(res < x);

    return res;
}

và giải pháp trừ của chúng là:

u32b sat_subu32b(u32b x, u32b y)
{
    u32b res = x - y;
    res &= -(res <= x);

    return res;
}

đã sửa đổi cho uint8_t:

uint8_t sat_subu8b(uint8_t x, uint8_t y)
{
    uint8_t res = x - y;
    res &= -(res <= x);

    return res;
}

2
@ user1969104 đó có thể là trường hợp nhưng như nhận xét trong bài viết chỉ ra, điều đó được giải quyết bằng cách truyền sang không dấu trước khi áp dụng trừ một lần. Trong thực tế , không chắc bạn sẽ phải đối phó với bất cứ điều gì khác ngoài sự bổ sung của hai thứ .
Shafik Yaghmour

2
Đây có thể là một câu trả lời C tốt, nhưng không phải là một câu trả lời C ++ rất tốt.
Yakk - Adam Nevraumont

4
@Yakk Điều gì khiến câu trả lời này trở thành câu trả lời C ++ "tệ"? Đây là những phép toán cơ bản và tôi không hiểu nó sẽ được hiểu như thế nào là chỉ C hay C ++ tồi.
JPhi1618

4
@ JPhi1618 Một câu trả lời C ++ tốt hơn có thể là template<class T>struct sat{T t;};với các toán tử quá tải mà bão hòa? Sử dụng không gian tên hợp lý. Chủ yếu là đường.
Yakk - Adam Nevraumont

6
@Yakk, À, được rồi. Tôi chỉ xem đây là một ví dụ tối thiểu mà OP có thể điều chỉnh khi cần thiết. Tôi sẽ không mong đợi để thấy rằng hoàn thành một triển khai. Cảm ơn đã làm rõ.
JPhi1618

40

Một phương pháp đơn giản là phát hiện tràn và đặt lại giá trị cho phù hợp như bên dưới

bsub = b - x;
if (bsub > b)
{
    bsub = 0;
}

badd = b + x;
if (badd < b)
{
    badd = 255;
}

GCC có thể tối ưu hóa kiểm tra tràn thành nhiệm vụ có điều kiện khi biên dịch với -O2.

Tôi đã đo lường mức độ tối ưu hóa so với các giải pháp khác. Với 1000000000+ thao tác trên PC của tôi, giải pháp này và giải pháp của @ShafikYaghmour trung bình là 4,2 giây và của @chux trung bình là 4,8 giây. Giải pháp này cũng dễ đọc hơn.


5
@ user694733 Nó không được tối ưu hóa đi, nó được tối ưu hóa thành nhiệm vụ có điều kiện tùy thuộc vào cờ thực hiện.
fuz

2
Có user694733 là đúng. Nó được tối ưu hóa thành một bài tập có điều kiện.
user1969104

Điều này sẽ không hoạt động cho tất cả các trường hợp, ví dụ badd: b = 155 x = 201, hơn badd = 156 và lớn hơn b. Bạn sẽ cần phải so sánh kết quả với min () hoặc max () của hai biến, tùy thuộc vào hoạt động
Cristian F

@CristianF Làm thế nào để bạn tính được 155 + 201 = 156? Tôi nghĩ rằng nó cần phải là 155 + 201 = 356% 256 = 100. Tôi không nghĩ rằng min (), max () là cần thiết trong bất kỳ sự kết hợp nào của các giá trị b, x.
user1969104

16

Đối với phép trừ:

diff = (a - b)*(a >= b);

Thêm vào:

sum = (a + b) | -(a > (255 - b))

Sự phát triển

// sum = (a + b)*(a <= (255-b)); this fails
// sum = (a + b) | -(a <= (255 - b)) falis too

Nhờ vào @R_Kapp

Nhờ vào @NathanOliver

Bài tập này cho thấy giá trị của việc viết mã đơn giản.

sum = b + min(255 - b, a);

sumlẽ (a + b) | -(a <= (255 - b))nào?
R_Kapp

Bạn có thể làm sum = ((a + b) | (!!((a + b) & ~0xFF) * 0xFF)) & 0xFF, giả sử sizeof(int) > sizeof(unsigned char), nhưng điều này trông phức tạp đến mức tôi không biết liệu bạn có thu được gì với nó hay không (ngoài việc đau đầu).
user694733

@ user694733 Có và thậm chí có thể (a+b+1)*(a <= (255-b)) - 1.
chux - Phục hồi Monica

@NathanOliver Cảm ơn bạn đã giám sát - khía cạnh đáng chú ý của điều này là subdễ dàng như giới hạn 0. Nhưng các giới hạn khác gây ra phức tạp và theo bình luận của user2079303 .
chux - Phục hồi Monica

1
@ user1969104 OP không nói rõ về "tốt hơn" (không gian mã so với hiệu suất tốc độ) cũng như nền tảng mục tiêu cũng như trình biên dịch. Đánh giá tốc độ có ý nghĩa nhất trong bối cảnh của vấn đề lớn hơn chưa được đăng.
chux - Phục hồi Monica

13

Nếu bạn đang sử dụng phiên bản gcc hoặc clang đủ gần đây (có thể cũng có một số phiên bản khác), bạn có thể sử dụng cài sẵn để phát hiện tràn.

if (__builtin_add_overflow(a,b,&c))
{
  c = UINT_MAX;
}

Đây là câu trả lời tốt nhất. Sử dụng trình biên dịch tích hợp sẵn thay vì phép thuật bit không chỉ nhanh hơn mà còn rõ ràng hơn và làm cho mã dễ bảo trì hơn.
Cephalopod

Cảm ơn bạn, @erebos. Tôi chắc chắn sẽ thử điều này trên các nền tảng có sẵn.
ovk

3
Tôi không thể nhận gcc để tạo mã brachless với cái này, điều này hơi thất vọng. Điều đặc biệt đáng tiếc ở đây là clang sử dụng các tên khác nhau cho những thứ này .
Shafik Yaghmour

1
@Cephalopod Và nó hoàn toàn không có định dạng chéo, rất có thể thậm chí không hoạt động trên một trình biên dịch khác. Không phải là một giải pháp tốt cho thế kỷ 21.
Ela782

1
@ Ela782 Hoàn toàn ngược lại: tích hợp sẵn không phải là giải pháp tốt cho thế kỷ 20. Chào mừng đến với tương lai!
Cephalopod

3

Ngoài ra:

unsigned temp = a+b;  // temp>>8 will be 1 if overflow else 0
unsigned char c = temp | -(temp >> 8);

Đối với phép trừ:

unsigned temp = a-b;  // temp>>8 will be 0xFF if neg-overflow else 0
unsigned char c = temp & ~(temp >> 8);

Không cần toán tử so sánh hoặc phép nhân.


3

Nếu bạn sẵn sàng sử dụng lắp ráp hoặc bản chất, tôi nghĩ tôi có một giải pháp tối ưu.

Đối với phép trừ:

Chúng ta có thể sử dụng sbb hướng dẫn

Trong MSVC, chúng ta có thể sử dụng hàm nội tại _subborrow_u64 (cũng có sẵn ở các kích thước bit khác).

Đây là cách nó được sử dụng:

// *c = a - (b + borrow)
// borrow_flag is set to 1 if (a < (b + borrow))
borrow_flag = _subborrow_u64(borrow_flag, a, b, c);

Đây là cách chúng tôi có thể áp dụng nó vào tình huống của bạn

uint64_t sub_no_underflow(uint64_t a, uint64_t b){
    uint64_t result;
    borrow_flag = _subborrow_u64(0, a, b, &result);
    return result * !borrow_flag;
}

Ngoài ra:

Chúng ta có thể sử dụngadcx hướng dẫn

Trong MSVC, chúng ta có thể sử dụng hàm nội tại _addcarry_u64 (cũng có sẵn ở các kích thước bit khác).

Đây là cách nó được sử dụng:

// *c = a + b + carry
// carry_flag is set to 1 if there is a carry bit
carry_flag = _addcarry_u64(carry_flag, a, b, c);

Đây là cách chúng tôi có thể áp dụng nó vào tình huống của bạn

uint64_t add_no_overflow(uint64_t a, uint64_t b){
    uint64_t result;
    carry_flag = _addcarry_u64(0, a, b, &result);
    return !carry_flag * result - carry_flag;
}

Tôi không thích cái này nhiều như cái trừ, nhưng tôi nghĩ nó khá tiện lợi.

Nếu phần bổ sung bị tràn carry_flag = 1,. Not-ing carry_flagcho kết quả là 0, vì vậy !carry_flag * result = 0khi có tràn. Và vì 0 - 1sẽ đặt giá trị tích phân không dấu thành giá trị lớn nhất của nó, hàm sẽ trả về kết quả của phép cộng nếu không có dấu và trả về giá trị lớn nhất của giá trị tích phân đã chọn nếu có dấu.


1
Bạn có thể muốn đề cập rằng câu trả lời này dành cho một kiến ​​trúc tập lệnh cụ thể (x86?) Và sẽ yêu cầu thực hiện lại cho từng kiến ​​trúc đích (SPARC, MIPS, ARM, v.v.)
Toby Speight

2

cái này thì sao:

bsum = a + b;
bsum = (bsum < a || bsum < b) ? 255 : bsum;

bsub = a - b;
bsub = (bsub > a || bsub > b) ? 0 : bsub;

Tôi đã sửa lỗi đánh máy (rõ ràng?), Nhưng tôi vẫn không nghĩ điều này là chính xác.
Bathsheba

Điều này cũng bao gồm sự phân nhánh.
fuz

Tôi sẽ xóa câu trả lời này chỉ là một câu hỏi nhanh trong hợp ngữ mà không cần tối ưu hóa sự khác biệt giữa toán tử bậc ba và câu lệnh if / else là gì?

@GRC Không có sự khác biệt.
fuz

@GRC FUZxxl đúng, nhưng, như mọi khi, hãy cố gắng bản thân. Ngay cả khi bạn không biết lắp ráp (bạn có thể đặt câu hỏi ở đây trên SO nếu điều gì đó không rõ ràng với bạn), chỉ cần kiểm tra độ dài / hướng dẫn bạn sẽ biết.
edmz

2

Tất cả có thể được thực hiện trong số học byte không dấu

// Addition without overflow
return (b > 255 - a) ? 255 : a + b

// Subtraction without underflow
return (b > a) ? 0 : a - b;

1
Đây thực sự là một trong những giải pháp tốt nhất. Tất cả những người khác tạo phân số hoặc phép cộng trước đây thực sự đang tạo ra một hành vi không xác định trong C ++, dẫn đến việc trình biên dịch có thể làm bất cứ điều gì nó muốn. Trong thực tế, bạn hầu như có thể dự đoán điều gì sẽ xảy ra, nhưng vẫn còn.
Adrien Hamelin

2

Nếu bạn muốn thực hiện việc này với hai byte, hãy sử dụng mã đơn giản nhất có thể.

Nếu bạn muốn thực hiện việc này với hai mươi tỷ byte, hãy kiểm tra xem những hướng dẫn vectơ nào có sẵn trên bộ xử lý của bạn và liệu chúng có thể được sử dụng hay không. Bạn có thể thấy rằng bộ xử lý của bạn có thể thực hiện 32 thao tác này với một lệnh duy nhất.


2

Bạn cũng có thể sử dụng thư viện số an toàn tại Vườn ươm Thư viện Boost . Nó cung cấp các thay thế thả vào cho int, long, v.v., đảm bảo rằng bạn sẽ không bao giờ bị tràn, dòng dưới, v.v. không bị phát hiện.


7
Cung cấp một ví dụ về cách sử dụng thư viện sẽ làm cho câu trả lời này tốt hơn. Ngoài ra, họ có cung cấp một đảm bảo không brachless?
Shafik Yaghmour

Thư viện có nhiều tài liệu và ví dụ. Nhưng cuối cùng, nó dễ dàng như bao gồm tiêu đề thích hợp và thay thế <int> an toàn cho int.
Robert Ramey

không nhánh? Tôi đoán bạn là người đàn ông không nhánh. Thư viện sử dụng lập trình ẩn mẫu để chỉ kiểm tra thời gian chạy khi cần thiết. Ví dụ: unsigned char lần unsigned char sẽ dẫn đến unsigned int. Điều này không bao giờ có thể bị tràn nên không cần kiểm tra gì cả. Mặt khác, thời gian chưa được đánh dấu chưa được đánh dấu có thể bị tràn vì vậy nó phải được kiểm tra trong thời gian chạy.
Robert Ramey

1

Nếu bạn gọi các phương thức đó nhiều, thì cách nhanh nhất sẽ không phải là thao tác bit mà có lẽ là bảng tra cứu. Xác định một mảng có độ dài 511 cho mỗi phép toán. Ví dụ cho phép trừ (phép trừ)

static unsigned char   maxTable[511];
memset(maxTable, 0, 255);           // If smaller, emulates cutoff at zero
maxTable[255]=0;                    // If equal     - return zero
for (int i=0; i<256; i++)
    maxTable[255+i] = i;            // If greater   - return the difference

Mảng là tĩnh và chỉ được khởi tạo một lần. Bây giờ phép trừ của bạn có thể được định nghĩa là phương pháp nội tuyến hoặc sử dụng trình biên dịch trước:

#define MINUS(A,B)    maxTable[A-B+255];

Làm thế nào nó hoạt động? Bạn muốn tính trước tất cả các phép trừ có thể có cho các ký tự không dấu. Kết quả thay đổi từ -255 đến +255, tổng số 511 kết quả khác nhau. Chúng tôi xác định một mảng tất cả các kết quả có thể có nhưng vì trong C, chúng tôi không thể truy cập nó từ các chỉ số âm nên chúng tôi sử dụng +255 (trong [A-B + 255]). Bạn có thể loại bỏ hành động này bằng cách xác định một con trỏ ở giữa mảng.

const unsigned char *result = maxTable+255;
#define MINUS(A,B)    result[A-B];

sử dụng nó như:

bsub  = MINUS(13,15); // i.e 13-15 with zero cutoff as requested

Lưu ý rằng quá trình thực hiện cực kỳ nhanh chóng. Chỉ một phép trừ và một sai lệch con trỏ để nhận được kết quả. Không phân nhánh. Các mảng tĩnh rất ngắn nên chúng sẽ được tải đầy đủ vào bộ nhớ đệm của CPU để tăng tốc độ tính toán hơn nữa

Tương tự sẽ hoạt động đối với phép cộng nhưng với một bảng khác một chút (256 phần tử đầu tiên sẽ là chỉ số và 255 phần tử cuối cùng sẽ bằng 255 để mô phỏng mức giới hạn vượt quá 255.

Nếu bạn nhấn mạnh vào phép toán bit, các câu trả lời sử dụng (a> b) là sai. Điều này vẫn có thể được triển khai dưới dạng phân nhánh. Sử dụng kỹ thuật bit ký hiệu

// (num1>num2) ? 1 : 0
#define        is_int_biggerNotEqual( num1,num2) ((((__int32)((num2)-(num1)))&0x80000000)>>31)

Bây giờ bạn có thể sử dụng nó để tính trừ và cộng.

Nếu bạn muốn mô phỏng các hàm max (), min () mà không sử dụng phân nhánh:

inline __int32 MIN_INT(__int32 x, __int32 y){   __int32 d=x-y; return y+(d&(d>>31)); }              

inline __int32 MAX_INT(__int32 x, __int32 y){   __int32 d=x-y; return x-(d&(d>>31)); }

Các ví dụ của tôi ở trên sử dụng số nguyên 32 bit. Bạn có thể thay đổi nó thành 64, mặc dù tôi tin rằng các phép tính 32 bit chạy nhanh hơn một chút. Tùy thuộc vào bạn


2
Nó có thể sẽ không, trên thực tế: đầu tiên, tất nhiên, việc tải lên bảng chậm. Các hoạt động bit mất 1 chu kỳ, tải từ bộ nhớ mất khoảng 80 ns; ngay cả từ bộ nhớ đệm L1, chúng tôi đang ở trong khoảng 20 ns, tức là gần 7 chu kỳ trên một CPU 3GHz.
edmz

Bạn không hoàn toàn chính xác. Phương thức LUT sẽ sử dụng một vài vòng kết nối nhưng thao tác bit cũng không phải là một chu trình đơn lẻ. Có một vài hành động tuần tự. Ví dụ, chỉ tính toán MAX () yêu cầu 2 phép trừ và phép toán logic và một dịch chuyển phải. Và đừng quên thăng hạng / cách chức số nguyên
DanielHsH

1
Tôi muốn nói rằng các hoạt động bitwise đơn mất 1 chu kỳ, tự nhiên giả sử các toán hạng thanh ghi. Với đoạn mã mà Shafik đưa ra, tiếng clang xuất ra 4 lệnh cơ bản. Ngoài ra (x > y), là không nhánh.
edmz

Đầu tiên, (x> y) có thể sử dụng phân nhánh. Bạn không biết mình đang chạy trên kiến ​​trúc nào. Tôi có xu hướng đồng ý rằng nó có thể không phân nhánh trên kiến ​​trúc Intel. Hầu hết các điện thoại thông minh không phải của Intel. Đó cũng là lý do mà bạn không thể biết sẽ có bao nhiêu hướng dẫn lắp ráp. Hãy thử giải pháp của tôi trên PC của bạn. Tôi muốn nghe kết quả.
DanielHsH

1
Bộ nhớ đệm L1 nhanh hơn nhiều so với 20 ns, theo thứ tự có thể là 4 chu kỳ xử lý. Và có khả năng sẽ sử dụng một đơn vị thực thi không được sử dụng khác, và dù sao cũng sẽ được kết nối đầy đủ. Đo lường nó. Và 20ns là 60 chu kỳ trong CPU 3 GHz.
gnasher729
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.