IEEE 754 — Floating-Point-এর Bit-বাই-Bit ব্যাকরণ
IEEE 754 Floating-Point Standard
একটা 32-বিট বা 64-বিট pattern-কে sign, biased exponent, আর implicit-leading-1 mantissa-তে ভাগ করে ঠিক কীভাবে সংখ্যায় রূপান্তর করা হয় — আর হাতে-কলমে দেখানো, কেন 0.1 store করলে যা জমা থাকে তা ঠিক 0.1 নয়।
আগে এটা বুঝি
একটা চ্যালেঞ্জ দিয়ে শুরু করি। এই ৩২-বিট pattern-টা একটা float:
0 01111011 10011001100110011001101সংখ্যাটা কত? এই লেসনের শেষে আপনি এটা হাতে বের করতে পারবেন — আর দেখবেন সংখ্যাটা এমন একটা মান যা আপনি প্রতিদিন লেখেন, তবু bit হিসেবে এটা যা প্রতিনিধিত্ব করে সেটা সেই মানের সাথে হুবহু মেলে না।
গত লেসনে আমরা floating-point-এর ধারণাটা দেখেছি — sign × significand × 2^exponent, scientific notation-এর বাইনারি
সংস্করণ। কিন্তু ধারণা আর engineering-এর মাঝে একটা বিশাল ফাঁক
আছে: ঠিক কয়টা বিট sign-এ, কয়টা exponent-এ, কয়টা
significand-এ? Exponent negative হলে কীভাবে লেখা হয়? আর
0, ∞, “সংজ্ঞাহীন” (0/0)-এর মতো বিশেষ মান কীভাবে
প্রতিনিধিত্ব হয়?
১৯৮৫-এর আগে প্রতিটা hardware vendor এই প্রশ্নগুলোর নিজস্ব উত্তর
দিত — আর ফলাফল ছিল বিশৃঙ্খলা (গত লেসনের ইতিহাস অংশ মনে করুন)।
IEEE 754 এই সবগুলো প্রশ্নের একটাই, সার্বজনীন উত্তর দেয়। আজ
পৃথিবীর প্রায় প্রতিটা CPU, GPU, প্রতিটা ভাষার float/double
— সবাই ঠিক এই নিয়মে চলে। এই লেসনটা শেষ হলে আপনি জানবেন ঠিক
কী ঘটে যখন আপনি লেখেন float x = 0.1f; — শুধু ধারণাগতভাবে নয়,
বিট-বাই-বিট।
মূল ধারণা
সম্পূর্ণ bit layout
IEEE 754 তিনটা field সংজ্ঞায়িত করে, পাশাপাশি, একটা fixed ক্রমে:
Single precision (float32) — মোট 32 bit
বিট: 31 30 ────────────── 23 22 ──────────────────────── 0
┌───┬──────────────────┬───────────────────────────────┐
│ S │ Exponent (8) │ Mantissa (23) │
└───┴──────────────────┴───────────────────────────────┘
sign bias = 127 implicit leading 1 আলাদা
Double precision (float64) — মোট 64 bit
বিট: 63 62 ───────────── 52 51 ──────────────────────── 0
┌───┬─────────────────┬─────────────────────────────────┐
│ S │ Exponent (11) │ Mantissa (52) │
└───┴─────────────────┴─────────────────────────────────┘
sign bias = 1023 implicit leading 1 আলাদা| Format | মোট bit | Sign | Exponent bit | Mantissa bit | Bias | ~ দশমিক digit | Range (finite, normal) |
|---|---|---|---|---|---|---|---|
half (float16) | 16 | 1 | 5 | 10 | 15 | 3.3 | 6.10×10⁻⁵ … 65504 |
| bfloat16 | 16 | 1 | 8 | 7 | 127 | 2.4 | float32-এর কাছাকাছি range |
single (float32) | 32 | 1 | 8 | 23 | 127 | 7.2 | 1.18×10⁻³⁸ … 3.40×10³⁸ |
double (float64) | 64 | 1 | 11 | 52 | 1023 | 15.9 | 2.23×10⁻³⁰⁸ … 1.80×10³⁰⁸ |
(IEEE 754 আরও একটা format সংজ্ঞায়িত করে — binary128 quad
precision, ১১২-বিট mantissa, 1+15+112=128 বিট মোট, HPC আর
কিছু বৈজ্ঞানিক simulation-এ মাঝে মাঝে ব্যবহৃত।)
মূল সূত্র — normalized সংখ্যার জন্য
1.M মানে mantissa field-এর সামনে একটা অদৃশ্য 1 বসিয়ে
পড়া — এই “implicit leading bit”-এর গল্প একটু পরে।
দশমিক digit-এর হিসাব কোথা থেকে এলো
n বিট mantissa (implicit বিট-সহ) থেকে প্রায় কত দশমিক digit
নির্ভরযোগ্য, তার সূত্র:
Single precision-এ 24 বিট কার্যকর mantissa (২৩ stored + ১
implicit): 24 \times 0.301 \approx 7.2 digit। এই কারণেই বলা
হয় “float-এ প্রায় ৭ digit নির্ভরযোগ্য” — অষ্টম digit-এর পর
থেকে যা দেখছেন তা প্রায়ই noise।
Biased exponent — কেন two’s complement নয়
Exponent negative হতে পারে (খুব ছোট সংখ্যার জন্য) বা positive
(খুব বড় সংখ্যার জন্য)। স্বাভাবিক প্রবৃত্তি হতো — আগের লেসনগুলোয়
signed integer-এর জন্য যা শিখেছি — two’s complement ব্যবহার
করা। IEEE 754 তা করে না। এর বদলে exponent-এ একটা bias
(single-এ 127, double-এ 1023) যোগ করে store করা হয়:
e_actual = -4 হলে single precision-এ E_stored = -4 + 127 = 123, সবসময় একটা অ-negative সংখ্যা, সাধারণ unsigned
binary-তে সরাসরি লেখা যায়।
Implicit leading bit — বিনামূল্যে একটা বাড়তি বিট
একটা normalized বাইনারি সংখ্যাকে সবসময় এভাবে লেখা যায়:
কেন সবসময় 1? কারণ normalization মানেই radix point-কে সেই
জায়গায় সরানো যেখানে ঠিক একটা non-zero digit তার আগে থাকে
— আর বাইনারিতে non-zero digit মানে অবশ্যই 1 (0 ছাড়া অন্য
কোনো digit বাইনারিতে নেই)।
তার মানে normalized সংখ্যার mantissa-র প্রথম বিট সবসময়ই
1 — সেটা store করার কোনো দরকার নেই! IEEE 754 এই বিটটা
বাদ দেয় (“implicit” বা “hidden” bit), আর বাকি ২৩ (single)
বা ৫২ (double) বিট শুধু 1.-এর পরের অংশ store করে।
ফলাফল: ২৩টা stored বিট দিয়ে আসলে ২৪-বিট precision পাওয়া
যায় — বিনামূল্যে একটা বাড়তি বিট। এই একটা trick-ই single
precision-কে ~7.2 digit থেকে বাস্তবে সামান্য বেশি নির্ভরযোগ্য
করে তোলে, যদি বিটটা সরাসরি store করা হতো।
ভেতরে কী ঘটছে
যেখানে representation-এর আসল রূপ বেরিয়ে আসে
১. 0.1-কে হাতে এনকোড করা — সম্পূর্ণ প্রক্রিয়া
এটাই এই লেসনের কেন্দ্রীয় হিসাব। চলুন 0.1-কে ৩২-বিট IEEE 754-এ
রূপান্তর করি, প্রতিটা ধাপ দেখিয়ে।
ধাপ ১ — Normalize করা। 0.1-কে 1.xxxx \times 2^e আকারে
লিখতে হবে। 0.1 আছে 2^{-4} = 0.0625 আর 2^{-3} = 0.125-এর
মাঝে, তাই e = -4।
তাহলে 0.1 = 1.6 \times 2^{-4}।
ধাপ ২ — 1.6-এর বাইনারি ভগ্নাংশ বের করা। 0.6-এর বাইনারি
digit বের করি বারবার 2 দিয়ে গুণ করে (আগের লেসনের হুবহু একই
পদ্ধতি):
0.6 × 2 = 1.2 → বিট 1 (remainder 0.2)
0.2 × 2 = 0.4 → বিট 0 (remainder 0.4)
0.4 × 2 = 0.8 → বিট 0 (remainder 0.8)
0.8 × 2 = 1.6 → বিট 1 (remainder 0.6 — চক্র পুনরাবৃত্তি!)চক্র দৈর্ঘ্য ৪: 1001 অনন্তকাল পুনরাবৃত্তি হবে।
1.6_{10} = 1.\overline{1001}_2 = 1.100110011001100110011001100...__2
ধাপ ৩ — ২৩ বিটে কাটা, round করা। Single precision-এ mantissa field মাত্র ২৩ বিট। প্রথম ২৪ বিট (২৩ রাখব + ১টা “round bit” দেখব) বের করি:
বিট নং: 1234 5678 9012 3456 7890 123 | 24
মান: 1001 1001 1001 1001 1001 100 | 1
└──────── রাখা হবে (23 বিট) ──┘ └ round bitবিট ২৪ (round bit) = 1, আর তার পরেও আরও 1 আছে (বিট ২৫ = 1)
— তাই এটা tie নয়, discarded অংশ স্পষ্টভাবে অর্ধেকের চেয়ে
বেশি। নিয়ম: round up।
২৩-বিট string 10011001100110011001100-এর শেষ বিট 0 কে 1
করে দিতে হয় (round up):
ধাপ ৪ — exponent বায়াস করা। e = -4, bias 127:
ধাপ ৫ — সব জোড়া লাগানো।
sign exponent mantissa (23 bit)
0 01111011 10011001100110011001101হেক্সে: 0x3DCCCCCD
- 0x3DCCCCCDমেমরিতে যা আছে — মাত্র ৩২টা বিট, কোনো "অর্থ" এখনো নেই
- sign = 0ধনাত্মক
- biased exponent = 123unbias: 123 − 127 = −4
- stored mantissa = 1001100110011001100110123 বিট, round-up হওয়ার পর
- implicit leading 1 যোগsignificand = 1.10011001100110011001101₂
- value = significand × 2⁻⁴= 1.6000000238... × 0.0625
- 0.100000001490116119384765625আসল stored decimal মান — 0.1 নয়!
যাচাই — আসল stored মান ঠিক কত?
এটাই 0.1f হিসেবে মেমরিতে যা সত্যিই আছে। 0.1-এর চেয়ে বেশি,
পার্থক্য প্রায় 1.49 \times 10^{-9} — খুবই ছোট, কিন্তু শূন্য
নয়। Python-এর print(0.1) যে 0.1 দেখায়, সেটা display-এর
সময় round করার ফল, ভেতরের raw মান নয়।
Double precision-এ তুলনা। ঠিক একই প্রক্রিয়া, কিন্তু ৫২ বিট mantissa দিয়ে (round bit ৫৩ নম্বরে):
হেক্স: 0x3FB999999999999A
sign 0
exponent 01111111011 (= 1019, unbias: 1019 − 1023 = −4)
mantissa 1001100110011001100110011001100110011001100110011010
স্টোর করা মান: 0.1000000000000000055511151231257827021181583404541015625double-এর error (~5.55 \times 10^{-18}) float-এর error-এর
(~1.49 \times 10^{-9}) চেয়ে প্রায় দশ কোটি গুণ ছোট — এটাই
বেশি mantissa বিটের সরাসরি সুবিধা। কিন্তু শূন্য নয়।
double-ও 0.1-কে exactly ধরতে পারে না — শুধু অনেক বেশি
নিখুঁতভাবে আনুমানিক করে। এই একই fact আপনি নিজে struct.pack
দিয়ে verify করবেন Experiment অংশে।
২. বিশেষ মান — যখন exponent field-এর দুই প্রান্তে বিশেষ অর্থ
Exponent field-এর দুইটা মান সংরক্ষিত — সব 0, আর সব 1
(single-এ 255, double-এ 2047)। এই দুই প্রান্ত normal
সংখ্যার নিয়ম মানে না, বরং বিশেষ মানের জন্য জায়গা তৈরি করে:
| Exponent field | Mantissa | অর্থ |
|---|---|---|
সব 0 (0) | সব 0 | ±0 (sign bit অনুযায়ী) |
সব 0 (0) | অ-শূন্য | Subnormal — implicit বিট 0, e = 1 - bias |
1 থেকে সর্বোচ্চ−১ | যেকোনো | Normal — implicit বিট 1 |
সব 1 (255/2047) | সব 0 | ±Infinity |
সব 1 (255/2047) | অ-শূন্য, MSB=1 | quiet NaN |
সব 1 (255/2047) | অ-শূন্য, MSB=0 | signaling NaN |
+0 বনাম −0। IEEE 754-এ শূন্যের দুইটা bit pattern আছে —
0x00000000 (+0) আর 0x80000000 (−0)। +0 == -0 তুলনায়
true, কিন্তু তারা identical bit pattern নয়। পার্থক্যটা
observable:
>>> 1.0 / 0.0
inf
>>> 1.0 / -0.0
-inf
>>> 0.0 == -0.0
Trueকেন দরকার? Signed zero জটিল সংখ্যা, limit calculation-এ direction
সংরক্ষণ করে — -0.0 বলে “আমি শূন্যের দিকে negative side থেকে
পৌঁছেছি”, যা কিছু গাণিতিক function-এর (যেমন log(x) যখন x
শূন্যের দিকে যায়) সঠিক আচরণের জন্য গুরুত্বপূর্ণ।
±Infinity। Overflow (যেমন float32-এর সীমা 3.4 \times 10^{38} ছাড়িয়ে গেলে) বা 1.0/0.0-এর ফলাফল। সাধারণ arithmetic
সংজ্ঞা মেনে চলে: ∞ + 1 = ∞, ∞ - ∞ = NaN, 1/∞ = 0।
int division-এর সাথে তুলনা করুন — 1/0 integer-এ একটা
crash (division-by-zero exception/trap), কিন্তু 1.0/0.0
floating-point-এ একটা সংজ্ঞায়িত মান (inf), কোনো crash
ছাড়াই। এটা একটা সচেতন ডিজাইন সিদ্ধান্ত — floating-point-এ
exception ছাড়াই হিসাব চালিয়ে যাওয়া যায়, শেষে ফলাফল পরীক্ষা
করা যায় (isinf() দিয়ে)।
NaN — Not a Number। যখন কোনো operation-এর কোনো অর্থবহ ফলাফল
নেই: 0.0/0.0, inf - inf, sqrt(-1.0) (real domain-এ)।
দুই ধরনের NaN:
- quiet NaN (qNaN): নীরবে propagate করে — কোনো exception ছাড়াই ফলাফলে ছড়িয়ে যায়। সাধারণ ব্যবহারিক NaN।
- signaling NaN (sNaN): ব্যবহার করা হলে একটা floating-point exception/trap তৈরি করতে পারে — কিছু hardware-এ “uninitialized memory” চিহ্নিত করতে ব্যবহৃত হয় (একটা sNaN দিয়ে memory pre-fill করে রাখলে, কেউ সেটা না জেনে ব্যবহার করলে সাথে সাথে trap হয়ে ধরা পড়ে)।
কোনটা কোনটা তা ঠিক হয় mantissa-র সবচেয়ে বড় (MSB) বিট দিয়ে —
IEEE 754-2008-এর সুপারিশ, x86/ARM-এ ব্যবহৃত convention: 1 =
quiet, 0 = signaling (বাকি mantissa বিটের অন্তত একটা 1
হতেই হবে, নাহলে সেটা Infinity হয়ে যাবে)।
quiet NaN (canonical, x86): 0 11111111 10000000000000000000000 = 0x7FC00000
signaling NaN (একটা উদাহরণ): 0 11111111 00000000000000000000001 = 0x7F800001৩. Subnormal সংখ্যা — শূন্যের কাছে একটা ফাঁক এড়ানো
Normal সংখ্যার ক্ষুদ্রতম positive মান কত? Exponent field-এর
ক্ষুদ্রতম বৈধ normal মান 1 (কারণ 0 subnormal-এর জন্য
সংরক্ষিত), তাই ক্ষুদ্রতম normal exponent e = 1 - 127 = -126।
Mantissa সব 0 ধরলে:
এখন প্রশ্ন — এর চেয়ে ছোট positive সংখ্যা কী হবে? যদি IEEE 754
এখানেই থেমে যেত (শুধু normal সংখ্যা), তাহলে 0 আর
1.18 \times 10^{-38}-এর মধ্যে একটা বিশাল, আকস্মিক ফাঁক
থাকত — কোনো representable সংখ্যা নেই, subtraction করলে
a - b = 0 হয়ে যেত যদিও a \ne b।
Subnormal (denormalized) সংখ্যা এই ফাঁক বন্ধ করে। যখন
exponent field সব 0, নিয়ম বদলে যায়:
লক্ষ্য করুন — exponent এখনো -126 (bias-থেকে 1-127, 0-127
নয়!), শুধু implicit leading bit 1-এর বদলে 0। এর মানে
subnormal সংখ্যাগুলো ক্রমশ কম precision নিয়ে শূন্যের দিকে
এগোয় — একে বলে gradual underflow।
subnormal ছাড়া হলে:
0 ─────────────── (বিশাল ফাঁক, কোনো representable মান নেই) ─────────────── 2⁻¹²⁶
(ক্ষুদ্রতম normal)
subnormal সহ (বাস্তব IEEE 754):
0 ─┬──┬──┬──┬──┬──┬──┬── ... ──┬── 2⁻¹²⁶
│ │ │ │ │ │ │ │
ধাপ ২⁻¹⁴⁹ সমান দূরত্বে, একদম শূন্য পর্যন্ত (linear spacing)৪. Rounding mode — round-to-nearest-even কেন “even”
যখন কোনো সংখ্যা দুইটা representable মানের ঠিক মাঝখানে পড়ে (একটা tie), কোনদিকে round করা উচিত? IEEE 754-এর default rounding mode — round-to-nearest, ties-to-even (সংক্ষেপে “round-half-even”, বা প্রচলিত নাম “banker’s rounding”)।
নিয়ম: সাধারণত নিকটতম representable মানে round হয়। কিন্তু ঠিক
মাঝখানে (tie) হলে, সেই মানে round হয় যার শেষ stored বিট 0
(জোড়/even)।
কেন শুধু “সবসময় উপরে round করো” নয়? কারণ সেটা একটা পদ্ধতিগত bias তৈরি করে। যদি প্রতিটা tie সবসময় উপরে যায়, লক্ষ লক্ষ operation-এ ছোট ছোট round-up জমতে জমতে গড় মানকে সত্যিকারের গড়ের চেয়ে সামান্য বেশি দেখাবে — একটা পরিসংখ্যানগত পক্ষপাত। Round-to-even এই bias এড়ায় — অর্ধেক tie উপরে যায়, অর্ধেক নিচে (কারণ “শেষ বিট জোড়” শর্তটা এলোমেলোভাবে উপরে বা নিচের প্রতিবেশীর সাথে মেলে)।
একটা সহজ decimal analogy: 2.5-কে নিকটতম integer-এ round
করতে হবে। 2 (জোড়) না 3 (বিজোড়)? Round-half-even বলবে 2
— কারণ 2 জোড়। 3.5 হলে round হবে 4 (জোড়), 3 নয়। বহুবার
round করলে গড়ে bias শূন্যের কাছাকাছি থাকে।
0.1-এর encoding-এ আমরা যে rounding দেখেছি (round bit 1,
পরের বিটও 1) সেটা tie ছিল না — discarded অংশ স্পষ্টভাবে
অর্ধেকের বেশি ছিল, তাই round up অবধারিত ছিল, even/odd নিয়ম
প্রযোজ্যই হয়নি। প্রশ্ন অংশে (Question ৫) আমরা একটা প্রকৃত tie
case দেখব।
উদাহরণ
দুইটা সম্পূর্ণ hand-worked উদাহরণ
উদাহরণ ১ — একটা exact case: −12.5
−12.5-এর বাইনারি: 12.5 = 1100.1₂ (8+4=12, .5=2⁻¹)।
Normalize: 1100.1₂ = 1.1001₂ \times 2^3।
- sign:
1(negative) - exponent:
3 + 127 = 130 = 10000010₂ - mantissa:
1001তারপর ১৯টা0দিয়ে ২৩ বিট পূরণ:10010000000000000000000
পুরো pattern: 1 10000010 10010000000000000000000 =
0xC1480000
কোনো rounding দরকার হয়নি — 12.5 এর বাইনারি expansion সসীম
(কারণ 12.5 = 25/2, হর শুধু 2-এর ঘাত, আগের লেসনের নিয়ম
অনুযায়ী)। এই ধরনের “গোল” সংখ্যা IEEE 754-এ exactly store
হয় — কোনো error নেই।
উদাহরণ ২ — উল্টো দিকে: bit pattern থেকে decimal
দেওয়া আছে 0x42480000। এটা কোন সংখ্যা?
বাইনারি: 0100 0010 0100 1000 0000 0000 0000 0000
- sign (বিট ৩১) =
0→ ধনাত্মক - exponent (বিট ৩০-২৩) =
10000100₂=132→ unbias:132 - 127 = 5 - mantissa (বিট ২২-০) =
1001000...0→1.1001₂-এর ভগ্নাংশ অংশ
উত্তর: 50.0 — আরেকটা exact, “গোল” সংখ্যা।
দুইটা উদাহরণই ইচ্ছাকৃতভাবে exact বেছে নেওয়া হয়েছে যাতে encode/decode
প্রক্রিয়াটা rounding-এর জটিলতা ছাড়া স্পষ্ট দেখা যায়। “hood”
অংশের 0.1 উদাহরণটা মনে করুন — সেখানেই আসল জটিলতা: বেশিরভাগ
বাস্তব-জীবনের দশমিক সংখ্যাই exact নয়, round করতে হয়।
নিজে চালিয়ে দেখুন
যেকোনো float-এর raw bits দেখুন — struct দিয়ে
import struct
def show_float32(x: float):
packed = struct.pack('>f', x) # big-endian 4 byte
hex_str = packed.hex()
bits = ''.join(f'{b:08b}' for b in packed)
sign, exp, mant = bits[0], bits[1:9], bits[9:]
print(f"value : {x}")
print(f"hex : 0x{hex_str.upper()}")
print(f"bits : {sign} {exp} {mant}")
print(f"biased exp : {int(exp, 2)} (unbiased: {int(exp, 2) - 127})")
# আসল stored decimal মান — struct দিয়ে ফেরত আনি
(back,) = struct.unpack('>f', packed)
print(f"stored exact : {back!r}")
print()
show_float32(0.1)
show_float32(-12.5)
show_float32(50.0)
show_float32(0.0)
show_float32(-0.0)
show_float32(float('inf'))
show_float32(float('nan'))প্রত্যাশিত output:
value : 0.1
hex : 0x3DCCCCCD
bits : 0 01111011 10011001100110011001101
biased exp : 123 (unbiased: -4)
stored exact : 0.10000000149011612
value : -12.5
hex : 0xC1480000
bits : 1 10000010 10010000000000000000000
biased exp : 130 (unbiased: 3)
stored exact : -12.5
value : 50.0
hex : 0x42480000
bits : 0 10000100 10010000000000000000000
biased exp : 132 (unbiased: 5)
stored exact : 50.0
value : 0.0
hex : 0x00000000
...
value : -0.0
hex : 0x80000000
...
value : inf
hex : 0x7F800000
...
value : nan
hex : 0x7FC00000
...লক্ষ্য করুন 0x3DCCCCCD আর stored exact : 0.10000000149011612
— হাতে করা হিসাবের সাথে হুবহু মিলে যাচ্ছে। আর nan-এর হেক্স
0x7FC00000 — mantissa-র MSB 1, যেটা এই লেসনে দেখানো quiet
NaN convention-এর সাথে মেলে।
আমরা হাতে যে 0.1f এনকোডিং বের করেছি (0x3DCCCCCD), সেটাই সত্যিকারের hardware/interpreter store করে — হাতের হিসাব আর মেশিনের বাস্তবতা মিলে যায়।
Special value-এর আচরণ — signed zero, Infinity, NaN
import math
# signed zero
print(0.0 == -0.0) # True — সমান
print(math.copysign(1, 0.0)) # 1.0
print(math.copysign(1, -0.0)) # -1.0 — কিন্তু sign আলাদা!
print(1.0 / 0.0 if False else "skip") # (Python এ ZeroDivisionError ওঠে, C-তে ওঠে না)
# NaN
nan = float('nan')
print(nan == nan) # False
print(nan != nan) # True — NaN চেনার পুরনো পদ্ধতি
print(math.isnan(nan)) # True
# subnormal — স্বাভাবিক arithmetic দিয়েই পৌঁছানো যায়
import struct
tiny = struct.unpack('>f', struct.pack('>f', 1e-40))[0]
print(tiny) # একটা subnormal মান — normal-এর সীমার নিচে
packed = struct.pack('>f', tiny)
bits = ''.join(f'{b:08b}' for b in packed)
print(f"exponent bits: {bits[1:9]}") # সব 0 — subnormal-এর চিহ্নC-তে infinity সরাসরি দেখা যায় (কোনো exception ছাড়াই):
#include <stdio.h>
#include <math.h>
int main(void) {
float a = 1.0f, zero = 0.0f, negzero = -0.0f;
printf("1/0.0 = %f\n", a / zero); /* inf */
printf("1/-0.0 = %f\n", a / negzero); /* -inf */
printf("0.0 == -0.0 : %d\n", zero == negzero); /* 1 (true) */
float nan = 0.0f / 0.0f;
printf("nan == nan : %d\n", nan == nan); /* 0 (false)! */
printf("isnan(nan) : %d\n", isnan(nan)); /* 1 */
return 0;
}প্রত্যাশিত output (C):
1/0.0 = inf
1/-0.0 = -inf
0.0 == -0.0 : 1
nan == nan : 0
isnan(nan) : 1int হলে a / zero সাথে সাথে crash করত (SIGFPE)। এখানে
floating-point চুপচাপ inf ফেরত দিয়ে চালিয়ে যাচ্ছে — এটাই
আগের অংশে বলা “exception ছাড়া হিসাব চালিয়ে যাওয়া”-র ডিজাইন
দর্শন।
±0 সমান কিন্তু identical নয়, NaN নিজের সাথেও অসমান, আর subnormal সংখ্যা স্বাভাবিক arithmetic-এই তৈরি হতে পারে — এগুলো edge case না, প্রতিদিনের arithmetic-এর অংশ।
নিজে বানান
একটা float-এর raw bits ডিকোড করে sign/exponent/mantissa আলাদা করুন
- float-এর raw বাইট বের করুন (Python: struct.pack; C: union বা memcpy)
- সেই বাইট থেকে sign, exponent, mantissa তিনটা field আলাদা করে বের করুন bit-shifting দিয়ে
- exponent-কে unbias করুন এবং normal/subnormal/special case নির্ণয় করুন
- mantissa-কে fraction-এ রূপান্তর করে (implicit বিট যোগ করে) চূড়ান্ত decimal মান পুনর্গঠন করুন
- পুনর্গঠিত মান আসল input-এর সাথে মিলিয়ে ১০০টা random float-এ যাচাই করুন
এই একটা exercise-ই এই পুরো লেসনের সারমর্ম — যদি আপনি নিজে bits থেকে সংখ্যা পুনর্গঠন করতে পারেন, IEEE 754 আর কোনো “black box” থাকবে না।
Python — struct দিয়ে:
import struct
def decode_float32(x: float) -> dict:
raw = struct.unpack('>I', struct.pack('>f', x))[0] # 32-bit unsigned int হিসেবে পড়া
sign = (raw >> 31) & 0x1
exp_field = (raw >> 23) & 0xFF
mant_field = raw & 0x7FFFFF # নিচের 23 বিট
if exp_field == 0 and mant_field == 0:
kind, value = 'zero', 0.0 if sign == 0 else -0.0
elif exp_field == 0:
kind = 'subnormal'
value = ((-1) ** sign) * (mant_field / 2**23) * 2**(1 - 127)
elif exp_field == 0xFF and mant_field == 0:
kind, value = 'infinity', float('inf') if sign == 0 else float('-inf')
elif exp_field == 0xFF:
kind, value = 'nan', float('nan')
else:
kind = 'normal'
significand = 1 + mant_field / 2**23 # implicit leading 1 যোগ
value = ((-1) ** sign) * significand * 2**(exp_field - 127)
return {
'sign': sign, 'exponent_field': exp_field, 'mantissa_field': mant_field,
'kind': kind, 'reconstructed': value,
}
for x in [0.1, -12.5, 50.0, 0.0, -0.0, float('inf'), 1e-40]:
d = decode_float32(x)
print(f"{x!r:>12} → sign={d['sign']} exp={d['exponent_field']:>3} "
f"kind={d['kind']:>9} reconstructed={d['reconstructed']!r}")C — union দিয়ে type punning (নিরাপদ পদ্ধতি):
#include <stdio.h>
#include <stdint.h>
#include <math.h>
typedef union {
float f;
uint32_t bits;
} float_bits;
void decode_float32(float x) {
float_bits fb;
fb.f = x; /* একই memory, দুইভাবে পড়া */
uint32_t raw = fb.bits;
int sign = (raw >> 31) & 0x1;
int exp_field = (raw >> 23) & 0xFF;
uint32_t mant_field = raw & 0x7FFFFF;
printf("value=%g sign=%d exp_field=%d mant_field=0x%06X\n",
x, sign, exp_field, mant_field);
if (exp_field == 0xFF) {
printf(" -> %s\n", mant_field == 0 ? "infinity" : "NaN");
} else if (exp_field == 0 && mant_field == 0) {
printf(" -> zero\n");
} else if (exp_field == 0) {
double val = (mant_field / 8388608.0) * pow(2, -126);
printf(" -> subnormal, reconstructed = %.20g\n", sign ? -val : val);
} else {
double significand = 1.0 + mant_field / 8388608.0; /* 2^23 = 8388608 */
double val = significand * pow(2, exp_field - 127);
printf(" -> normal, reconstructed = %.20g\n", sign ? -val : val);
}
}
int main(void) {
decode_float32(0.1f);
decode_float32(-12.5f);
decode_float32(50.0f);
return 0;
}নিজে বাড়ান:
- Double precision (৬৪-বিট) সংস্করণ লিখুন — একই যুক্তি, শুধু
bit width আর bias বদলাবে (
11বিট exponent, bias1023,52বিট mantissa) encode_float32(sign, exponent, mantissa)— উল্টো দিকের function লিখুন, যেটা তিনটা field থেকে raw bits জোড়া লাগায়- একটা function লিখুন যেটা বলে দেবে দুইটা float ঠিক কতগুলো “representable step” (ULP — unit in the last place) দূরে, raw bit pattern-কে integer হিসেবে বিয়োগ করে (biased exponent trick-এর সরাসরি প্রয়োগ!)
0.1f + 0.2f-এর bits decode করুন,0.3f-এর bits-এর সাথে তুলনা করুন — এটাই পরের লেসনের শুরুর বিন্দু
বাস্তব সিস্টেমে
IEEE 754 যেখানে প্রতিদিন কাজ করছে
JavaScript-এর একমাত্র number type। JavaScript-এ 1, 1.5,
1e300 — সবই একই টাইপ: IEEE 754 double। এমনকি “integer”ও।
ফলাফল: Number.MAX_SAFE_INTEGER = 2^{53} - 1 — কারণ double-এর
৫২-বিট mantissa + implicit বিট মিলে ৫৩ বিট পর্যন্ত integer
exactly represent করতে পারে, তার বেশি হলে সব integer
representable থাকে না (2^{53} + 1 কে double-এ store করলে
2^{53} হয়ে যায়!)।
GPU shader-এর মূল ভাষা। প্রতিটা GPU shader core native-ভাবে
float32 (কখনো float16) arithmetic-এ ডিজাইন করা — graphics
pipeline-এর প্রতিটা vertex transform, প্রতিটা pixel color হিসাব
এই bit layout-এর উপর নির্ভর করে।
Machine learning-এর reduced-precision format। আধুনিক deep
learning training-এ full float32 প্রায়ই অপ্রয়োজনীয় ব্যয়বহুল
(memory + compute)। তাই দুইটা ১৬-বিট বিকল্প জনপ্রিয় —
- IEEE
float16(৫ exponent, ১০ mantissa) — কম range, বেশি precision। bfloat16(Google-এর ডিজাইন, TPU-তে native) —float32-এর সমান ৮ exponent বিট রাখে (তাই একই range, overflow-এর ভয় কম) কিন্তু মাত্র ৭ mantissa বিট। ML training-এ range বেশি গুরুত্বপূর্ণ (gradient বিশাল বা ক্ষুদ্র হতে পারে), precision-এর ক্ষতি সহনীয় — তাই এই trade-off ইচ্ছাকৃত।
PostgreSQL/MySQL-এর REAL/DOUBLE PRECISION। সরাসরি IEEE
754 single/double — SQL standard specifically IEEE 754 reference
করে যখন এই টাইপগুলো সংজ্ঞায়িত করে।
Denormal number আর audio DSP-এর performance bug। অনেক CPU-তে
(বিশেষত পুরনো x86) subnormal number নিয়ে arithmetic normal
সংখ্যার চেয়ে ১০-১০০ গুণ ধীর — কারণ hardware fast-path সেগুলো
হ্যান্ডেল করে না, একটা slow microcode fallback লাগে। Audio
processing chain-এ silence-এর পর একটা exponentially-decaying
signal (যেমন reverb tail) ধীরে ধীরে subnormal range-এ নেমে
যেতে পারে — হঠাৎ পুরো audio thread CPU খেয়ে ফেলে, শ্রবণযোগ্য
glitch তৈরি করে। প্রতিকার: CPU-র “flush-to-zero” (FTZ) আর
“denormals-are-zero” (DAZ) mode চালু করা (x86-এ MXCSR register
বিট), যেটা subnormal-কে সরাসরি শূন্যে round করে দেয় — precision-এর
সামান্য ক্ষতির বিনিময়ে predictable performance।
HDR ইমেজ ফরম্যাট। ILM-এর OpenEXR (চলচ্চিত্র VFX শিল্পে
ব্যবহৃত HDR ইমেজ format) প্রতি pixel channel-এ IEEE float16
ব্যবহার করে — কারণ HDR-এ brightness range খুব বিশাল (সূর্যের
আলো থেকে ছায়া পর্যন্ত), যা 8-bit বা এমনকি uint16 integer
দিয়ে ধরা যায় না, কিন্তু float16-এর floating exponent দিয়ে
সহজেই যায়।
NaN-কে “missing data”-র সংকেত হিসেবে ব্যবহার। বৈজ্ঞানিক computing-এ (NumPy, pandas) NaN প্রায়ই “sensor থেকে ডেটা আসেনি” বা “গণনাযোগ্য নয়” বোঝাতে ব্যবহার হয় — আর IEEE 754-এর NaN-propagation নিয়ম (যেকোনো operation-এ NaN জড়ালে ফলাফল NaN) স্বয়ংক্রিয়ভাবে “অজানা” মান পুরো হিসাবের মধ্য দিয়ে ছড়িয়ে দেয়, প্রতিটা ধাপে manual check ছাড়াই।
File format-এর magic number হিসেবে বিটের ব্যবহার। এই
লেসনের biased-exponent insight (raw bits-কে integer হিসেবে
তুলনা করলে সঠিক ক্রম পাওয়া যায়) সরাসরি ব্যবহৃত হয় sorting
algorithm আর database index-এ, যেখানে floating-point column-এর
উপর ORDER BY বা B-tree index build করার সময় engine প্রায়ই
raw bit representation-এর উপর ভিত্তি করেই তুলনা করে — কোনো
আলাদা floating-point-নির্দিষ্ট comparator ছাড়াই, ঠিক এই লেসনের
Question ৩-এর প্রমাণ অনুযায়ী।
Text-based diff tool আর floating-point-এর সংঘাত। Git-এর মতো version control system টেক্সট হিসেবে ফাইল তুলনা করে, bit বা মান হিসেবে নয় — তাই একটা scientific dataset-এ যদি সামান্য ভিন্ন floating-point library বা platform ব্যবহারে একই হিসাবের টেক্সট-রূপ সামান্য বদলায় (যেমন শেষ digit-এ), পুরো লাইনটাই “changed” দেখাবে, যদিও অন্তর্নিহিত মান কার্যত অভিন্ন। এই কারণে scientific/ML dataset version control-এ প্রায়ই tolerance-ভিত্তিক, floating-point-সচেতন diff tool ব্যবহার করা হয়, প্লেইন টেক্সট diff-এর বদলে।
যে ভুলগুলো সবাই করে
“NaN == NaN true হওয়া উচিত — অন্য ভাষায় তো undefined == undefined true।”
IEEE 754-এর সিদ্ধান্ত ইচ্ছাকৃতভাবে উল্টো। NaN-এর অর্থ “এই
মানটা কী তা সংজ্ঞায়িত নয়” — দুইটা ভিন্ন, অসম্পর্কিত হিসাব থেকে
আসা দুইটা NaN আদৌ “একই” কি না বলাটাই অর্থহীন (0.0/0.0-এর NaN
আর sqrt(-1.0)-এর NaN “একই” এই দাবি করার কোনো ভিত্তি নেই)।
তাই IEEE 754 বেছে নিয়েছে: NaN-এর সাথে যেকোনো তুলনা
(এমনকি নিজের সাথেও) false, শুধু != ছাড়া (NaN != NaN সবসময়
true)। এই একটা ব্যতিক্রমী নিয়মই দশকের পর দশক ধরে NaN চেনার
সবচেয়ে নির্ভরযোগ্য, portable কৌশল ছিল — isnan() function
সবসময় সব জায়গায় ছিল না।
“+0.0 আর −0.0 একই জিনিস, যেহেতু তুলনা করলে সমান আসে।”
0.0 == -0.0 সত্যিই True, কিন্তু bit pattern হিসেবে তারা
সম্পূর্ণ ভিন্ন (0x00000000 বনাম 0x80000000), আর এই
পার্থক্য observable:
>>> 1.0 / 0.0
inf
>>> 1.0 / -0.0
-infEquality operator (==) এই পার্থক্যটা ইচ্ছাকৃতভাবে আড়াল করে
(কারণ বেশিরভাগ ব্যবহারিক ক্ষেত্রে +0 আর -0 সমান আচরণ করা
উচিত), কিন্তু division-এর মতো operation-এ sign-টা টিকে থাকে,
কারণ সেটা limit-এর দিক নির্দেশ করে।
“Subnormal number একটা rare edge case, উপেক্ষা করলে ক্ষতি নেই।”
Subnormal একটা deliberate, প্রয়োজনীয় design feature — এটা ছাড়া
শূন্যের কাছে একটা “sudden gap” থাকত, যেখানে দুইটা ভিন্ন সংখ্যা
বিয়োগ করলে 0 আসতে পারত।
তবে এটা সত্য যে subnormal নিয়ে সাবধান থাকতে হয় performance-এর কারণে, correctness-এর কারণে নয় — অনেক CPU subnormal arithmetic অনেক ধীরে চালায় (আগের “বাস্তব সিস্টেমে” অংশের audio DSP উদাহরণ মনে করুন)। তাই real-time system-এ FTZ/DAZ চালু করা সাধারণ practice — কিন্তু এটা একটা conscious trade-off (সামান্য precision বনাম predictable speed), “subnormal গুরুত্বহীন” এই বিশ্বাসের কারণে নয়।
“বেশি exponent বিট মানেই বেশি precision।”
উল্টো ভাগ — exponent বিট নিয়ন্ত্রণ করে range, mantissa বিট নিয়ন্ত্রণ করে precision। এই দুটো সম্পূর্ণ স্বতন্ত্র resource, আর তাদের মধ্যে trade-off করা যায়।
float16 (৫ exponent, ১০ mantissa) বনাম bfloat16 (৮ exponent,
৭ mantissa) — দুটোই ১৬ বিট মোট, কিন্তু সম্পূর্ণ ভিন্ন trade-off:
bfloat16-এর range float32-এর সমান (বেশি exponent বিট), কিন্তু
precision কম (float16-এর চেয়েও কম mantissa বিট)। কোনটা
“ভালো” সম্পূর্ণ নির্ভর করে ব্যবহারের উপর — deep learning
training-এ range (overflow এড়ানো) বেশি গুরুত্বপূর্ণ, তাই
bfloat16 জনপ্রিয়; scientific simulation-এ প্রায়ই precision
বেশি গুরুত্বপূর্ণ।
বুঝেছেন কি না দেখুন
1Raw bit pattern 0xC1480000 দেওয়া আছে (এটা IEEE 754 single
precision)। sign, exponent, mantissa আলাদা করে চূড়ান্ত decimal
মান বের করুন।
প্রয়োগ
0xC1480000 দেওয়া আছে (এটা IEEE 754 single
precision)। sign, exponent, mantissa আলাদা করে চূড়ান্ত decimal
মান বের করুন।হেক্স 0xC1480000-কে বাইনারিতে রূপান্তর করি (প্রতিটা হেক্স digit
= ৪ বিট):
C = 1100 1 = 0001 4 = 0100 8 = 1000 0000 0000 0000 0000পূর্ণ ৩২ বিট: 1100 0001 0100 1000 0000 0000 0000 0000
Field আলাদা করা:
- sign (বিট ৩১) =
1→ ঋণাত্মক - exponent (বিট ৩০-২৩, ৮ বিট) =
10000010=130 - mantissa (বিট ২২-০, ২৩ বিট) =
10010000000000000000000
Unbias: 130 - 127 = 3
Significand: 1.M = 1.10010000000000000000000₂ =
1 + 2^{-1} + 2^{-4} = 1 + 0.5 + 0.0625 = 1.5625
চূড়ান্ত মান:
এটা এই লেসনের “example” অংশে হাতে এনকোড করা -12.5-এরই ঠিক
উল্টো (decode) প্রক্রিয়া — এনকোড আর ডিকোড একে অপরের সম্পূর্ণ
বিপরীত অপারেশন, আর এই সংখ্যাটা এনকোড/ডিকোড দুইদিকেই exact
(কোনো rounding error নেই), কারণ 12.5-এর বাইনারি expansion
সসীম।
2100.375-কে ৩২-বিট IEEE 754-এ হাতে এনকোড করুন। (ইঙ্গিত:
0.375 = 0.011₂)
প্রয়োগ
100.375-কে ৩২-বিট IEEE 754-এ হাতে এনকোড করুন। (ইঙ্গিত:
0.375 = 0.011₂)পূর্ণসংখ্যা অংশ: 100 = 1100100₂ (64+32+4=100)
ভগ্নাংশ অংশ: 0.375 = 0.25 + 0.125 = 2^{-2} + 2^{-3} = 0.011₂
পূর্ণ বাইনারি: 100.375 = 1100100.011₂
Normalize: radix point-কে বাঁয়ে ৬ ঘর সরাতে হবে যাতে প্রথম
1-এর ঠিক পরে বিন্দু আসে:
Exponent: 6 + 127 = 133 = 10000101₂
Mantissa: 1. -এর পরের অংশ 100100011 (৯ বিট), ২৩ বিট
পূরণ করতে আরও ১৪টা 0 লাগবে:
পূর্ণ pattern: 0 10000101 10010001100000000000000
হেক্সে: বিটগুলো নিবলে ভাগ করি — 0100 0010 1100 1000 1100 0000 0000 0000 = 0x42C8C000
এখানেও কোনো rounding দরকার হয়নি — 100.375-এর বাইনারি expansion
সসীম, কারণ 0.375 = 3/8, হর 8 = 2^3 শুধু 2-এর ঘাত। এই
লেসনের মূল শিক্ষা মনে করিয়ে দেয়: শুধু সেসব ভগ্নাংশ exact যাদের
হর 2-এর ঘাত — 0.1-এর মতো বেশিরভাগ “স্বাভাবিক” দশমিক
সংখ্যা এই কপালটা পায় না।
3প্রমাণ করুন কেন biased exponent দুইটা ধনাত্মক float-কে raw
bit pattern হিসেবে unsigned integer তুলনা করলে সঠিক numeric
order দেয়। একটা concrete উদাহরণ দিয়ে দেখান।
যুক্তি
দাবি: দুইটা ধনাত্মক float a, b-এর জন্য, a \lt b
(প্রকৃত সংখ্যা হিসেবে) হলে এবং শুধু তখনই তাদের raw ৩২-বিট
pattern-কে unsigned integer পড়লে \text{bits}(a) \lt \text{bits}(b)।
যুক্তি: Sign বিট দুটোরই 0 (ধরে নিচ্ছি ধনাত্মক), তাই তুলনাটা
কার্যত exponent+mantissa field-এর (৩১ বিট) unsigned তুলনায়
নেমে আসে। এই ৩১ বিটকে একটা single big-endian integer হিসেবে
দেখুন — exponent field mantissa field-এর চেয়ে বেশি “গুরুত্বপূর্ণ”
বিট পজিশনে বসে (আগে আসে), ঠিক দশমিকে শত-দশক-এক digit-এর মতো।
তাই:
- যদি দুইটা সংখ্যার exponent ভিন্ন হয়, বড় exponent-এর সংখ্যাটার bit pattern অবশ্যই বড় হবে (mantissa যাই হোক না কেন) — কারণ exponent field উচ্চতর বিট position দখল করে।
- আর যেহেতু exponent biased (তাই সবসময় একটা non-negative integer, monotonically সংখ্যার প্রকৃত exponent-এর সাথে সমানুপাতিক), বড় biased exponent মানেই বড় প্রকৃত exponent, মানেই বড় সংখ্যা।
- যদি exponent সমান হয়, বড় mantissa field মানেই বড় significand, মানেই বড় সংখ্যা — mantissa field-ও সরাসরি unsigned integer হিসেবে সঠিক ক্রমে থাকে (কোনো bias লাগে না, কারণ mantissa সবসময় non-negative fraction)।
Concrete উদাহরণ (এই লেসন থেকেই):
0.1f→ exponent field123, পুরো bits (unsigned int হিসেবে) =0x3DCCCCCD=1,036,831,94950.0f→ exponent field132, পুরো bits =0x42480000=1,111,752,704
0.1 \lt 50.0 (প্রকৃত সংখ্যা), আর সত্যিই
1,036,831,949 \lt 1,111,752,704 (raw bit pattern,
unsigned integer হিসেবে)। মিলে যায়।
যদি exponent two’s complement হতো, 0.1-এর মতো ছোট
সংখ্যার (negative exponent) MSB 1 হতো — unsigned পড়লে সেটা
একটা বিশাল সংখ্যা মনে হতো, বড় সংখ্যার (positive exponent, MSB
0) চেয়েও বড়। Order সম্পূর্ণ ভেঙে পড়ত। এটাই biased exponent-এর
আসল কারণ — শুধু “negative সংখ্যা লেখার একটা উপায়” নয়, বরং একটা
তুলনা-বান্ধব encoding।
(Sign বিট ভিন্ন হলে বাড়তি একটা ধাপ লাগে — negative float-এর জন্য magnitude অনুযায়ী উল্টো ক্রম, কিন্তু সেই কেসটা হ্যান্ডেল করাও sign বিট আলাদা চেক করে সহজেই সম্ভব।)
4Bit pattern 0 00000000 00000000000000000000001 (single
precision) — সবচেয়ে ছোট non-zero mantissa, exponent field সব
0। এটা কোন সংখ্যা? এটা normal সংখ্যার ক্ষুদ্রতম মানের (≈1.18 × 10⁻³⁸) সাথে কীভাবে তুলনা করে?
প্রয়োগ
0 00000000 00000000000000000000001 (single
precision) — সবচেয়ে ছোট non-zero mantissa, exponent field সব
0। এটা কোন সংখ্যা? এটা normal সংখ্যার ক্ষুদ্রতম মানের (≈1.18 × 10⁻³⁸) সাথে কীভাবে তুলনা করে?Exponent field সব 0 আর mantissa অ-শূন্য — টেবিল অনুযায়ী এটা
subnormal।
Subnormal সূত্র: value = 0.M × 2^{-126} (implicit বিট এখানে
1 নয়, 0)।
Mantissa field = 1 (দশমিকে), যার মান 0.M = 1 / 2^{23} = 1/8388608।
এটাই single precision-এর সম্পূর্ণ ক্ষুদ্রতম representable positive সংখ্যা — সবচেয়ে ছোট subnormal।
তুলনা: ক্ষুদ্রতম normal 2^{-126} \approx 1.175 \times 10^{-38}।
অনুপাত:
অর্থাৎ ক্ষুদ্রতম normal সংখ্যা ক্ষুদ্রতম subnormal-এর চেয়ে
প্রায় ৮৪ লক্ষ গুণ বড়। Subnormal range (2^{-149} থেকে
2^{-126} পর্যন্ত) precision হারাতে হারাতে ধীরে ধীরে শূন্যের
দিকে নামে — gradual underflow-এর সংজ্ঞা অনুযায়ীই। ঠিক 2^{-149}
থেকে 2 \times 2^{-149} = 2^{-148}-এ যেতে mantissa field 1
থেকে 2-এ বাড়ে (এখনো শুধু ১ বিট তথ্য), যেখানে normal range-এ
প্রতিটা ধাপে পূর্ণ ২৩ বিট mantissa পাওয়া যায়।
5ধরুন একটা hypothetical ৪-বিট mantissa float-এ (শুধু এই উদাহরণের
জন্য সরলীকৃত) দুইটা representable মান আছে 10.0₂ (mantissa
শেষ বিট 0, জোড়) আর 10.1₂ (পরের representable মান, মানে
পরের mantissa 11.0, শেষ বিট 0 — অপেক্ষা করুন, এটা ভুল সেটআপ;
বরং ভাবুন 1.010₂ \times 2^1 (mantissa শেষ বিট 0) আর
1.011₂ \times 2^1 (mantissa শেষ বিট 1) দুইটা পরপর representable
মান, আর প্রকৃত ফলাফল ঠিক এই দুইয়ের মাঝখানে পড়েছে। Round-to-nearest-even
কোনদিকে round করবে, আর round-half-up করলে কী পার্থক্য হতো?
ডিজাইন
10.0₂ (mantissa
শেষ বিট 0, জোড়) আর 10.1₂ (পরের representable মান, মানে
পরের mantissa 11.0, শেষ বিট 0 — অপেক্ষা করুন, এটা ভুল সেটআপ;
বরং ভাবুন 1.010₂ \times 2^1 (mantissa শেষ বিট 0) আর
1.011₂ \times 2^1 (mantissa শেষ বিট 1) দুইটা পরপর representable
মান, আর প্রকৃত ফলাফল ঠিক এই দুইয়ের মাঝখানে পড়েছে। Round-to-nearest-even
কোনদিকে round করবে, আর round-half-up করলে কী পার্থক্য হতো?এই পরিস্থিতি একটা classic tie — প্রকৃত ফলাফল ঠিক দুইটা representable মানের মাঝখানে, দুই দিক থেকেই সমান দূরত্বে।
দুইটা প্রার্থী:
1.010₂ \times 2^1— শেষ mantissa বিট0(জোড়)1.011₂ \times 2^1— শেষ mantissa বিট1(বিজোড়)
Round-to-nearest-even নিয়ম: tie হলে সেই প্রতিবেশী বেছে নাও
যার শেষ বিট 0। এখানে সেটা প্রথমটা — 1.010₂ \times 2^1।
যদি round-half-up (সবসময় বড় মানের দিকে) হতো: তাহলে সবসময়
1.011₂ \times 2^1 (দ্বিতীয়টা) বেছে নেওয়া হতো, tie-এর দিক
নির্বিশেষে — কারণ round-half-up শুধু “উপরের প্রতিবেশী কোনটা”
জিজ্ঞেস করে, “কোনটা জোড়” নয়।
পার্থক্যটা কেন গুরুত্বপূর্ণ — পরিসংখ্যানগত যুক্তি:
কল্পনা করুন এমন হাজার হাজার tie-case ঘটছে একটা বড় সংখ্যক হিসাবে (যেমন একটা লম্বা সময় ধরে চলা simulation, বা একটা বড় dataset-এর উপর repeated averaging)। যদি প্রতিটা tie সবসময় বড় মানের দিকে round হয় (round-half-up), তাহলে প্রতিটা tie-এ গড়ে একটা ছোট, একই দিকের bias যোগ হয় — লক্ষ লক্ষ operation-এর পর চূড়ান্ত ফলাফল সত্যিকারের মানের চেয়ে পদ্ধতিগতভাবে বড় দেখাতে পারে।
Round-to-nearest-even-এ, tie কোনদিকে যাবে তা নির্ভর করে “কোন প্রতিবেশীর শেষ বিট জোড়” তার উপর — যেটা ব্যবহারিকভাবে data-নির্ভর, এলোমেলো (কোনো systematic pattern নেই যে সবসময় উপরে বা সবসময় নিচে যাবে)। ফলে বহু tie-এর গড়ে bias প্রায় বাতিল হয়ে যায় — round-half-up-এর তুলনায় statistically নিরপেক্ষ।
এই একই যুক্তি accounting/statistics-এ “banker’s rounding” নামে পরিচিত — bank-এর সুদের হিসাবে বহু ছোট ছোট rounding হয়, আর সবসময় একদিকে bias করলে দীর্ঘমেয়াদে ব্যাংকের পক্ষে (বা বিপক্ষে) একটা পদ্ধতিগত সুবিধা/ক্ষতি জমতে থাকত।
6“hood” অংশে আমরা 0.1-কে হাতে এনকোড করেছি। এবার নিজে করুন —
1/3-কে ৩২-বিট IEEE 754-এ হাতে এনকোড করুন, rounding সহ। চূড়ান্ত
হেক্স মান দিন।
প্রয়োগ
0.1-কে হাতে এনকোড করেছি। এবার নিজে করুন —
1/3-কে ৩২-বিট IEEE 754-এ হাতে এনকোড করুন, rounding সহ। চূড়ান্ত
হেক্স মান দিন।ধাপ ১ — normalize। 1/3 আছে 2^{-2}=0.25 আর 2^{-1}=0.5-এর
মাঝে, তাই e=-2।
ধাপ ২ — 0.333...-এর বাইনারি বের করা। আগের লেসনে (fixed
vs floating) আমরা দেখিয়েছি 1/3 = 0.(01)_2 — পুনরাবৃত্তি
দৈর্ঘ্য ২, কারণ 2-এর multiplicative order mod 3 হলো ২।
1.333..._{10} = 1.\overline{01}_2 = 1.01010101010101...__2
ধাপ ৩ — ২৩ বিটে কাটা, round করা। Bit পজিশন বিজোড় হলে 0,
জোড় হলে 1 (প্যাটার্ন 01 repeating)। বিট ২৩ (বিজোড়) = 0,
বিট ২৪ (round bit, জোড়) = 1।
বিট ২৪ =1, আর তার পরেও নন-জিরো বিট আছে (বিট ২৬, ২৮…) —
tie নয়, স্পষ্ট round-up।
২৩-বিট stored mantissa (round করার আগে): 01010101010101010101010
(১১ বার 01, তারপর 0) — শেষ বিট 0, round-up করলে 1 হয়ে
যায়:
ধাপ ৪ — exponent বায়াস করা।
ধাপ ৫ — জোড়া লাগানো।
sign exponent mantissa (23 bit)
0 01111101 01010101010101010101011হেক্সে: 0x3EAAAAAB
এই মানটাই struct.pack('>f', 1/3) চালালে সত্যিই পাওয়া যায় —
এই লেসনের প্রথম Experiment-এ show_float32(1/3) যোগ করে নিজে
যাচাই করে দেখুন। লক্ষ্য করুন 0.1-এর মতো এখানেও mantissa-র
প্যাটার্ন পুনরাবৃত্ত (01 বনাম 0.1-এর 1001) — কারণ দুটোই
আসলে একই ধরনের সমস্যা: হর-এ 2-বহির্ভূত মৌলিক উৎপাদক থাকা
ভগ্নাংশ, যা বাইনারিতে কখনো সসীম হতে পারে না।
এরপর কী
এরপর কী — যখন এই ছোট্ট error-গুলো জমতে শুরু করে
এই লেসনে আমরা দেখেছি 0.1f-এর আসল stored মান
0.100000001490116119384765625 — 0.1-এর চেয়ে সামান্য বেশি।
একটা সংখ্যায় এই পার্থক্য অকল্পনীয়ভাবে ছোট — কার্যত অদৃশ্য।
কিন্তু প্রশ্ন হলো: এই ছোট্ট error যখন হাজার, লক্ষ, কোটিবার arithmetic operation-এর মধ্য দিয়ে যায়, তখন কী হয়? কখনো তারা একে অপরকে বাতিল করে, কখনো জমতে থাকে, আর কখনো কখনো একটা একক subtraction-ই বিশাল একটা relative error তৈরি করে দেয় (catastrophic cancellation)।
পরের লেসনে আমরা দেখব:
- কেন
0.1 + 0.2 \ne 0.3— এই লেসনের bit-level knowledge দিয়ে সরাসরি প্রমাণ - Catastrophic cancellation — দুইটা প্রায়-সমান বড় সংখ্যা বিয়োগ করলে কীভাবে নির্ভুলতা ধ্বংস হয়, একটা সম্পূর্ণ hand-worked উদাহরণ সহ
- Machine epsilon আর কেন float-দুটোকে কখনো সরাসরি
==দিয়ে তুলনা করা উচিত নয় - Kahan summation — কীভাবে একটা চতুর algorithm accumulated rounding error প্রায় বাতিল করে দেয়
- ১৯৯১ সালের Patriot missile ব্যর্থতা — একটা truncated
0.1-এর error কীভাবে ১০০ ঘণ্টা ধরে জমে একটা মারাত্মক ব্যর্থতায় রূপ নিয়েছিল
আজকের লেসনের প্রতিটা bit layout, প্রতিটা rounding rule — সবকিছুই সেই গল্পগুলোর ভিত্তি।
আরও পড়ুন
- IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019) — IEEE Computer Society · মূল standard — প্রতিটা bit field-এর আনুষ্ঠানিক সংজ্ঞা
- What Every Computer Scientist Should Know About Floating-Point Arithmetic — David Goldberg (1991) · IEEE 754-এর সবচেয়ে বেশি উদ্ধৃত, প্রামাণ্য ব্যাখ্যা
- Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic — William Kahan · standard-এর মূল স্থপতির নিজের বয়ানে ডিজাইন সিদ্ধান্তগুলোর যুক্তি