ข้ามไปที่เนื้อหา
บทที่ 4อ่านประมาณ 18 นาที

การบวกและการลบเลข Floating-Point

เรียนจบบทนี้แล้วจะ

  • เข้าใจว่าทำไมบวกเลข Floating-Point ตรง ๆ ไม่ได้
  • อธิบายขั้นตอนหลักของการบวกและลบได้
  • ติดตามขั้นตอนระหว่างการคำนวณได้
  • เห็นว่าการปัดเศษทำให้ผลต่างจากคำตอบแม่นยำได้อย่างไร
หัวข้อในบทนี้

ทำไมบวกเลข Floating-Point ตรง ๆ ไม่ได้

ลองบวกเลขฐานสิบแบบ scientific notation: 1.5 × 10³ + 2.0 × 10¹ จะเอา 1.5 + 2.0 ไม่ได้ เพราะเลขชี้กำลังไม่เท่ากัน ต้องปรับให้เป็น ×10³ ก่อน:

1.5 × 103 + 0.02 × 103 = 1.52 × 103

Floating-Point ก็เหมือนกัน ต้องทำให้ Exponent เท่ากันก่อน โดยเลื่อน Significand ของตัวที่ Exponent น้อยกว่าไปทางขวา แล้วจึงบวกหรือลบ Significand

7 ขั้นของการบวก/ลบ Floating-Point

  1. encode ตัวเลขทั้งสองเป็น IEEE 754 (ใช้ค่าที่เก็บจริง ไม่ใช่ค่าที่พิมพ์)
  2. แยก Sign, Exponent และ (เติม hidden bit)
  3. เทียบ Exponent แล้วเลื่อน Significand ของตัวที่ Exponent น้อยกว่าไปทางขวาจนเท่ากัน
  4. บวก Significand ถ้า Sign เหมือนกัน หรือลบถ้า Sign ต่างกัน (การลบ A − B คือ A + (−B) จึงกลับ Sign ของ B ก่อน)
  5. Normalize: ถ้าผล ≥ 2 เลื่อนขวา 1 บิตแล้ว Exponent + 1 · ถ้าบิตหน้าเป็น 0 (ลบกันแล้วเหลือน้อย) เลื่อนซ้ายจนได้ 1.xxx
  6. ปัดเศษให้เหลือ 24 บิตด้วย Guard, Round, Sticky
  7. encode ผลลัพธ์เป็น 32 บิต

แบบจำลองเพื่อการเรียนรู้

ขั้นตอนในบทนี้เป็นแบบจำลองของขั้นหลักเพื่อให้เข้าใจหลักการ ไม่ใช่รายละเอียดของวงจรบวกเลขในฮาร์ดแวร์ทุกแบบ แต่ผลลัพธ์ทุกบิตตรงกับมาตรฐาน IEEE 754

Guard, Round และ Sticky

ตอนเลื่อน Significand ไปทางขวา บิตบางส่วนจะเลยตำแหน่งสุดท้ายที่เก็บได้ ถ้าทิ้งไปเลยจะปัดผิด ฮาร์ดแวร์จึงเก็บบิตเพิ่ม 3 ช่อง เรียกว่า :

  • Guard (G): บิตแรกถัดจากบิตสุดท้ายที่เก็บได้ (มีค่าครึ่งหนึ่งของบิตสุดท้าย)
  • Round (R): บิตถัดจาก Guard
  • Sticky (S): OR ของบิตที่เหลือทั้งหมด ถ้ามีบิต 1 หลุดออกไปแม้แต่ตัวเดียว S = 1

3 บิตนี้พอสำหรับปัดเศษให้ได้ผลเหมือนคำนวณแบบแม่นยำแล้วปัดครั้งเดียว

Round to nearest, ties to even

IEEE 754 ปัดแบบ เป็นค่าเริ่มต้น ดูจาก G, R, S:

GR หรือ Sความหมายปัด
0อะไรก็ได้ส่วนที่ทิ้งน้อยกว่าครึ่งลง (ตัดทิ้ง)
1มี 1ส่วนที่ทิ้งมากกว่าครึ่งขึ้น (+1 ที่บิตสุดท้าย)
10 ทั้งคู่กึ่งกลางพอดี (tie)เลือกให้บิตสุดท้ายเป็น 0 (เลขคู่)

ที่ต้องเลือกเลขคู่ตอนเสมอ เพราะถ้าปัดขึ้นทุกครั้ง ผลรวมของการคำนวณจำนวนมากจะเอียงไปทางมากขึ้นเรื่อย ๆ การเลือกเลขคู่ทำให้ปัดขึ้นกับปัดลงพอ ๆ กัน

ตัวอย่าง A1–A6

ตัวเลขทุกตัวด้านล่างคำนวณสดจาก engine (กดตัวอย่างใน Simulator ด้านล่างเพื่อดูทีละขั้น) ตัวอย่างที่น่าสนใจที่สุดคือ A2: 0.1 + 0.2 ไม่ได้ 0.3 พอดี ผลแม่นยำของค่าที่เก็บจริงคือ 0.300000004470348358154296875 แล้วถูกปัดเป็น 0.3

A10.5 + -0.4375= 0x3D800000 = 0.0625ลบกันแล้วต้อง Normalize ซ้าย 3 บิต ไม่ต้องปัด
A20.1 + 0.2= 0x3E99999A = 0.3ผลไม่ใช่ 0.3 พอดี เพราะ 0.1 และ 0.2 เก็บแบบประมาณ แล้วยังต้องปัดขึ้นอีก
A31 + 1e-8= 0x3F800000 = 1ตัวเล็กถูกเลื่อนจนหลุดหมด เหลือแค่ Sticky จึงหายไปทั้งตัว
A416777216 + 1= 0x4B800000 = 16777216กึ่งกลางพอดี (tie) และบิตสุดท้ายเป็น 0 อยู่แล้ว จึงปัดลง
A516777216 + 3= 0x4B800002 = 16777220กึ่งกลางพอดี (tie) แต่บิตสุดท้ายเป็น 1 จึงปัดขึ้นให้เป็นเลขคู่
A61.0000001 − 1= 0x34000000 = 1.1920929 × 10−7ลบค่าที่ใกล้กันมาก บิตหน้าหายไปเยอะ ต้อง Normalize ซ้ายหลายบิต

Floating-Point Simulator: บวกและลบ

พิมพ์ตัวเลข 2 ค่าแล้วเลือก + หรือ − เครื่องมือจะเก็บทั้งสองค่าเป็น Single Precision ก่อน แล้วคำนวณตามกฎของ Floating-Point ทีละขั้น (ไม่ได้คำนวณคำตอบแม่นยำแล้วค่อยแปลง) กดกล่องใน flowchart เพื่อกระโดดไปขั้นนั้นได้ ในขั้นเลื่อนให้ Exponent เท่ากัน ลองกด “เลื่อนทีละบิต”

พิมพ์ทศนิยมหรือ scientific notation เช่น 0.1, -6.5, 1e-8 หรือ inf, nan · ใช้จุดเป็นจุดทศนิยม

ตัวอย่าง

A = 0.10x3DCCCCCD
S1 บิต
Exponent8 บิต
Fraction23 บิต

เก็บได้ไม่พอดี ค่าที่เก็บจริงคือ 0.10000000149011611938…

B = 0.20x3E4CCCCD
S1 บิต
Exponent8 บิต
Fraction23 บิต

เก็บได้ไม่พอดี ค่าที่เก็บจริงคือ 0.20000000298023223876…

  • Sign
  • Exponent
  • Fraction
  • Hidden bit
  • Guard / Round / Sticky

1/8แยกส่วนของ A

ขั้นที่ 1 จาก 8: แยกส่วนของ A

ตัวถูกดำเนินการ A0x3DCCCCCD
S1 บิต
Exponent8 บิต
Fraction23 บิต

ประเภท: Normal · ค่าที่เก็บจริง: 0.100000001490116119384765625

Sign = 0 · Biased exponent = 123 (Exponent จริง −4)

Significand = 1.10011001100110011001101

ขั้นที่ 1/8

ผลลัพธ์ 0.1 + 0.2

ผลแม่นยำของค่าที่พิมพ์
0.3
ผลแม่นยำของค่าที่เก็บจริงใน float
0.300000004470348358154296875
ผล float (binary32)
0.30x3E99999Aเก็บจริง: 0.300000011920928955078125

ผลต่างจากค่าที่พิมพ์: 1.1920928955… × 10−8

  • Inexact (ปัดเศษแล้วไม่ตรง)

สรุปท้ายบท

  • ต้องปรับ Exponent ให้เท่ากันก่อน โดยเลื่อน Significand ของตัวที่ Exponent น้อยกว่าไปทางขวา
  • Sign เหมือนกันให้บวก Significand ต่างกันให้ลบ ผลใช้ Sign ของตัวที่มีขนาดใหญ่กว่า (A − B = A + (−B))
  • Normalize: ผล ≥ 2 เลื่อนขวา 1 บิต · ลบกันจนบิตหน้าหาย เลื่อนซ้ายได้หลายบิต แล้วปรับ Exponent ตาม
  • ปัดเศษด้วย Guard, Round, Sticky แบบ round to nearest, ties to even
  • ความแม่นยำจำกัดทำให้ผลต่างจากคำตอบแม่นยำได้ เช่น 0.1 + 0.2 ไม่ได้ 0.3 พอดี และ 1 + 1e-8 ได้ 1 (ตัวเล็กหายไปทั้งตัว)