วิธีที่รวดเร็วในการปัด double เป็น 32-bit int อธิบาย


169

เมื่ออ่านLua ของรหัสที่มาผมสังเกตเห็นว่า Lua ใช้macroในการออกรอบdoubleกับ int32 ฉันดึงข้อมูลmacroและดูเหมือนว่า:

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

ที่นี่ENDIANLOCถูกกำหนดให้เป็นendianness , 0สำหรับ endian เล็ก ๆ น้อย ๆ1สำหรับ endian ใหญ่ Lua จัดการกับ endianness อย่างระมัดระวัง tย่อมาจากชนิดจำนวนเต็มเช่นหรือintunsigned int

ฉันทำวิจัยเล็กน้อยและมีรูปแบบที่ง่ายกว่าmacroซึ่งใช้ความคิดเดียวกัน:

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

หรือในสไตล์ C ++:

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

เคล็ดลับนี้สามารถทำงานกับเครื่องใด ๆ ที่ใช้IEEE 754 (ซึ่งหมายถึงทุกเครื่องในปัจจุบัน) มันทำงานสำหรับตัวเลขทั้งบวกและลบและการปัดเศษตามกฎของธนาคาร (สิ่งนี้ไม่น่าแปลกใจเนื่องจากเป็นไปตาม IEEE 754)

ฉันเขียนโปรแกรมเล็ก ๆ เพื่อทดสอบ:

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

และมันจะออกผลลัพธ์ -12345679 ตามที่คาดไว้

ฉันต้องการทราบรายละเอียดวิธีการmacroทำงานที่ซับซ้อนนี้ จำนวนมายากล6755399441055744.0เป็นจริง2^51 + 2^52หรือ1.5 * 2^52และในไบนารีสามารถแสดงเป็น1.5 1.1เมื่อเลขจำนวนเต็ม 32 บิตใด ๆ ถูกเพิ่มเข้าไปในจำนวนเวทย์มนตร์นี้ก็หายไปจากที่นี่ เคล็ดลับนี้ทำงานอย่างไร

PS: นี่คือในรหัสที่มา Lua, Llimits.h

อัปเดต :

  1. เนื่องจาก @Mysticial ชี้ให้เห็นวิธีการนี้ไม่ได้ จำกัด อยู่ที่ 32 บิตintแต่ก็สามารถขยายได้ถึง 64 บิตintตราบใดที่ตัวเลขนั้นอยู่ในช่วง 2 ^ 52 ( macroความต้องการการปรับเปลี่ยนบางอย่าง)
  2. วัสดุบางอย่างบอกว่าวิธีนี้ไม่สามารถใช้ในDirect3Dได้
  3. เมื่อทำงานกับ Microsoft Assembler สำหรับ x86 จะมีการmacroเขียนที่เร็วขึ้นassembly(สิ่งนี้ยังแยกมาจากแหล่ง Lua):

    #define double2int(i,n)  __asm {__asm fld n   __asm fistp i}
  4. มีหมายเลขเวทย์มนตร์ที่คล้ายกันสำหรับหมายเลขความแม่นยำเดียว: 1.5 * 2 ^23


3
"เร็ว" เมื่อเทียบกับอะไร
คอรีเนลสัน

3
@CoryNelson รวดเร็วเทียบกับนักแสดงที่เรียบง่าย วิธีนี้เมื่อใช้งานอย่างถูกต้อง (กับ SSE อินทริน) ค่อนข้างเร็วกว่าการร่ายอย่างมากร้อยเท่า (ซึ่งเรียกการเรียกใช้ฟังก์ชันที่น่ารังเกียจไปยังรหัสการแปลงที่ค่อนข้างแพง)
Mysticial

2
ขวา - ftoiฉันสามารถดูได้ว่ามันจะเร็วกว่า แต่ถ้าคุณกำลังพูดถึง SSE ทำไมไม่ใช้แค่คำสั่งเดียวCVTTSD2SI?
คอรีเนลสัน

3
@tmyklebu หลายกรณีการใช้งานที่ไปdouble -> int64แน่นอนอยู่ใน2^52ช่วง สิ่งเหล่านี้เป็นเรื่องปกติโดยเฉพาะอย่างยิ่งเมื่อดำเนินการโน้มน้าวใจจำนวนเต็มโดยใช้ FFT ที่เป็นทศนิยม
ลึกลับ

7
@MSalters ไม่จำเป็นต้องเป็นเรื่องจริง นักแสดงจะต้องมีคุณสมบัติตรงตามข้อกำหนดของภาษา - รวมถึงการจัดการกรณีล้นและ NAN อย่างเหมาะสม (หรืออะไรก็ตามที่คอมไพเลอร์ระบุในกรณี IB หรือ UB) การตรวจสอบเหล่านี้มีแนวโน้มที่จะมีราคาแพงมาก เคล็ดลับที่กล่าวถึงในคำถามนี้ละเว้นกรณีมุมอย่างสมบูรณ์ ดังนั้นหากคุณต้องการความเร็วและแอปพลิเคชั่นของคุณไม่สนใจ (หรือไม่เคยเจอ) กรณีมุมเช่นนี้การแฮ็คนี้เหมาะสมอย่างยิ่ง
ลึกลับ

คำตอบ:


161

A doubleแสดงแบบนี้:

ตัวแทนสอง

และมันสามารถถูกมองว่าเป็นจำนวนเต็ม 32 บิตสองตัว ตอนนี้intโค้ดของคุณในทุกเวอร์ชั่น (สมมติว่ามันเป็น 32- บิตint) คืออันที่อยู่ทางขวาของรูปดังนั้นสิ่งที่คุณทำในตอนท้ายก็คือการใช้ mantissa 32 บิตที่ต่ำที่สุด


ตอนนี้ถึงจำนวนเวทมนตร์; ตามที่คุณระบุไว้อย่างถูกต้อง 6755399441055744 คือ 2 ^ 51 + 2 ^ 52; การเพิ่มจำนวนดังกล่าวบังคับdoubleให้เข้าไปใน "ช่วงหวาน" ระหว่าง 2 ^ 52 และ 2 ^ 53 ซึ่งตามที่อธิบายโดย Wikipedia ที่นี่มีคุณสมบัติที่น่าสนใจ:

ระหว่าง 2 52 = 4,503,599,627,370,496 และ 2 53 = 9,007,199,254,740,992 ตัวเลขที่สามารถแทนได้นั้นเป็นจำนวนเต็ม

สิ่งนี้ตามมาจากความจริงที่ว่า mantissa นั้นกว้าง 52 บิต

ข้อเท็จจริงที่น่าสนใจอีกข้อเกี่ยวกับการเพิ่ม 2 51 +2 52ก็คือมันมีผลต่อแมนทิสซาเฉพาะในบิตสูงสุดสองบิตเท่านั้นซึ่งถูกทิ้งอยู่แล้วเนื่องจากเราใช้ 32 บิตที่ต่ำที่สุดเท่านั้น


สุดท้าย แต่ไม่ท้ายสุด: เป็นสัญญาณ

จุดลอยตัว IEEE 754 ใช้ขนาดและการแสดงสัญลักษณ์ในขณะที่จำนวนเต็มบนเครื่อง "ปกติ" ใช้เลขคณิตประกอบ 2 ของ นี่จัดการได้อย่างไรที่นี่?

เราพูดถึงจำนวนเต็มบวกเท่านั้น ตอนนี้สมมติว่าเรากำลังจัดการกับจำนวนลบในช่วงที่สามารถแทนได้ด้วย 32- บิตintดังนั้นน้อยกว่า (ในค่าสัมบูรณ์) กว่า (-2 ^ 31 + 1); -aเรียกมันว่า เห็นได้ชัดว่าจำนวนดังกล่าวเป็นบวกโดยการเพิ่มจำนวนเวทย์มนตร์และค่าที่ได้คือ 2 52 +2 51 + (- a)

ทีนี้เราจะได้อะไรถ้าเราตีความ mantissa ในส่วนที่สมบูรณ์ของ 2? จะต้องเป็นผลมาจากผลรวม 2 ของ (2 52 +2 51 ) และ (-a) อีกครั้งในระยะแรกส่งผลกระทบเพียงสองบิตบนสิ่งที่เหลืออยู่ในบิต 0 ~ 50 คือการเป็นตัวแทนที่สมบูรณ์ของ 2 (-a) (อีกครั้งลบด้วยสองบิตบน)

เนื่องจากการลดจำนวนส่วนประกอบ 2 ลงเหลือความกว้างที่เล็กลงทำได้โดยการตัดบิตพิเศษทางซ้ายการตัด 32 บิตด้านล่างทำให้เราได้อย่างถูกต้อง (-a) ใน 32 บิตซึ่งเป็นส่วนประกอบทางคณิตศาสตร์ของ 2


"" "ความจริงที่น่าสนใจอีกประการเกี่ยวกับการเพิ่ม 2 ^ 51 + 2 ^ 52 คือมันส่งผลกระทบต่อแมนทิสซาเฉพาะในบิตสูงสุดสองบิตเท่านั้น - ซึ่งถูกทิ้งอยู่แล้ว การเพิ่มสิ่งนี้อาจเปลี่ยนแมนติสซาทั้งหมด!
YvesgereY

@ จอห์น: แน่นอนว่าจุดรวมของพวกเขาคือการบังคับให้ค่าอยู่ในช่วงนั้นซึ่งเห็นได้ชัดว่าอาจส่งผลให้เปลี่ยนแมนทิสซา (ระหว่างสิ่งอื่น ๆ ) ในแง่ของค่าเดิม สิ่งที่ฉันพูดที่นี่คือเมื่อคุณอยู่ในช่วงนั้นบิตเดียวที่แตกต่างจากจำนวน 53 บิตที่สอดคล้องกันคือบิต 51 และ 52 ซึ่งถูกทิ้งอยู่แล้ว
Matteo Italia

2
สำหรับผู้ที่ต้องการแปลงเป็นint64_tคุณสามารถทำได้โดยการขยับแมนทิสซาไปทางซ้ายและจากนั้น 13 บิต สิ่งนี้จะล้างเลขชี้กำลังและสองบิตออกจากหมายเลข 'วิเศษ' แต่จะเก็บและเผยแพร่สัญลักษณ์เป็นจำนวนเต็ม 64 บิตที่เซ็นชื่อทั้งหมด union { double d; int64_t l; } magic; magic.d = input + 6755399441055744.0; magic.l <<= 13; magic.l >>= 13;
วอย Migch
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.