Một phương pháp nhanh để làm tròn gấp đôi thành int 32 bit


169

Khi đọc mã nguồn của Lua , tôi nhận thấy rằng Lua sử dụng a macrođể làm tròn từ a doubleđến 32 bit int. Tôi đã trích xuất macro, và nó trông như thế này:

union i_cast {double d; int i[2]};
#define double2int(i, d, t)  \
    {volatile union i_cast u; u.d = (d) + 6755399441055744.0; \
    (i) = (t)u.i[ENDIANLOC];}

Ở đây ENDIANLOCđược định nghĩa là endianness , 0cho endian nhỏ, 1cho endian lớn. Lua cẩn thận xử lý endianness. tlà viết tắt của kiểu số nguyên, như inthoặc unsigned int.

Tôi đã làm một nghiên cứu nhỏ và có một định dạng đơn giản hơn macrosử dụng cùng một suy nghĩ:

#define double2int(i, d) \
    {double t = ((d) + 6755399441055744.0); i = *((int *)(&t));}

Hoặc theo kiểu C ++:

inline int double2int(double d)
{
    d += 6755399441055744.0;
    return reinterpret_cast<int&>(d);
}

Thủ thuật này có thể hoạt động trên mọi máy sử dụng IEEE 754 (có nghĩa là khá nhiều máy hiện nay). Nó hoạt động cho cả số dương và số âm và làm tròn theo Quy tắc của Ngân hàng . (Điều này không gây ngạc nhiên, vì nó tuân theo IEEE 754.)

Tôi đã viết một chương trình nhỏ để kiểm tra nó:

int main()
{
    double d = -12345678.9;
    int i;
    double2int(i, d)
    printf("%d\n", i);
    return 0;
}

Và nó xuất ra -12345679, như mong đợi.

Tôi muốn đi vào chi tiết cách thức macrohoạt động của mánh khóe này . Số ma thuật 6755399441055744.0thực sự là 2^51 + 2^52, hoặc 1.5 * 2^52, và 1.5trong nhị phân có thể được biểu diễn dưới dạng 1.1. Khi bất kỳ số nguyên 32 bit nào được thêm vào số ma thuật này, tôi sẽ bị mất từ ​​đây. Thủ thuật này hoạt động như thế nào?

PS: Đây là mã nguồn Lua, Llimits.h .

CẬP NHẬT :

  1. Như @Mysticial chỉ ra, phương pháp này không giới hạn ở mức 32 bit int, nó cũng có thể được mở rộng thành 64 bit intmiễn là con số nằm trong phạm vi 2 ^ 52. ( macroCần một số sửa đổi.)
  2. Một số tài liệu nói rằng phương pháp này không thể được sử dụng trong Direct3D .
  3. Khi làm việc với trình biên dịch Microsoft cho x86, thậm chí còn macrođược viết nhanh hơn assembly(điều này cũng được trích xuất từ ​​nguồn Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. Có một số ma thuật tương tự cho số chính xác duy nhất: 1.5 * 2 ^23


3
"Nhanh" so với cái gì?
Cory Nelson

3
@CoryNelson Nhanh so với dàn diễn viên đơn giản. Phương pháp này, khi được thực hiện đúng cách (với nội tại SSE) nhanh hơn hàng trăm lần so với diễn viên. (gọi một hàm gọi khó chịu đến một mã chuyển đổi khá đắt tiền)
Mystical

2
Phải - tôi có thể thấy nó nhanh hơn ftoi. Nhưng nếu bạn đang nói SSE, tại sao không sử dụng chỉ dẫn duy nhất CVTTSD2SI?
Cory Nelson

3
@tmyklebu Nhiều trường hợp sử dụng double -> int64thực sự nằm trong 2^52phạm vi. Điều này đặc biệt phổ biến khi thực hiện các kết hợp số nguyên bằng cách sử dụng các FFT dấu phẩy động.
Bí ẩn

7
@MSalters Không nhất thiết phải đúng. Một diễn viên phải sống theo đặc điểm kỹ thuật của ngôn ngữ - bao gồm xử lý đúng các trường hợp tràn và NAN. (hoặc bất cứ điều gì trình biên dịch chỉ định trong trường hợp IB hoặc UB) Những kiểm tra này có xu hướng rất tốn kém. Thủ thuật được đề cập trong câu hỏi này hoàn toàn bỏ qua các trường hợp góc như vậy. Vì vậy, nếu bạn muốn tốc độ và ứng dụng của bạn không quan tâm (hoặc không bao giờ gặp phải) các trường hợp góc như vậy, thì cách hack này là hoàn toàn phù hợp.
Bí ẩn

Câu trả lời:


161

A doubleđược biểu diễn như thế này:

đại diện kép

và nó có thể được xem như hai số nguyên 32 bit; bây giờ, intđược lấy trong tất cả các phiên bản mã của bạn (giả sử là 32 bit int) là cái ở bên phải trong hình, do đó, những gì bạn đang làm cuối cùng chỉ là lấy 32 bit mantissa thấp nhất.


Bây giờ, đến số ma thuật; như bạn đã nói chính xác, 6755399441055744 là 2 ^ 51 + 2 ^ 52; việc thêm một số như vậy buộc doublephải đi vào "phạm vi ngọt ngào" giữa 2 ^ 52 và 2 ^ 53, như được giải thích bởi Wikipedia ở đây , có một tính chất thú vị:

Trong khoảng từ 2 52 = 4,503,599,627,370,496 và 2 53 = 9,007,199,254,740,992 các số có thể biểu diễn chính xác là các số nguyên

Điều này xuất phát từ thực tế là lớp phủ rộng 52 bit.

Một sự thật thú vị khác về việc thêm 2 51 +2 52 là nó chỉ ảnh hưởng đến lớp phủ ở hai bit cao nhất - dù sao cũng bị loại bỏ, vì chúng ta chỉ lấy 32 bit thấp nhất.


Cuối cùng nhưng không kém phần quan trọng: dấu hiệu.

Điểm nổi IEEE 754 sử dụng biểu diễn cường độ và ký hiệu, trong khi số nguyên trên các máy "bình thường" sử dụng số học bổ sung của 2; Làm thế nào điều này được xử lý ở đây?

Chúng tôi chỉ nói về số nguyên dương; bây giờ giả sử chúng ta đang xử lý một số âm trong phạm vi có thể biểu thị bằng 32 bit int, do đó ít hơn (về giá trị tuyệt đối) so với (-2 ^ 31 + 1); gọi nó -a. Một số như vậy rõ ràng được thực hiện tích cực bằng cách thêm số ma thuật và giá trị kết quả là 2 52 +2 51 + (- a).

Bây giờ, chúng ta sẽ nhận được gì nếu chúng ta diễn giải mantissa trong biểu diễn bổ sung của 2? Nó phải là kết quả của tổng cộng 2 của (2 52 +2 51 ) và (-a). Một lần nữa, thuật ngữ đầu tiên chỉ ảnh hưởng đến hai bit trên, phần còn lại trong các bit 0 ~ 50 là biểu diễn phần bù 2 của (-a) (một lần nữa, trừ hai bit trên).

Do việc giảm số bổ sung 2 xuống chiều rộng nhỏ hơn được thực hiện chỉ bằng cách cắt đi các bit thừa ở bên trái, việc lấy 32 bit thấp hơn sẽ cho chúng ta chính xác (-a) trong 32 bit, số học bổ sung của 2 bit.


"" "Một sự thật thú vị khác về việc thêm 2 ^ 51 + 2 ^ 52 là nó chỉ ảnh hưởng đến lớp phủ ở hai bit cao nhất - dù sao cũng bị loại bỏ, vì chúng ta chỉ lấy 32 bit thấp nhất của nó" "" Đó là gì? Thêm điều này có thể thay đổi tất cả các lớp phủ!
YvesgereY

@ John: tất nhiên, toàn bộ quan điểm của việc thêm chúng là buộc giá trị nằm trong phạm vi đó, điều này rõ ràng có thể dẫn đến thay đổi mantissa (giữa những thứ khác) so với giá trị ban đầu. Điều tôi đã nói ở đây là, một khi bạn ở trong phạm vi đó, các bit duy nhất khác với số nguyên 53 bit tương ứng là bit 51 và 52, dù sao cũng bị loại bỏ.
Matteo Italia

2
Đối với những người muốn chuyển đổi sang int64_tbạn có thể làm điều đó bằng cách dịch chuyển lớp phủ sang trái và sau đó phải 13 bit. Điều này sẽ xóa số mũ và hai bit khỏi số 'ma thuật', nhưng sẽ giữ và truyền dấu hiệu cho toàn bộ số nguyên có chữ ký 64 bit. union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
Wojciech Migda
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.