এই পর্বে যা শিখবেন
- fixed point বনাম floating point — পরিসর আর নির্ভুলতার দর-কষাকষি
- IEEE 754 single আর double-এর বিট-বিন্যাস, bias আর hidden bit — হাতে encode আর decode (পরীক্ষার প্রিয় প্রশ্ন)
- exponent field-এর পুরো টেবিল: ±0, ±∞, quiet আর signalling NaN, subnormal আর gradual underflow
- নির্ভুলতার হিসাব: significant digit, machine epsilon, ULP আর অসম সংখ্যারেখা — প্রমাণসহ
- AI-এর ছোট format (FP16, bfloat16, FP8), decimal floating point আর টাকার হিসাবের সঠিক উপায়
১ · কেন floating point — বিশাল পরিসর আর বৈজ্ঞানিক নোটেশন
রসায়ন বইয়ে Avogadro সংখ্যা লেখা থাকে ৬.০২ × ১০২৩, আর একটি ইলেকট্রনের ভর প্রায় ৯.১১ × ১০−৩১ কেজি। এই দুটি সংখ্যা পুরো অঙ্কে লিখতে প্রথমটিতে ২৪ অঙ্ক আর দ্বিতীয়টিতে দশমিকের পরে ৩০টি শূন্য লাগত। বিজ্ঞানীরা তাই বৈজ্ঞানিক নোটেশন ব্যবহার করেন: কয়েকটি অর্থপূর্ণ অঙ্ক (৬.০২) আর একটি ঘাত (১০২৩)। অর্থপূর্ণ অঙ্কগুলো বলে “কত নিখুঁত”, আর ঘাত বলে “কত বড়”।
পর্ব ০৫-এ দেখা integer এই কাজ পারে না। ৩২ বিটের unsigned integer-এর পরিসর ০ থেকে প্রায় ৪.৩ × ১০৯, আর ভগ্নাংশ একেবারেই নেই। অথচ পদার্থবিজ্ঞানের simulation, ছবির রং, শব্দের তরঙ্গ, GPS-এর অবস্থান আর AI মডেলের ওজন — সবখানে দরকার ভগ্নাংশ আর বিশাল পরিসর।
প্রথম উপায়: fixed point
সবচেয়ে সরল উপায় হলো দশমিক বিন্দুকে একটি নির্দিষ্ট জায়গায় “আঁকা” ধরে নেওয়া। একে বলে fixed point. দোকানের হিসাবখাতায় যেমন সব দাম পয়সায় লেখা হয় আর মনে মনে শেষ দুই অঙ্কের আগে বিন্দু বসানো হয় — ১০৫০৭৫ মানে ৳১০৫০.৭৫। Binary-তে একই কাজ করে Q format: Q8.8 মানে ১৬ বিটের signed integer, যার নিচের ৮ বিট ভগ্নাংশ। এর পরিসর −১২৮ থেকে প্রায় ১২৭.৯৯৬, আর প্রতিটি ধাপ ঠিক ১/২৫৬ = ০.০০৩৯০৬২৫। যোগ-বিয়োগ সাধারণ integer-এর মতোই, তাই খুব সস্তা circuit-এ চলে — DSP chip আর ছোট microcontroller-এ fixed point আজও জনপ্রিয়। Q format-এ গুণ করলে বিন্দু কোথায় সরে আর কীভাবে আবার shift করতে হয়, তা পর্ব ২১-এর গুণের আলোচনায় পাবেন।
কিন্তু fixed point-এর সমস্যা হলো ধাপ সব জায়গায় সমান। Q8.8-এ ১০০-এর কাছেও ধাপ ১/২৫৬, আর ০.০০১-এর কাছেও ধাপ ১/২৫৬ — অর্থাৎ ০.০০১ রাখতে গেলে প্রায় পুরোটাই হারিয়ে যায় (সবচেয়ে কাছের মান ০ বা ০.০০৩৯)। ছোট সংখ্যার জন্য আপেক্ষিক ভুল বিশাল, আর বড় সংখ্যা (৩০০) আঁটেই না।
দ্বিতীয় উপায়: বিন্দুকে ভাসতে দেওয়া
Floating point-এর বুদ্ধি হলো, বিটগুলোকে দুই ভাগে ভাগ করা: একটি অংশ রাখে অর্থপূর্ণ অঙ্ক (significand, পুরোনো বইয়ে mantissa), আরেকটি রাখে ঘাত (exponent). Exponent বদলালে বিন্দু ডানে-বাঁয়ে সরে — “ভাসে”। ফলে ছোট সংখ্যার কাছে ধাপ ছোট, বড় সংখ্যার কাছে ধাপ বড়, কিন্তু আপেক্ষিক নির্ভুলতা প্রায় সব জায়গায় সমান।
দোকানের ৮ অঙ্কের ক্যালকুলেটর ভাবুন। এতে ১২৩৪৫৬৭৮ লেখা যায়, ০.০০০০০১২ও লেখা যায়, কিন্তু ১২৩৪৫৬৭৮.৯ লেখা যায় না — জায়গা মাত্র ৮ অঙ্কের। বিজ্ঞানের ক্যালকুলেটর এর সমাধান করে “1.2345679 E 7” দেখিয়ে: অঙ্ক সেই ৮টিই, কিন্তু E-এর পরের সংখ্যা বিন্দুটিকে যেখানে দরকার সেখানে সরিয়ে নেয়। শেষ অঙ্কটি গোল করা হয়েছে — আর এই “গোল করা”-ই floating point-এর সব আনন্দ আর সব বিপদের উৎস।
| বৈশিষ্ট্য | Fixed point (যেমন Q16.16, ৩২ বিট) | Floating point (IEEE single, ৩২ বিট) |
|---|---|---|
| পরিসর | প্রায় ±৩২৭৬৮ | প্রায় ±৩.৪ × ১০৩৮ |
| সবচেয়ে ছোট ধনাত্মক মান | ২−১৬ ≈ ১.৫ × ১০−৫ | ২−১৪৯ ≈ ১.৪ × ১০−৪৫ |
| ধাপের মাপ | সব জায়গায় সমান (absolute) | মানের অনুপাতে (relative), প্রায় ৭ দশমিক অঙ্ক |
| Hardware | সাধারণ integer ALU | আলাদা FPU — বড়, বেশি শক্তি খায় |
| কোথায় | DSP, পুরোনো গেম-কনসোল, ছোট microcontroller, টাকার হিসাব | বিজ্ঞান, গ্রাফিক্স, AI, প্রায় সব সাধারণ প্রোগ্রাম |
২ · Binary বৈজ্ঞানিক নোটেশন, hidden bit আর IEEE 754-এর জন্ম
দশমিকে ১৩.৬২৫ = ১.৩৬২৫ × ১০১. Binary-তেও একই কাজ হয়, শুধু ভিত্তি ২। পর্ব ০৪-এর বারবার ভাগ আর বারবার গুণের নিয়মে ১৩.৬২৫ = 1101.1012. বিন্দুকে তিন ঘর বাঁয়ে সরালে:
1101.1012 = 1.1011012 × 23বিন্দুর বাঁয়ে ঠিক একটি অশূন্য অঙ্ক রেখে লেখার এই রূপকে বলে normalized রূপ। দশমিকে বাঁয়ের অঙ্কটি ১ থেকে ৯-এর যেকোনোটি হতে পারে, কিন্তু binary-তে অশূন্য অঙ্ক একটিই — 1. তাই প্রতিটি normalized binary সংখ্যা 1 দিয়ে শুরু হয়। যে বিট সবসময় 1, তা মেমরিতে লিখে রাখার কোনো মানে নেই। IEEE 754 তাই সেটি বাদ দেয় — একে বলে hidden bit বা implicit leading 1. ফলে ২৩টি বিট জায়গা দিয়ে ২৪ বিটের নির্ভুলতা পাওয়া যায়: একটি বিট বিনা খরচে।
সাধারণ রূপটি তাহলে:
1.f2 × 2eএখানে s চিহ্ন-বিট (0 ধনাত্মক, 1 ঋণাত্মক), f হলো বিন্দুর পরের বিটগুলো (fraction), আর e আসল সূচক। লক্ষ করুন, চিহ্ন আলাদা একটি বিট — এটি পর্ব ০৫-এর sign-magnitude চিন্তা, two's complement নয়। তাই +x আর −x-এর মধ্যে পার্থক্য কেবল একটি বিট।
বিশৃঙ্খলা থেকে চুক্তি। ১৯৬০ আর ৭০-এর দশকে প্রায় প্রতিটি কম্পিউটার কোম্পানির নিজস্ব floating-point format ছিল। IBM System/360 ব্যবহার করত ভিত্তি ১৬ (hex) — ফলে নির্ভুলতা সংখ্যাভেদে কমবেশি হতো; DEC-এর VAX-এর ছিল নিজস্ব F, D আর G format; CDC আর Cray-র মেশিনে গোল করার নিয়মই আলাদা ছিল। একই Fortran প্রোগ্রাম ভিন্ন মেশিনে ভিন্ন উত্তর দিত, আর কোনো মেশিনে x != y হলেও x - y শূন্য হতে পারত। ১৯৭০-এর দশকের শেষে Intel তাদের 8087 coprocessor-এর নকশায় University of California, Berkeley-র অধ্যাপক William Kahan-এর পরামর্শ নেয়, আর প্রায় একই সময়ে IEEE একটি কমিটি গড়ে। Kahan, Jerome Coonen আর Harold Stone-এর খসড়া (প্রচলিত নাম “K-C-S”) দীর্ঘ বিতর্কের পরে IEEE 754-1985 হিসেবে গৃহীত হয়; বিশেষত subnormal নিয়ে DEC-এর পক্ষ থেকে জোরালো আপত্তি ছিল বলে জানা যায়। Kahan-কে এই standard-এর প্রধান কারিগর হিসেবে ব্যাপকভাবে কৃতিত্ব দেওয়া হয়, আর ১৯৮৯ সালে তিনি Turing Award পান। ২০০৮ সালের সংশোধনে যোগ হয় fused multiply-add, ১৬ আর ১২৮ বিটের binary format আর decimal format; ২০১৯ সালের সংশোধনটি তুলনায় ছোট — মূলত স্পষ্টীকরণ আর কিছু নতুন “recommended” operation.
আজ x86-64-এর SSE/AVX, ARM-এর FP/NEON আর RISC-V-র F ও D extension — সবাই IEEE 754 মানে। RISC-V-তে float-এর জন্য আলাদা ৩২টি register (f0–f31) আর flw/fsw, fadd.s-এর মতো instruction আছে; এদের কয়েকটি (যেমন fadd.s, fmadd.s) পর্ব ২৪-এ আসবে। এই পর্বে আমাদের প্রশ্ন একটিই: বিটগুলো ঠিক কীভাবে সাজানো।
৩ · Single আর double precision — বিটের নকশা আর হাতে encode
IEEE 754-এর দুটি প্রধান format হলো single precision (৩২ বিট, C-র float) আর double precision (৬৪ বিট, C-র double, JavaScript-এর Number, Python-এর float). দুটির নকশা একই — তিনটি field পাশাপাশি:
| Format | sign / exponent / fraction | bias | আসল e-এর পরিসর (normal) | নির্ভুলতা |
|---|---|---|---|---|
| single (binary32) | ১ / ৮ / ২৩ | ১২৭ | −১২৬ থেকে +১২৭ | ২৪ বিট ≈ ৭.২২ দশমিক অঙ্ক |
| double (binary64) | ১ / ১১ / ৫২ | ১০২৩ | −১০২২ থেকে +১০২৩ | ৫৩ বিট ≈ ১৫.৯৫ দশমিক অঙ্ক |
Exponent কেন biased — two's complement নয় কেন?
আসল সূচক e ঋণাত্মকও হয় (০.১-এর জন্য e = −৪)। IEEE 754 e-কে সরাসরি না রেখে রাখে E = e + bias — পর্ব ০৫-এর excess-K পদ্ধতি, যেখানে single-এর bias ১২৭। কেন? কারণ তাতে E একটি সাধারণ unsigned সংখ্যা হয়ে যায়, আর ছোট সূচক মানে ছোট E. ফলে দুটি ধনাত্মক float-এর বিট-প্যাটার্নকে সাধারণ unsigned integer হিসেবে তুলনা করলেই বড়-ছোট বোঝা যায় — exponent ওপরের বিটে, fraction নিচে, তাই integer comparator নিজেই আগে exponent, পরে fraction মেলায়। এর ব্যবহার অংশ ৭-এ দেখবেন। ৮ বিটে E = ০ থেকে ২৫৫; কিন্তু ০ আর ২৫৫ বিশেষ মানের জন্য সংরক্ষিত (অংশ ৫), তাই normal সংখ্যায় E = ১…২৫৪, অর্থাৎ e = −১২৬…+১২৭।
হাতে encode — −১৩.৬২৫ থেকে বিট
বিশ্ববিদ্যালয় আর চাকরির পরীক্ষায় সবচেয়ে বেশি আসা প্রশ্ন: “−১৩.৬২৫-কে IEEE 754 single precision-এ লিখুন, hex-এ উত্তর দিন।” প্রতিটি ধাপে “পরে” চাপুন:
x = -13.625 is negative
sign bit s = 1
from now on work with |x| = 13.625
integer 13 = 8 + 4 + 1 -> 1101
fraction .625 x 2 = 1.25 -> 1
.25 x 2 = 0.5 -> 0
.5 x 2 = 1.0 -> 1 (stops)
13.625 = 1101.101 (base 2)
1101.101 = 1.101101 x 2^3 (point moved 3 places left) hidden bit = 1, f = 101101...
E = e + 127 = 3 + 127 = 130 130 = 128 + 2 = 1000 0010
drop the hidden 1: 101101
pad to 23 bits: 10110100000000000000000
(no bits left over -> exact, no rounding)
s │ E │ f 1 │ 10000010 │ 10110100000000000000000 groups of 4: 1100 0001 0101 1010 0000 0000 0000 0000
1100 0001 0101 1010 0000 ...
C 1 5 A 0 0 0 0
-13.625f = 0xC15A0000
little-endian memory: 00 00 5A C1
- চিহ্ন আলাদা করা। সংখ্যাটি ঋণাত্মক, তাই s = 1. Sign-magnitude বলে বাকি কাজ হয় শুধু মান ১৩.৬২৫ নিয়ে — two's complement-এর মতো সব বিট উল্টাতে হয় না।
- Binary-তে লেখা। পূর্ণ অংশ ১৩ = ৮ + ৪ + ১ = 1101. ভগ্নাংশ ০.৬২৫-কে বারবার ২ দিয়ে গুণ: ১.২৫ (1), ০.৫ (0), ১.০ (1) — ভগ্নাংশ শূন্য হয়ে থেমে গেল। তাই ১৩.৬২৫ = 1101.101, একদম নির্ভুল।
- Normalize. বিন্দুকে ৩ ঘর বাঁয়ে সরিয়ে 1.101101 × 23. বাঁয়ের 1-টিই hidden bit; আসল সূচক e = ৩।
- Bias যোগ। E = ৩ + ১২৭ = ১৩০. ১৩০ = ১২৮ + ২, তাই ৮ বিটে 1000 0010.
- Fraction. hidden 1 বাদ দিলে থাকে 101101 — মাত্র ৬টি বিট। ডানে ১৭টি শূন্য বসিয়ে ২৩ বিট করা হলো। কোনো বিট বাড়তি থাকেনি, তাই rounding লাগেনি।
- জোড়া লাগানো। s (১ বিট), E (৮ বিট), f (২৩ বিট) পাশাপাশি — মোট ৩২ বিট। চার-চার বিটে ভাগ করলে 1100 0001 0101 1010, তারপর চারটি 0000.
- Hex. 1100 = C, 0001 = 1, 0101 = 5, 1010 = A, বাকি সব 0 — উত্তর
0xC15A0000. x86 বা RISC-V-র মতো little-endian মেশিনে মেমরিতে byte-গুলো উল্টো ক্রমে বসে:00 00 5A C1(পর্ব ০৬-এর endianness).
পরীক্ষার তিনটি সাধারণ ভুল: (১) bias যোগ করতে ভুলে যাওয়া (E = ৩ লেখা); (২) hidden 1-কে fraction-এর প্রথম বিট হিসেবে লিখে ফেলা — তখন সব বিট এক ঘর ডানে সরে যায়; (৩) fraction-এর শূন্যগুলো বাঁ দিকে বসানো। মনে রাখুন, fraction হলো বিন্দুর পরের বিট, বাঁ থেকে ডানে — শূন্য বসে ডানে।
৪ · Hex থেকে সংখ্যা — decode করা, আর একটি encoder-decoder
উল্টো প্রশ্নটিও সমান জনপ্রিয়: “মেমরিতে 0x40490FDB আছে; এটি float হলে মান কত?” Debugger-এ, network packet-এ বা ফাইলের hex dump-এ float এভাবেই দেখা যায়। নিয়ম একটাই — তিনটি field আলাদা করুন, তারপর সূত্র বসান।
4 0 4 9 0 F D B 0100 0000 0100 1001 0000 1111 1101 1011
s = 0 E = 1000 0000 f = 100 1001 0000 1111 1101 1011
E = 1000 0000 = 128 E is not 0 and not 255 -> normal e = E - 127 = 1
1.f = 1.10010010000111111011011 (base 2)
= 1 + 0x490FDB / 2^23
= 1 + 4788187 / 8388608
= 1.5707963705...
value = (-1)^0 x 1.5707963705... x 2^1
= 3.1415927410125732...
pi = 3.14159265358979...
stored = 3.14159274101257...
error = 8.74e-8
ULP at 2 = 2^-22 = 2.38e-7 (half = 1.19e-7)
-> 0x40490FDB is the float nearest to pi
- Binary-তে ছড়ানো। প্রতিটি hex অঙ্ক ঠিক ৪টি বিট: 4 = 0100, 0 = 0000, 4 = 0100, 9 = 1001, 0 = 0000, F = 1111, D = 1101, B = 1011. মোট ৩২ বিট।
- Field আলাদা। প্রথম বিট 0 — চিহ্ন। পরের ৮টি 1000 0000 — exponent. বাকি ২৩টি fraction. Hex-এর ৪-বিট সীমানা আর field-এর সীমানা মেলে না, তাই আগে binary-তে লেখা জরুরি।
- Exponent. 1000 0000 = ১২৮. এটি ০ বা ২৫৫ নয়, তাই সাধারণ normal সংখ্যা, hidden bit 1. আসল সূচক e = ১২৮ − ১২৭ = ১।
- Significand. fraction-এর ২৩ বিট hex-এ 0x490FDB = ৪৭৮৮১৮৭. তাই 1.f = ১ + ৪৭৮৮১৮৭ / ৮৩৮৮৬০৮ ≈ ১.৫৭০৭৯৬৩৭০৫ — চেনা লাগছে? এটি প্রায় π/২।
- মান। ধনাত্মক, × ২১: ১.৫৭০৭৯৬৩৭০৫… × ২ = ৩.১৪১৫৯২৭৪১০১২৫৭৩২… (হুবহু মান ৩.১৪১৫৯২৭৪১০১২৫৭৩২৪২১৮৭৫, কারণ প্রতিটি float একটি সসীম binary ভগ্নাংশ)।
- কতটা কাছে? আসল π থেকে পার্থক্য প্রায় ৮.৭৪ × ১০−৮. ২ থেকে ৪-এর মধ্যে পাশাপাশি দুটি float-এর দূরত্ব ২−২২ ≈ ২.৩৮ × ১০−৭, তার অর্ধেক ১.১৯ × ১০−৭. ভুলটি অর্ধেক ধাপের কম, তাই এটিই π-এর সবচেয়ে কাছের float — C-তে
(float)M_PIঠিক এই বিটগুলোই দেয়।
এবার হাতের দুটি হিসাব যন্ত্রে মিলিয়ে নিন। নিচের JavaScript simulator কোনো library ছাড়াই একটি দশমিক সংখ্যাকে ধাপে ধাপে single precision-এ encode করে — normalize, bias, ২৩ বিট কেটে নেওয়া আর বাড়তি বিট দেখে rounding — আর ফলটি ব্রাউজারের নিজস্ব Float32Array/DataView-এর সাথে মেলায়। দ্বিতীয় ফাংশনটি যেকোনো ৩২ বা ৬৪ বিটের hex pattern decode করে। Run চাপুন:
// IEEE 754 by hand: encode a decimal step by step, decode hex patterns
const dv = new DataView(new ArrayBuffer(8)), b2 = (v, w) => v.toString(2).padStart(w, "0");
function encode32(x) { // normal numbers only
const s = x < 0 ? 1 : 0; let a = Math.abs(x), e = 0;
while (a >= 2) { a /= 2; e++; } // normalise: 1 <= a < 2
while (a < 1) { a *= 2; e--; }
const exact = (a - 1) * 2 ** 23; let f = Math.floor(exact);
const rest = exact - f; // the bits that do not fit
if (rest > 0.5 || (rest === 0.5 && f % 2 === 1)) f++; // nearest, ties to even
if (f === 2 ** 23) { f = 0; e++; } // rounding carried into exponent
const E = e + 127, bits = ((s << 31) | (E << 23) | f) >>> 0;
dv.setFloat32(0, x);
console.log(`${x}: sign ${s} | ${a.toString(2).slice(0, 18)}... x 2^${e} | E = ${e}+127 = ${E} = ${b2(E, 8)}`);
console.log(` fraction ${b2(f, 23)} (rounding rest ${rest.toFixed(2)})`);
console.log(` = 0x${bits.toString(16).toUpperCase()} Float32Array says 0x${dv.getUint32(0).toString(16).toUpperCase()}`);
}
function decode(hex, bitsW) { // bitsW = 32 or 64
const [EW, FW, bias] = bitsW === 32 ? [8, 23, 127] : [11, 52, 1023];
const b = BigInt(hex), s = Number(b >> BigInt(bitsW - 1));
const E = Number((b >> BigInt(FW)) & ((1n << BigInt(EW)) - 1n));
const f = b & ((1n << BigInt(FW)) - 1n), max = 2 ** EW - 1;
const v = bitsW === 32 ? (dv.setUint32(0, Number(b)), dv.getFloat32(0))
: (dv.setBigUint64(0, b), dv.getFloat64(0));
const kind = E === max ? (f ? "NaN" : "infinity") : E === 0 ? (f ? "subnormal" : "zero")
: `normal, 1.f x 2^(${E}-${bias}) = 2^${E - bias}`;
console.log(`${hex.padEnd(18)} s=${s} E=${E} f=0x${f.toString(16)} ${kind} -> ${Object.is(v, -0) ? "-0" : v}`);
}
[-13.625, 0.1, 16777217, 1e30].forEach(encode32);
["0x40490FDB", "0xC15A0000", "0x00000001", "0x80000000", "0x7F800000", "0x7FC00000"]
.forEach(h => decode(h, 32));
["0x3FB999999999999A", "0x400921FB54442D18"].forEach(h => decode(h, 64));আউটপুট (নির্ধারিত — প্রতিবার একই):
-13.625: sign 1 | 1.101101... x 2^3 | E = 3+127 = 130 = 10000010
fraction 10110100000000000000000 (rounding rest 0.00)
= 0xC15A0000 Float32Array says 0xC15A0000
0.1: sign 0 | 1.1001100110011001... x 2^-4 | E = -4+127 = 123 = 01111011
fraction 10011001100110011001101 (rounding rest 0.80)
= 0x3DCCCCCD Float32Array says 0x3DCCCCCD
16777217: sign 0 | 1.0000000000000000... x 2^24 | E = 24+127 = 151 = 10010111
fraction 00000000000000000000000 (rounding rest 0.50)
= 0x4B800000 Float32Array says 0x4B800000
1e+30: sign 0 | 1.1001001111100101... x 2^99 | E = 99+127 = 226 = 11100010
fraction 10010011111001011001010 (rounding rest 0.80)
= 0x7149F2CA Float32Array says 0x7149F2CA
0x40490FDB s=0 E=128 f=0x490fdb normal, 1.f x 2^(128-127) = 2^1 -> 3.1415927410125732
0xC15A0000 s=1 E=130 f=0x5a0000 normal, 1.f x 2^(130-127) = 2^3 -> -13.625
0x00000001 s=0 E=0 f=0x1 subnormal -> 1.401298464324817e-45
0x80000000 s=1 E=0 f=0x0 zero -> -0
0x7F800000 s=0 E=255 f=0x0 infinity -> Infinity
0x7FC00000 s=0 E=255 f=0x400000 NaN -> NaN
0x3FB999999999999A s=0 E=1019 f=0x999999999999a normal, 1.f x 2^(1019-1023) = 2^-4 -> 0.1
0x400921FB54442D18 s=0 E=1024 f=0x921fb54442d18 normal, 1.f x 2^(1024-1023) = 2^1 -> 3.141592653589793
প্রথম তিন লাইনে stepper-এর উত্তর: E = ১৩০ = 10000010, fraction 101101 আর শূন্য, ফল 0xC15A0000 — Float32Array-ও একই বলছে, আর “rounding rest 0.00” মানে কোনো বিট বাদ পড়েনি। 0.1-এর বেলায় ২৩ বিটের পরেও বিট থেকে যায়; বাদ পড়া অংশ ০.৮০ ধাপ, অর্থাৎ অর্ধেকের বেশি, তাই শেষ বিট এক বাড়ে: ...1100 হয়ে যায় ...1101, ফল 0x3DCCCCCD. 16777217 = ২২৪ + ১-এর বাদ পড়া অংশ ঠিক ০.৫০ — দুই প্রতিবেশীর ঠিক মাঝখানে! তখন নিয়ম “ties to even”: যে প্রতিবেশীর শেষ বিট 0, সেটি নেওয়া হয় — fraction সব শূন্য, মান ১৬৭৭৭২১৬ (0x4B800000). Decode অংশে π, −১৩.৬২৫, সবচেয়ে ছোট subnormal (0x00000001 ≈ ১.৪ × ১০−৪৫), −0, ∞ আর NaN; শেষ দুই লাইন double-এর: 0x3FB999999999999A হলো ০.১, আর 0x400921FB54442D18 হলো double-এর π, যা float-এর চেয়ে অনেক বেশি অঙ্ক পর্যন্ত মেলে। (simulator-টি শুধু normal সংখ্যা encode করে, আর ইনপুট আগে double-এ গোল হয় — খুব বিরল কিছু ক্ষেত্রে এই “দুবার গোল করা” এক বিট পার্থক্য আনতে পারে।)
৫ · বিশেষ মান — ±0, ±∞, NaN আর subnormal
Exponent field-এর দুটি মান — সব 0 আর সব 1 — IEEE 754 সাধারণ সংখ্যার জন্য রাখেনি। এই দুটি দিয়ে সে এমন জিনিস প্রকাশ করে, যা কোনো integer format পারে না। পুরো টেবিলটি মুখস্থ রাখার মতো:
| E (single-এ ৮ বিট) | f | অর্থ | মান | উদাহরণ (hex) |
|---|---|---|---|---|
| ০ (সব 0) | ০ | শূন্য | ±0 | 00000000, 80000000 |
| ০ (সব 0) | ≠ ০ | subnormal | ±0.f × ২−১২৬ | 00000001 ≈ ১.৪ × ১০−৪৫ |
| ১ … ২৫৪ | যেকোনো | normal | ±1.f × ২E−১২৭ | 3F800000 = ১.০ |
| ২৫৫ (সব 1) | ০ | অসীম | ±∞ | 7F800000, FF800000 |
| ২৫৫ (সব 1) | ≠ ০ | NaN | “সংখ্যা নয়” | 7FC00000 (quiet) |
Double-এ একই টেবিল, শুধু E-এর সব-1 মান ২০৪৭, আর subnormal-এর সূচক −১০২২।
±0 — শূন্যেরও চিহ্ন আছে
চিহ্ন আলাদা বিট বলে 0x00000000 (+0) আর 0x80000000 (−0) দুটিই বৈধ। তুলনায় -0.0 == 0.0 সত্য, কিন্তু আচরণে পার্থক্য আছে: 1/+0 = +∞ আর 1/−0 = −∞. কেন রাখা হলো? একটি খুব ছোট ঋণাত্মক সংখ্যা underflow হয়ে শূন্য হলে −0 অন্তত বলে দেয় “এটি নিচের দিক থেকে এসেছে”। জটিল সংখ্যার ফাংশনে (যেমন বর্গমূল বা log-এর “branch cut”-এর দুই পাশে) এই চিহ্ন সঠিক উত্তর বেছে নিতে সাহায্য করে — Kahan এ নিয়ে ১৯৮৭ সালে একটি বিখ্যাত প্রবন্ধও লিখেছিলেন। দৈনন্দিন ভাষায়: তাপমাত্রা −০.০০০১° থেকে গোল হয়ে “−০°” দেখালে বোঝা যায় বরফ গলেনি।
±∞ — overflow-এর ভদ্র উত্তর
সবচেয়ে বড় finite float প্রায় ৩.৪ × ১০৩৮ (0x7F7FFFFF = (২ − ২−২৩) × ২১২৭); double-এ প্রায় ১.৮ × ১০৩০৮. এর চেয়ে বড় ফল হলে প্রোগ্রাম থামে না, ফল হয় ∞ — আর ∞ নিয়ে হিসাব চলতে থাকে নিয়মমতো: ∞ + ১ = ∞, ১/∞ = ০, আর ধনাত্মক সংখ্যা/০ = +∞. পর্ব ০৫-এ দেখা integer overflow-এর মতো চুপচাপ ঘুরে ঋণাত্মক হয়ে যায় না — এটি floating point-এর একটি বড় নিরাপত্তা-গুণ।
NaN — “এটি কোনো সংখ্যা নয়”
০/০, ∞ − ∞, ০ × ∞ বা ঋণাত্মক সংখ্যার বর্গমূল (বাস্তব সংখ্যায়) — এদের কোনো অর্থপূর্ণ উত্তর নেই। IEEE 754 এখানে দেয় NaN (Not a Number): E সব 1, f অশূন্য। NaN-এর তিনটি বৈশিষ্ট্য পরীক্ষায় আর ইন্টারভিউতে প্রায়ই আসে:
- সংক্রামক: NaN-এর সাথে যেকোনো হিসাবের ফল NaN — একটি ভুল পুরো হিসাবের শেষ পর্যন্ত বয়ে যায়, হারিয়ে যায় না।
- নিজের সমানও নয়:
NaN == NaNমিথ্যা,NaN != NaNসত্য, আর<,>সবই মিথ্যা (“unordered”). তাইx != xহলো NaN চেনার পুরোনো কৌশল; আধুনিক কোডেisnan(x)ব্যবহার করুন। - দুই রকম: quiet NaN নীরবে ছড়ায়; signalling NaN ব্যবহার হলেই একটি “invalid operation” exception-flag তোলে — যেমন অ-initialized মেমরি চিহ্নিত করতে। ২০০৮-এর standard সুপারিশ করে fraction-এর সবচেয়ে বড় বিট 1 মানে quiet (তাই
0x7FC00000quiet,0x7F800001signalling); কিছু পুরোনো architecture (যেমন পুরোনো MIPS) উল্টো নিয়ম মানত বলে জানা যায়।
NaN-এর বাকি ২২টি fraction বিট (double-এ ৫১টি) “payload” — চাইলে এখানে তথ্য লুকানো যায়। কিছু JavaScript engine (যেমন Firefox-এর SpiderMonkey) এই কৌশলে (“NaN-boxing”) pointer আর ছোট integer-কে double-এর NaN-এর ভেতরে রাখে।
Subnormal — শূন্যের কাছে ধীরে ধীরে নামা
সবচেয়ে ছোট normal float ২−১২৬ ≈ ১.১৮ × ১০−৩৮. এর নিচে কী? যদি সরাসরি ০-তে লাফ দিত, তাহলে ০ আর ২−১২৬-এর মাঝে একটি বিশাল ফাঁক থাকত — অথচ ২−১২৬-এর ঠিক ওপরে পাশাপাশি float-এর দূরত্ব মাত্র ২−১৪৯. IEEE 754-এর সমাধান subnormal (পুরোনো নাম denormal) সংখ্যা: E = ০ হলে hidden bit 1 নয় 0, আর সূচক স্থির −১২৬:
0.f2 × 2−126এর ফলে শূন্যের কাছের মানগুলো সমান দূরত্বে (২−১৪৯) সাজানো — fixed point-এর মতো — আর নির্ভুলতা ধীরে ধীরে কমে। একে বলে gradual underflow. নিচের stepper-এ দেখুন এটি কোন সমস্যার সমাধান করে:
0x00800000 = 0 00000001 000...0
E = 1 -> 1.0 x 2^(1-127) = 2^-126
= 1.1754944e-38
x = 0x00C00000 = 1.5 x 2^-126 = 1.7632415e-38 y = 0x00800000 = 1.0 x 2^-126 = 1.1754944e-38 x != y
x - y = 0.5 x 2^-126 = 2^-127 = 5.877472e-39
smaller than the smallest normal (2^-126)
-> no normal float can hold it
E = 0 means 0.f x 2^-126
2^-127 = 0.1 (base 2) x 2^-126
f = 100 0000 0000 0000 0000 0000
x - y = 0x00400000 (exact!)
flush-to-zero: x - y -> 0
x != y but x - y == 0
if (x != y) z = 1 / (x - y); -> z = inf !
largest subnormal 0x007FFFFF = 1.1754942e-38 smallest subnormal 0x00000001 = 2^-149 = 1.4e-45 spacing everywhere below 2^-126: 2^-149 (2^-149) / 2 -> tie -> rounds to even -> 0
- Normal-এর তলা। E = 1, f = 0 — এটিই সবচেয়ে ছোট normal: ২−১২৬ ≈ ১.১৭৫৪৯৪৪ × ১০−৩৮, C-তে
FLT_MIN. - দুটি সংখ্যা নিই। x = ১.৫ × ২−১২৬ (fraction-এর প্রথম বিট 1), y = ২−১২৬. দুটিই normal, আর স্পষ্টতই x ≠ y.
- বিয়োগ। x − y = ০.৫ × ২−১২৬ = ২−১২৭ ≈ ৫.৮৮ × ১০−৩৯. Normal রূপে এর জন্য E = ০ লাগত — যা normal-এর জন্য নিষিদ্ধ।
- Subnormal বাঁচায়। E = ০ মানে 0.f × ২−১২৬. ২−১২৭ = 0.12 × ২−১২৬, তাই f-এর প্রথম বিট 1, বাকি 0 —
0x00400000. ফলটি একদম নির্ভুল। - Flush-to-zero হলে বিপদ। subnormal বাদ দিলে ফলটি ০ হয়ে যেত। তখন “x ≠ y অথচ x − y = ০” — আর
if (x != y)দিয়ে রক্ষা করা ভাগও শূন্য দিয়ে ভাগ হয়ে যেত। subnormal থাকলে প্রমাণ করা যায়: x ≠ y হলে x − y কখনো ০ নয়। - শেষ প্রান্ত। সবচেয়ে বড় subnormal
0x007FFFFF≈ ১.১৭৫৪৯৪২ × ১০−৩৮, সবচেয়ে ছোট0x00000001= ২−১৪৯ ≈ ১.৪ × ১০−৪৫ (C-তেFLT_TRUE_MIN). এর অর্ধেক ০ আর ২−১৪৯-এর ঠিক মাঝে — ties to even নিয়মে ফল ০. এখানেই underflow সম্পূর্ণ।
Double-এ একই চিত্র: সবচেয়ে ছোট normal ২−১০২২ ≈ ২.২২ × ১০−৩০৮, সবচেয়ে ছোট subnormal ২−১০৭৪ ≈ ৪.৯৪ × ১০−৩২৪. মূল্যটাও জানা দরকার: অনেক x86 CPU-তে subnormal ফল তৈরি হলে hardware একটি ধীর “microcode assist” পথে যায়, যাতে শতাধিক cycle পর্যন্ত লাগতে পারে। তাই গতি-প্রধান কাজে (গেম, অডিও, GPU) প্রায়ই flush-to-zero চালু রাখা হয় — x86-এর SSE-তে MXCSR register-এর FTZ আর DAZ বিট দিয়ে। এটি standard-এর বাইরে একটি সচেতন আপস।
এবার C দিয়ে সবকিছু একসাথে দেখি। প্রোগ্রামটি প্রতিটি float-এর বিট memcpy দিয়ে একটি uint32_t-এ আনে (type punning-এর নিরাপদ উপায়, পর্ব ০৬-এ 1.0f-এর byte দেখার সময় যেমন করেছিলেন), তিনটি field আলাদা করে, শ্রেণি বলে আর শুধু field থেকে মানটি আবার বানায়:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <float.h>
#include <math.h> /* only for the INFINITY, NAN macros */
static double pow2(int k) { /* exact 2^k without libm */
double r = 1.0;
for (; k != 0; k += (k > 0) ? -1 : 1) r = (k > 0) ? r * 2.0 : r / 2.0;
return r;
}
static void show(const char *name, float x) {
uint32_t b;
memcpy(&b, &x, sizeof b); /* safe type punning */
unsigned s = b >> 31, e = (b >> 23) & 0xFFu, f = b & 0x7FFFFFu;
const char *kind = e == 255 ? (f ? "NaN" : "infinity")
: e == 0 ? (f ? "subnormal" : "zero") : "normal";
double v = e == 255 ? (f ? (double)NAN : (double)INFINITY)
: e == 0 ? f * pow2(-149) /* 0.f x 2^-126 */
: (f | 0x800000u) * pow2((int)e - 150); /* 1.f x 2^(e-127) */
printf("%-7s 0x%08X s=%u e=%3u f=0x%06X %-9s -> %s%.9g\n",
name, (unsigned)b, s, e, f, kind, s ? "-" : "", v);
}
int main(void) {
show("1.0", 1.0f); show("-13.625", -13.625f); show("0.1f", 0.1f);
show("1e-40f", 1e-40f); show("INF", INFINITY); show("NAN", NAN); show("-0.0f", -0.0f);
printf("FLT_EPSILON %.9g = 2^-23\nFLT_MIN %.9g = 2^-126\n",
(double)FLT_EPSILON, (double)FLT_MIN);
#ifdef FLT_TRUE_MIN
printf("FLT_TRUE_MIN %.9g = 2^-149\n", (double)FLT_TRUE_MIN);
#else
printf("FLT_TRUE_MIN %.9g = 2^-149 (computed)\n", pow2(-149));
#endif
printf("FLT_MAX %.9g\n", (double)FLT_MAX);
return 0;
}Wandbox-এর gcc (Linux x86-64, glibc) আউটপুট:
1.0 0x3F800000 s=0 e=127 f=0x000000 normal -> 1
-13.625 0xC15A0000 s=1 e=130 f=0x5A0000 normal -> -13.625
0.1f 0x3DCCCCCD s=0 e=123 f=0x4CCCCD normal -> 0.100000001
1e-40f 0x000116C2 s=0 e= 0 f=0x0116C2 subnormal -> 9.9999461e-41
INF 0x7F800000 s=0 e=255 f=0x000000 infinity -> inf
NAN 0x7FC00000 s=0 e=255 f=0x400000 NaN -> nan
-0.0f 0x80000000 s=1 e= 0 f=0x000000 zero -> -0
FLT_EPSILON 1.1920929e-07 = 2^-23
FLT_MIN 1.17549435e-38 = 2^-126
FLT_TRUE_MIN 1.40129846e-45 = 2^-149
FLT_MAX 3.40282347e+38
প্রতিটি লাইন exponent টেবিলের একটি সারি। 1.0 = E ১২৭, f ০. -13.625-এর 0xC15A0000 stepper-এর সাথে হুবহু মিলল। 0.1f ফিরে আসে 0.100000001 হয়ে — হুবহু ০.১ নয়। 1e-40f normal-এর তলার নিচে, তাই E = ০, subnormal: রাখা গেছে প্রায় ৯.৯৯৯৯৪৬ × ১০−৪১, নির্ভুলতা মাত্র ১৭ বিটের মতো। INFINITY = E ২৫৫, f ০; glibc-র NAN = 0x7FC00000 — fraction-এর বড় বিট 1, অর্থাৎ quiet NaN. -0.0f = শুধু চিহ্ন-বিট। শেষ চার লাইন <float.h>-এর ধ্রুবক: FLT_EPSILON = ২−২৩, FLT_MIN = ২−১২৬, FLT_TRUE_MIN (C11-এ যোগ হয়েছে; না থাকলে কোডটি নিজে হিসাব করে) = ২−১৪৯, আর FLT_MAX ≈ ৩.৪০২৮২৩৪৭ × ১০৩৮. লক্ষ করুন, field থেকে বানানো মান প্রতিবার মূল মানের সাথে মিলছে — সূত্রটি সত্যিই পুরো format বর্ণনা করে।
৬ · নির্ভুলতা — significant digit, machine epsilon আর ULP
পরিসর বড় হলেও প্রতিটি float-এ অর্থপূর্ণ বিট সীমিত: single-এ ২৪টি, double-এ ৫৩টি। এর অর্থ কী, তা তিনটি ধারণায় ধরা যায়।
ULP — পাশাপাশি দুটি float-এর দূরত্ব
একই exponent-এর সব float — অর্থাৎ ২e থেকে ২e+১-এর মাঝের একটি “binade” — সমান দূরত্বে বসানো, কারণ শুধু fraction-এর ২৩ বিট বদলায়। Fraction-এর সবচেয়ে ছোট বিটের মান ২−২৩, তাই সেই binade-এ ধাপ:
এই ধাপের নাম ULP (unit in the last place). ১-এর কাছে single-এ ULP = ২−২৩ ≈ ১.১৯ × ১০−৭; ১০২৪-এর কাছে ২−১৩ ≈ ১.২২ × ১০−৪; ১০ লাখের কাছে ০.০৬২৫; আর ২২৪ = ১৬৭৭৭২১৬-এর কাছে ঠিক ২. অর্থাৎ ১৬৭৭৭২১৬-এর পরের float ১৬৭৭৭২১৮ — মাঝের ১৬৭৭৭২১৭ float-এ রাখাই যায় না। এটিই সবচেয়ে ছোট ধনাত্মক integer, যা float-এ হুবহু থাকে না (double-এ সেটি ২৫৩ + ১)। তাই float-এ কোনো counter ১ করে বাড়াতে থাকলে ১৬৭৭৭২১৬-এ পৌঁছে সেটি আটকে যায় — f + 1.0f == f.
ঢাকা থেকে রাজশাহীর রেললাইনের পাশে কিলোমিটার-ফলক ভাবুন — কিন্তু অদ্ভুত নিয়মে বসানো: স্টেশনের প্রথম ১ কিমিতে প্রতি মিটারে একটি ফলক, পরের ২ কিমিতে প্রতি ২ মিটারে, পরের ৪ কিমিতে প্রতি ৪ মিটারে… প্রতিবার দূরত্ব দ্বিগুণ হলে ফলকের ফাঁকও দ্বিগুণ। প্রতিটি অংশে ফলকের সংখ্যা সমান (single-এ ২২৩টি)। আপনার অবস্থান জানানো যায় শুধু সবচেয়ে কাছের ফলক দিয়ে — স্টেশনের কাছে মিটারের নির্ভুলতায়, দূরে গেলে কিলোমিটারের। floating-point সংখ্যারেখা ঠিক এমন: সমান নয়, লগারিদমিকভাবে ছড়ানো।
Machine epsilon আর আপেক্ষিক ভুলের সীমা — একটি ছোট প্রমাণ
Machine epsilon ε হলো ১ আর তার পরের float-এর দূরত্ব: single-এ ε = ২−২৩ ≈ ১.১৯ × ১০−৭ (C-র FLT_EPSILON), double-এ ২−৫২ ≈ ২.২২ × ১০−১৬ (DBL_EPSILON). এখন দাবি: যেকোনো বাস্তব সংখ্যা x (পরিসরের ভেতরে, normal অঞ্চলে) সবচেয়ে কাছের float-এ গোল করলে আপেক্ষিক ভুল সর্বোচ্চ ε/২।
প্রমাণ: ধরুন ২e ≤ |x| < ২e+১. ওই binade-এ পাশাপাশি float-এর দূরত্ব ২e−২৩ = ε · ২e. সবচেয়ে কাছের float বেছে নিলে ভুল সর্বোচ্চ অর্ধেক ধাপ, অর্থাৎ |fl(x) − x| ≤ ε · ২e / ২. দুই দিকে |x| দিয়ে ভাগ করি, আর |x| ≥ ২e ব্যবহার করি: |fl(x) − x| / |x| ≤ (ε · ২e / ২) / ২e = ε/২. ∎ এই ε/২ = ২−২৪ ≈ ৫.৯৬ × ১০−৮-কে বলে unit roundoff u. সাবধান: কিছু বই “machine epsilon” বলতে এই u-কেই বোঝায় — উত্তর লেখার আগে কোন সংজ্ঞা, তা উল্লেখ করুন।
দশমিক অঙ্কে কতটা? ২৪টি বিট মানে ২২৪ রকম অবস্থা; সমান সংখ্যক দশমিক অঙ্ক d হলে ১০d = ২২৪, তাই d = ২৪ × log১০২ ≈ ৭.২২. Double-এ ৫৩ × log১০২ ≈ ১৫.৯৫. ব্যবহারিক নিয়ম দুটি: যেকোনো ৬ অঙ্কের দশমিক সংখ্যা float-এ গিয়ে ফিরে এলে অক্ষত থাকে (FLT_DIG = ৬), কিন্তু একটি float-কে নির্ভুলভাবে আবার পড়তে চাইলে ৯ অঙ্ক ছাপতে হয় (FLT_DECIMAL_DIG = ৯)। Double-এ সংখ্যা দুটি ১৫ আর ১৭।
নিচের JavaScript simulator দেখায় মেমরিতে আসলে কী আছে: BigInt দিয়ে একটি double-এর হুবহু দশমিক মান বের করে (প্রতিটি double = m × ২k, আর ২−n = ৫n / ১০n, তাই দশমিক রূপ সবসময় সসীম), তারপর float-এর ULP মাপে আর ১৬৭৭৭২১৭-এর পরিণতি দেখায়:
// What is REALLY stored? Exact decimal value of a double, via BigInt
function exact(x) { // x > 0, finite
const dv = new DataView(new ArrayBuffer(8)); dv.setFloat64(0, x);
const b = dv.getBigUint64(0), E = Number((b >> 52n) & 0x7FFn);
let m = b & ((1n << 52n) - 1n), k = -1074; // value = m x 2^k
if (E) { m |= 1n << 52n; k = E - 1075; }
if (k >= 0) return (m << BigInt(k)).toString();
const d = (m * 5n ** BigInt(-k)).toString().padStart(1 - k, "0"); // m*2^k = m*5^-k / 10^-k
return (d.slice(0, d.length + k) + "." + d.slice(d.length + k)).replace(/0+$/, "");
}
for (const [name, v] of [["0.1", 0.1], ["0.2", 0.2], ["0.3", 0.3], ["0.1+0.2", 0.1 + 0.2],
["float(0.1)", Math.fround(0.1)]])
console.log(name.padEnd(10), exact(v));
console.log("0.1 + 0.2 === 0.3 ?", 0.1 + 0.2 === 0.3, "| toPrecision(17):", (0.1 + 0.2).toPrecision(17));
const f32 = new Float32Array(1), u32 = new Uint32Array(f32.buffer);
function ulp32(x) { f32[0] = x; u32[0] += 1; return f32[0] - x; } // gap to the next float
console.log("\nx float ULP double ULP");
for (const x of [1, 1024, 16777216, 1e10])
console.log(String(x).padEnd(12), String(ulp32(x)).padEnd(23), 2 ** (Math.floor(Math.log2(x)) - 52));
console.log("\nfloat(16777217) =", Math.fround(16777217), "| float(16777219) =", Math.fround(16777219));
let s = 0; for (let i = 0; i < 10; i++) s += 0.1;
console.log("ten times 0.1 =", s, "| ten times 10 poisha =", 10 * 10, "poisha = 1.00 taka");আউটপুট (নির্ধারিত — প্রতিবার একই):
0.1 0.1000000000000000055511151231257827021181583404541015625
0.2 0.200000000000000011102230246251565404236316680908203125
0.3 0.299999999999999988897769753748434595763683319091796875
0.1+0.2 0.3000000000000000444089209850062616169452667236328125
float(0.1) 0.100000001490116119384765625
0.1 + 0.2 === 0.3 ? false | toPrecision(17): 0.30000000000000004
x float ULP double ULP
1 1.1920928955078125e-7 2.220446049250313e-16
1024 0.0001220703125 2.2737367544323206e-13
16777216 2 3.725290298461914e-9
10000000000 1024 0.0000019073486328125
float(16777217) = 16777216 | float(16777219) = 16777220
ten times 0.1 = 0.9999999999999999 | ten times 10 poisha = 100 poisha = 1.00 taka
প্রথম পাঁচ লাইন হলো মেমরির সত্য। double-এর 0.1 আসলে ০.১০০০০০০০০০০০০০০০০৫৫৫…, 0.2 একটু বড়, আর 0.3 একটু ছোট (০.২৯৯৯৯…৮৮৮…)। 0.1 + 0.2-এর হুবহু যোগফল আবার গোল হয়ে যায় ০.৩০০০০০০০০০০০০০০০০৪৪৪…-এ — ০.৩-এর double থেকে ঠিক এক ULP ওপরে। তাই === বলে false, আর ১৭ অঙ্কে ছাপলে দেখা যায় 0.30000000000000004. float-এর ০.১ (0x3DCCCCCD) আরও দূরে: ০.১০০০০০০০১৪৯…. ULP টেবিলে float-এর ধাপ ১-এ ১.১৯ × ১০−৭, ১০২৪-এ ১.২২ × ১০−৪, ১৬৭৭৭২১৬-এ ২, আর ১০১০-এ ১০২৪ — মানের সাথে ধাপ বাড়ছে, কিন্তু double-এর ধাপ প্রতিবার ২২৯ গুণ ছোট। Math.fround(16777217) = ১৬৭৭৭২১৬ আর Math.fround(16777219) = ১৬৭৭৭২২০: দুটিই “ঠিক মাঝখানে”, আর ties to even যথাক্রমে নিচে আর ওপরে গেছে — যার fraction-এর শেষ বিট 0, সেদিকে। শেষ লাইন: ১০ বার ০.১ যোগ করলে 0.9999999999999999, অথচ ১০ বার ১০ পয়সা = ১০০ পয়সা, একদম ৳১.০০।
৭ · গোল করার নিয়ম, ০.১ + ০.২-এর রহস্য, বিটে সাজানো আর টাকার হিসাব
০.১ কেন হুবহু থাকে না, আর কোন দিকে গোল হয়
পর্ব ০৪-এ দেখেছেন, ০.১ binary-তে কখনো শেষ হয় না: 0.000110011001100…2. কারণ ১/১০-এর হরে ৫ আছে, আর binary-তে সসীম ভগ্নাংশ মানে হর শুধু ২-এর ঘাত। Normalize করলে ০.১ = 1.100110011001100…2 × ২−৪. Single-এ fraction-এর জায়গা ২৩ বিট:
kept 23 bits : 10011001100110011001100 | 1100 1100 ...
next bits : 1100... -> more than half an ULP
round up : 10011001100110011001101 -> 0x3DCCCCCD
IEEE 754-এর ডিফল্ট নিয়ম হলো round to nearest, ties to even: সবচেয়ে কাছের প্রতিনিধিত্বযোগ্য মান নিন; ঠিক মাঝখানে পড়লে সেটি নিন, যার শেষ বিট 0. “Even”-এর পক্ষে যুক্তি — স্কুলের “৫ হলে সবসময় ওপরে” নিয়মে অসংখ্য হিসাবে ভুল একদিকে জমে (bias তৈরি হয়), কিন্তু ties to even-এ অর্ধেক tie ওপরে আর অর্ধেক নিচে যায়, ফলে গড়ে ভুল কাটাকাটি হয়। Standard আরও তিনটি “directed” mode রাখে (শূন্যের দিকে, +∞-এর দিকে, −∞-এর দিকে); এগুলো হিসাবের (operation) ভেতরে কীভাবে কাজ করে, আর catastrophic cancellation, Kahan summation, fused multiply-add ও Patriot-Ariane ধরনের দুর্ঘটনা — সব আসছে পরের পর্ব ২৪-এ। এখানে মূল কথা: প্রতিটি দশমিক literal মেমরিতে যাওয়ার মুহূর্তেই একবার গোল হয়।
0.1 + 0.2 != 0.3 কোনো bug নয়, আর কোনো ভাষার দোষও নয় — C, Java, JavaScript, Python সবাই একই double ব্যবহার করে বলে একই উত্তর দেয়। তিনটি literal-ই গোল হয়েছে, যোগফলও গোল হয়েছে, আর গোল করার ভুলগুলো একই দিকে মেলেনি। তাই float তুলনা করুন একটি সহনসীমা দিয়ে: |a − b| ≤ tol × max(|a|, |b|), যেখানে tol সমস্যাভেদে বেছে নিতে হয় (কয়েক ε থেকে অনেক বড়)।
Float-কে integer হিসেবে সাজানো — biased exponent-এর পুরস্কার
অংশ ৩-এ বলেছিলাম, exponent biased বলেই ধনাত্মক float-এর বিট-প্যাটার্ন unsigned integer হিসেবে একই ক্রমে সাজে: ১.৫ = 0x3FC00000 < ২.০ = 0x40000000 < ১০৩০ = 0x7149F2CA. ঋণাত্মক সংখ্যায় sign-magnitude বলে ক্রম উল্টো (বড় magnitude মানে বেশি ঋণাত্মক)। একটি ছোট কৌশলে পুরো ক্রম ঠিক করা যায় — চিহ্ন-বিট 1 হলে সব বিট উল্টে দিন, নইলে শুধু চিহ্ন-বিট উল্টান:
key = (b & 0x80000000) ? ~b : (b | 0x80000000); /* unsigned compare of keys = float order */
এর ফলে database index, GPU-র radix sort আর কিছু hardware comparator floating-point তুলনা করে সাধারণ integer circuit দিয়ে। একই কারণে “পরের float” বের করা মানে বিট-প্যাটার্নে ১ যোগ — C-র nextafterf ভেতরে প্রায় এটিই করে, আর simulator-এর ulp32() ঠিক এভাবেই ULP মেপেছে। (NaN আর −0/+0 আলাদা করে সামলাতে হয়।)
টাকা কখনো float-এ নয় — integer আর decimal floating point
৳১০৫০.৭৫ binary float-এ হুবহু থাকে না, ৳০.১০ও না। একটি bKash-ধাঁচের অ্যাপে লাখো লেনদেনে প্রতিবার সামান্য ভুল জমলে হিসাব মেলে না, আর হিসাবের আইনি নিয়ম (কোন অঙ্কে কোন দিকে গোল হবে) binary float মানতে পারে না। পেশাদার সমাধান দুটি:
- ক্ষুদ্রতম এককে integer: ৳১০৫০.৭৫ = ১০৫০৭৫ পয়সা, একটি ৬৪-বিটের integer-এ। যোগ-বিয়োগ হুবহু; শুধু সুদ বা শতাংশের হিসাবে গোল করার নিয়মটি স্পষ্টভাবে লিখতে হয়।
- Decimal floating point: IEEE 754-2008 ভিত্তি-১০-এর format যোগ করেছে — decimal32, decimal64 (১৬ দশমিক অঙ্ক) আর decimal128 (৩৪ অঙ্ক) — যেখানে ০.১ হুবহু থাকে। IBM-এর কিছু POWER আর mainframe processor-এ এর hardware সমর্থন আছে বলে জানা যায়; অন্যরা software-এ চালায়: Python-এর
decimal, Java-রBigDecimal, আর C23-এ ঐচ্ছিক_Decimal64.
C আর Python-এর দিক থেকে এই অভ্যাসগুলো ধাপে ধাপে দেখানো আছে C কোর্সের data representation পর্বে আর Python কোর্সের সংখ্যা ও Decimal পর্বে।
৮ · ছোট format — FP16, bfloat16, FP8 আর AI hardware
একই নকশা (চিহ্ন + biased exponent + hidden bit-সহ fraction) যেকোনো মাপে বানানো যায়। Exponent-এর বিট ঠিক করে পরিসর, fraction-এর বিট ঠিক করে নির্ভুলতা — মোট বিট স্থির রাখলে একটি বাড়ালে অন্যটি কমে। AI-এর যুগে এই দর-কষাকষি হঠাৎ খুব গুরুত্বপূর্ণ হয়ে উঠেছে:
| Format | বিট | s / exp / frac | bias | সবচেয়ে বড় | সবচেয়ে ছোট normal | ε |
|---|---|---|---|---|---|---|
| double (FP64) | ৬৪ | ১ / ১১ / ৫২ | ১০২৩ | ≈ ১.৮ × ১০৩০৮ | ≈ ২.২ × ১০−৩০৮ | ২−৫২ |
| single (FP32) | ৩২ | ১ / ৮ / ২৩ | ১২৭ | ≈ ৩.৪ × ১০৩৮ | ≈ ১.১৮ × ১০−৩৮ | ২−২৩ |
| half (FP16) | ১৬ | ১ / ৫ / ১০ | ১৫ | ৬৫৫০৪ | ২−১৪ ≈ ৬.১ × ১০−৫ | ২−১০ |
| bfloat16 | ১৬ | ১ / ৮ / ৭ | ১২৭ | ≈ ৩.৩৯ × ১০৩৮ | ≈ ১.১৮ × ১০−৩৮ | ২−৭ |
| FP8 E4M3 | ৮ | ১ / ৪ / ৩ | ৭ | ৪৪৮ | ২−৬ | ২−৩ |
| FP8 E5M2 | ৮ | ১ / ৫ / ২ | ১৫ | ৫৭৩৪৪ | ২−১৪ | ২−২ |
- FP16 (IEEE 754-2008-এর binary16): ছবি, গ্রাফিক্স আর মোবাইল GPU-তে বহুদিনের পরিচিত। নির্ভুলতা প্রায় ৩.৩ দশমিক অঙ্ক, কিন্তু পরিসর ছোট — ৬৫৫০৪-এর ওপরে ∞. AI training-এ ছোট gradient-গুলো প্রায়ই এর নিচে পড়ে শূন্য হয়ে যায়, তাই FP16-এ training করতে “loss scaling” নামের কৌশল লাগে।
- bfloat16 (“brain float”, Google-এর গবেষকদের প্রস্তাব; training-এর জন্য বানানো দ্বিতীয় প্রজন্মের TPU থেকে ব্যাপক ব্যবহার — প্রথম TPU ছিল ৮ বিট integer-এর): float-এর ঠিক ওপরের ১৬ বিট। ৮ বিট exponent বলে পরিসর float-এর সমান, কিন্তু মাত্র ৭ বিট fraction — নির্ভুলতা প্রায় ২.৪ দশমিক অঙ্ক। Neural network সামান্য নির্ভুলতা হারানো সহ্য করে, কিন্তু পরিসরের বাইরে যাওয়া সহ্য করে না — তাই training-এ এটি জনপ্রিয়। আজ অনেক GPU, ARM আর x86 CPU-তে bfloat16 instruction আছে।
- FP8: দুটি প্রধান রূপ — E4M3 (বেশি নির্ভুলতা, সাধারণত ওজন আর activation-এর জন্য) আর E5M2 (বেশি পরিসর, gradient-এর জন্য)। শিল্পের কয়েকটি কোম্পানি মিলে ২০২৩-এর দিকে OCP (Open Compute Project)-এর মাধ্যমে একটি spec প্রকাশ করে; সেখানে E4M3-এ ∞ নেই আর NaN শুধু একটি pattern-এ (exponent আর fraction-এর সব বিট 1, চিহ্ন-বিট যা-ই হোক), যাতে বড় মান ৪৪৮ পর্যন্ত পৌঁছানো যায়, আর E5M2 IEEE-র নিয়মের কাছাকাছি। বিভিন্ন hardware-এর সংস্করণে খুঁটিনাটি পার্থক্য আছে, আর IEEE-ও (P3109 নামে) ছোট format standard করার কাজ করছে — তাই নির্দিষ্ট chip-এর জন্য তার ডকুমেন্টেশন দেখুন।
কেন AI hardware এগুলো চায়? (১) বিট অর্ধেক মানে একই memory bandwidth-এ দ্বিগুণ সংখ্যা — আর AI-এর গতি প্রায়ই মেমরিতেই আটকে থাকে (পর্ব ৪৪-এর memory wall). (২) গুণক circuit-এর আকার মোটামুটি significand-এর বিটের বর্গের অনুপাতে বাড়ে, তাই ২৪ বিটের গুণকের জায়গায় অনেকগুলো ৮ বা ৪ বিটের গুণক বসানো যায়। এজন্যই tensor unit সাধারণত ছোট format-এ গুণ করে আর যোগফল জমায় FP32-এ — একে বলে mixed precision. GPU আর tensor unit-এর ভেতরের গঠন পর্ব ৫৫-এ আসবে।
bfloat16 বানানো সহজ: float-এর নিচের ১৬ বিট ফেলে দিলেই হয়। কিন্তু “ফেলে দেওয়া” (truncation) আর “গোল করা”-র পার্থক্য কত? নিচের প্রোগ্রামে দুটিই দেখুন:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static uint32_t bits(float x) { uint32_t b; memcpy(&b, &x, 4); return b; }
static float from(uint32_t b) { float x; memcpy(&x, &b, 4); return x; }
/* bfloat16 = the top 16 bits of a float: 1 sign, 8 exponent, 7 fraction */
static uint16_t bf16_trunc(float x) { return (uint16_t)(bits(x) >> 16); }
static uint16_t bf16_round(float x) { /* round to nearest, ties to even */
uint32_t b = bits(x); /* (NaN/overflow checks omitted) */
b += 0x7FFFu + ((b >> 16) & 1u);
return (uint16_t)(b >> 16);
}
int main(void) {
const float v[] = { 3.14159265f, 0.1f, 1.0f / 3.0f, 123.456f, 65504.0f, 3.0e38f };
printf("%-12s %-10s | %-6s %-13s | %-6s %-13s %s\n", "float", "bits",
"trunc", "value", "round", "value", "rel.err");
for (int i = 0; i < 6; i++) {
uint16_t t = bf16_trunc(v[i]), r = bf16_round(v[i]);
float ft = from((uint32_t)t << 16), fr = from((uint32_t)r << 16);
double err = (fr - v[i]) / (double)v[i];
printf("%-12.8g 0x%08X | 0x%04X %-13.8g | 0x%04X %-13.8g %+.2e\n",
(double)v[i], (unsigned)bits(v[i]), (unsigned)t, (double)ft,
(unsigned)r, (double)fr, err);
}
printf("bf16 epsilon = 2^-7 = %g, float epsilon = 2^-23 = %g\n",
(double)(from(0x3F810000u) - 1.0f), (double)(from(0x3F800001u) - 1.0f));
return 0;
}Wandbox-এর gcc (Linux x86-64) আউটপুট:
float bits | trunc value | round value rel.err
3.1415927 0x40490FDB | 0x4049 3.140625 | 0x4049 3.140625 -3.08e-04
0.1 0x3DCCCCCD | 0x3DCC 0.099609375 | 0x3DCD 0.10009766 +9.77e-04
0.33333334 0x3EAAAAAB | 0x3EAA 0.33203125 | 0x3EAB 0.33398438 +1.95e-03
123.456 0x42F6E979 | 0x42F6 123 | 0x42F7 123.5 +3.56e-04
65504 0x477FE000 | 0x477F 65280 | 0x4780 65536 +4.89e-04
3e+38 0x7F61B1E6 | 0x7F61 2.990763e+38 | 0x7F62 3.0040553e+38 +1.35e-03
bf16 epsilon = 2^-7 = 0.0078125, float epsilon = 2^-23 = 1.19209e-07
π-এর ক্ষেত্রে নিচের ১৬ বিট 0x0FDB অর্ধেকের (0x8000) কম, তাই truncation আর rounding একই: 0x4049 = ৩.১৪০৬২৫, আপেক্ষিক ভুল প্রায় ৩ × ১০−৪. কিন্তু ০.১-এ নিচের বিট 0xCCCD অর্ধেকের বেশি — truncation দেয় ০.০৯৯৬…, rounding দেয় ০.১০০১… (0x3DCD). Rounding করলে আপেক্ষিক ভুল কখনো ২−৮ ≈ ৩.৯ × ১০−৩ ছাড়ায় না — এটিই bfloat16-এর unit roundoff (truncation-এ সীমাটি দ্বিগুণ, ২−৭)। 123.456 হয়ে যায় ১২৩.৫, আর 65504 (FP16-এর সর্বোচ্চ) bfloat16-এ ৬৫৫৩৬ — bfloat16 সেখানে অনায়াসে যায়, এমনকি ৩ × ১০৩৮-ও রাখে, যেখানে FP16 ∞ হয়ে যেত। Rounding কোডের 0x7FFF + ((b >> 16) & 1) হলো ties-to-even-এর বিট-কৌশল: ঠিক মাঝখানে পড়লে রাখা অংশের শেষ বিট বিজোড় হলে তবেই ওপরে যায়। (NaN আর overflow সামলানোর পরীক্ষা এখানে বাদ রাখা হয়েছে — বাস্তব library সেগুলো আলাদা করে দেখে।)
আরও গভীরে: x86-এর পুরোনো x87 FPU (8087 থেকে) ভেতরে ৮০ বিটের “extended precision” (৬৪ বিট significand, hidden bit ছাড়া স্পষ্ট লেখা) ব্যবহার করত; ফলে একই C কোড x87 আর SSE-তে সামান্য ভিন্ন উত্তর দিতে পারত — আজকের x86-64 compiler তাই সাধারণত SSE ব্যবহার করে। RISC-V-র F/D extension-এ NaN-এর payload বয়ে নেওয়া হয় না — যেকোনো NaN ফল হয় একটি “canonical NaN” 0x7FC00000, আর ৬৪-বিটের f-register-এ একটি single মান রাখলে ওপরের ৩২ বিট সব 1 করে রাখা হয় (“NaN-boxing”). ARM-এর FPCR-এ flush-to-zero আর “default NaN” মোড আছে, আর অনেক GPU গতির জন্য ডিফল্টে subnormal বাদ দেয়। গবেষণায় posit-এর মতো বিকল্প format নিয়েও কাজ চলছে, যেখানে exponent-এর বিটসংখ্যা মান অনুযায়ী বদলায়।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- −১৩.৬২৫-এর IEEE single রূপ? —
1 10000010 10110100…0=0xC15A0000. - Hidden bit কী, কেন? — normalized binary সংখ্যা সবসময় 1 দিয়ে শুরু, তাই সেটি না লিখে ১টি বাড়তি বিটের নির্ভুলতা (২৩ জায়গায় ২৪ বিট)।
- Exponent two's complement না রেখে biased কেন? — তাতে ধনাত্মক float-কে unsigned integer হিসেবে তুলনা করা যায়, আর শূন্যের সব বিট 0 থাকে।
- Denormal কেন দরকার? — gradual underflow: শূন্যের কাছে ফাঁক ভরায়, আর x ≠ y হলে x − y ≠ ০ নিশ্চিত করে।
- Float-এ সবচেয়ে ছোট কোন ধনাত্মক integer হুবহু রাখা যায় না? — ২২৪ + ১ = ১৬৭৭৭২১৭; double-এ ২৫৩ + ১.
৯ · AI-এর যুগে — float-এর বিট নিজে যাচাই করুন
IEEE 754-এর প্রশ্নে AI-এর উত্তর প্রায়ই “প্রায় ঠিক” — আর এই বিষয়ে প্রায় ঠিক মানে ভুল। একটি বিট এক ঘর সরে গেলে মান দ্বিগুণ বা অর্ধেক হয়ে যায়। সুখবর হলো, প্রতিটি দাবি এক লাইনের কোডে যাচাই করা যায়।
| AI প্রায়ই যা করে | সত্যিটা | যাচাইয়ের উপায় |
|---|---|---|
| hidden 1-কে fraction-এ লিখে ফেলে, বা bias ১২৮ ধরে | bias ১২৭; fraction শুরু হয় বিন্দুর পরের বিট থেকে | JS-এ DataView.setFloat32, C-তে memcpy দিয়ে hex ছাপুন |
| “float-এর নির্ভুলতা ঠিক ৭ অঙ্ক”, “epsilon = ২−২৪” | ≈ ৭.২২ অঙ্ক; ε-এর দুই সংজ্ঞা (২−২৩ বনাম ২−২৪) | FLT_EPSILON, FLT_DIG ছাপুন |
| সবচেয়ে ছোট float = ১.১৮ × ১০−৩৮ | এটি সবচেয়ে ছোট normal; subnormal নেমে যায় ১.৪ × ১০−৪৫ পর্যন্ত | FLT_MIN বনাম FLT_TRUE_MIN |
NaN-কে x == NAN দিয়ে খোঁজে | NaN কোনো কিছুর সমান নয় | isnan(x); পরীক্ষা চালিয়ে দেখুন |
| bfloat16 আর FP16 গুলিয়ে ফেলে; FP8-এর সর্বোচ্চ মান বানিয়ে বলে | bf16 = ১/৮/৭, FP16 = ১/৫/১০; FP8 vendor-ভেদে আলাদা | OCP spec আর chip-এর ডকুমেন্টেশন |
AI-কে IEEE 754-এর প্রশ্ন করার সময় তাকে দিয়েই যাচাইয়ের পথ বের করিয়ে নিন:
(উত্তর মিলিয়ে নিন: −০.৪০৬২৫ = −1.1012 × ২−২, E = ১২৫, তাই 0xBED00000.)
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১IEEE 754-এর আগে floating point-এ এত বিশৃঙ্খলা কেন ছিল, আর একটি standard কীভাবে পুরো শিল্প বদলে দিল?
১৯৫০ থেকে ৭০-এর দশক পর্যন্ত floating point ছিল প্রতিটি কোম্পানির নিজস্ব নকশার জায়গা, আর নকশাকারীরা প্রায়ই গতি বা circuit-এর খরচকে নির্ভুলতার চেয়ে বেশি গুরুত্ব দিতেন। IBM System/360 (১৯৬৪) ভিত্তি ১৬ বেছে নেয়: exponent-এর এক ধাপে বিন্দু চার বিট সরে, তাই shift circuit সহজ হয়। কিন্তু এর মূল্য ছিল “wobbling precision” — significand-এর প্রথম hex অঙ্কটি ১ হলে সামনের তিনটি বিট শূন্য, অর্থাৎ কিছু সংখ্যায় নির্ভুলতা তিন বিট কম। CDC আর Cray-র supercomputer গতির জন্য গোল করার নিয়ম সরল রাখত; কিছু মেশিনে গুণ commutative থাকলেও ১ দিয়ে গুণ করলে মান সামান্য বদলে যেতে পারত বলে গল্প প্রচলিত। DEC VAX-এর format তুলনামূলক ভালো হলেও আলাদা।
এর ফল ছিল ভয়াবহ রকম ব্যবহারিক: বৈজ্ঞানিক library লেখকদের প্রতিটি মেশিনের জন্য আলাদা কৌশল লিখতে হতো, আর একটি প্রোগ্রাম এক মেশিনে ঠিক চললেও অন্যটিতে ভুল উত্তর দিত — কে ঠিক, বোঝার উপায় ছিল না। ১৯৭০-এর দশকের শেষে Intel যখন 8087 coprocessor নকশা করছিল, তারা William Kahan-কে পরামর্শক হিসেবে নেয়, আর উদ্দেশ্য দাঁড়ায় “সবচেয়ে ভালো” arithmetic বানানো, শুধু দ্রুত নয়। প্রায় একই সময়ে IEEE-র p754 কমিটিতে Kahan, Coonen আর Stone-এর খসড়া আর DEC-এর VAX-ভিত্তিক একটি প্রস্তাবের মধ্যে কয়েক বছর বিতর্ক চলে — যার কেন্দ্রে ছিল gradual underflow.
১৯৮৫-র standard চারটি বিপ্লবী জিনিস নিশ্চিত করে: প্রতিটি মৌলিক operation-এর ফল “যেন অসীম নির্ভুলতায় হিসাব করে তারপর একবার গোল করা” (correct rounding); ∞, NaN আর ±0-এর মতো বিশেষ মান, যাতে প্রোগ্রাম ক্র্যাশ না করে; subnormal; আর exception-flag. ফলে একই হিসাব ভিন্ন chip-এ বিট-বিট এক উত্তর দেওয়া সম্ভব হলো। ১৯৯০-এর দশকে প্রায় সব নতুন CPU এটি গ্রহণ করে; IBM-এর mainframe-ও পরে IEEE format যোগ করে।
মূল উপলব্ধি: standard-এর আসল মূল্য প্রযুক্তিতে নয়, পূর্বানুমেয়তায় — যখন সবাই একই নিয়ম মানে, তখন একটি গাণিতিক প্রমাণ বা একটি numerical library সব মেশিনে কাজে লাগে। IEEE 754 সেই ভিত্তি, যার ওপর আজকের বৈজ্ঞানিক কম্পিউটিং আর AI দাঁড়িয়ে।
প্র ০২Subnormal সংখ্যা নির্ভুলতা বাঁচায়, তবু অনেক hardware আর software সেগুলো বন্ধ করে রাখে — এই দ্বন্দ্ব কেন?
গাণিতিক দিক থেকে subnormal-এর যুক্তি পরিষ্কার। এগুলো না থাকলে ০ আর সবচেয়ে ছোট normal সংখ্যার মাঝে একটি “খাদ” থাকে, যার প্রস্থ পাশের ধাপের প্রায় ২২৩ গুণ (single-এ)। এই খাদে পড়া প্রতিটি ফল হঠাৎ ০ হয়ে যায় — আপেক্ষিক ভুল ১০০%. এর ফলে “x ≠ y হলে x − y ≠ ০”-এর মতো সরল নিয়ম ভাঙে, আর numerical algorithm-এর ভুল-বিশ্লেষণে আলাদা বিশেষ ক্ষেত্র ধরতে হয়। Gradual underflow থাকলে শূন্যের কাছেও ভুল সর্বোচ্চ অর্ধেক ২−১৪৯ — absolute সীমা স্থির থাকে, প্রমাণ সরল থাকে। Kahan আর তাঁর সহকর্মীরা এ নিয়ে লড়েছিলেন, আর DEC-এর পক্ষ যুক্তি দিয়েছিল যে এটি hardware-কে জটিল ও ধীর করবে।
দুই পক্ষই আংশিক ঠিক প্রমাণিত হয়েছে। Normal সংখ্যার pipeline ধরে নেয় সামনে hidden 1 আছে; subnormal-এ সেটি 0, আর ফলকে normalize করতে বাড়তি shift লাগে। অনেক CPU তাই এই বিরল ক্ষেত্রটি দ্রুত hardware-এ না করে একটি ধীর microcode পথে পাঠায় — x86-এ সেই ধাক্কা শতাধিক cycle পর্যন্ত হতে পারে বলে মাপা হয়েছে। অডিও software-এ এর একটি বিখ্যাত উদাহরণ আছে: reverb বা filter-এর লেজের শব্দ ক্রমশ ক্ষীণ হতে হতে subnormal অঞ্চলে ঢোকে, আর হঠাৎ CPU-র ব্যবহার কয়েকগুণ বেড়ে শব্দ ভেঙে যায়। সমাধান হিসেবে অডিও আর গেম প্রোগ্রামাররা FTZ/DAZ চালু করেন বা সংকেতে খুব ছোট “noise” যোগ করেন।
GPU-তে এই আপস আরও স্পষ্ট: অনেক GPU-র দ্রুত গণিত মোড ডিফল্টে subnormal ফেলে দেয়, কারণ গ্রাফিক্সে ১০−৩৮-এর নিচের মান কোনো পার্থক্য আনে না। আবার নতুন কিছু CPU নকশায় subnormal-ও পুরো গতিতে চলে, কারণ transistor এখন সস্তা — তাই দামটি ক্রমশ কমছে। বৈজ্ঞানিক simulation আর numerical library সাধারণত subnormal চালু রাখে, কারণ সেখানে সঠিকতাই প্রথম।
মূল উপলব্ধি: subnormal হলো নির্ভুলতা আর গতির একটি জীবন্ত দর-কষাকষি। ভালো প্রকৌশলী জানেন তাঁর প্রোগ্রাম কোন মোডে চলছে (FTZ চালু কি না), কারণ একই কোড দুই মোডে ভিন্ন সংখ্যা আর ভিন্ন গতি দেয়।
প্র ০৩AI training-এ bfloat16 বা FP8-এর মতো “কম নির্ভুল” সংখ্যা ব্যবহার করেও মডেল ঠিকঠাক শেখে কীভাবে?
Neural network-এর হিসাব মূলত লক্ষ-কোটি গুণ আর যোগ — matrix গুণ। প্রতিটি ওজন বা activation-এর সপ্তম দশমিক অঙ্ক মডেলের আচরণে প্রায় কিছুই বদলায় না; training নিজেই “noisy” (প্রতিবার ডেটার একটি এলোমেলো অংশ দেখে ওজন সামান্য সরে), তাই গোল করার ছোট ভুল সেই noise-এর মধ্যে মিশে যায়। কিন্তু একটি জিনিস মডেল সহ্য করে না — পরিসরের বাইরে যাওয়া। gradient-গুলো কখনো ১০−৮, কখনো ১০৪; FP16-এর সবচেয়ে ছোট normal প্রায় ৬ × ১০−৫ আর সর্বোচ্চ ৬৫৫০৪, তাই FP16-এ ছোট gradient শূন্য হয়ে যায় আর বড়টি ∞. এজন্যই ২০১৭-১৮ সালের দিকে FP16 training-এ “loss scaling” — পুরো loss-কে একটি বড় সংখ্যা দিয়ে গুণ করে gradient-কে নিরাপদ পরিসরে আনা — অপরিহার্য ছিল।
bfloat16 সমস্যাটির মূলে আঘাত করে: exponent float-এর সমান ৮ বিট, তাই পরিসর নিয়ে চিন্তাই নেই; নির্ভুলতা কম, কিন্তু সেটি মডেল সহ্য করে। আরেকটি সুবিধা — float থেকে bfloat16 বানানো মানে শুধু নিচের ১৬ বিট গোল করে ফেলা, তাই রূপান্তর খুব সস্তা। FP8-এ আরও এক ধাপ: দুই রকম format (ওজনের জন্য E4M3, gradient-এর জন্য E5M2) আর প্রতিটি tensor-এর জন্য আলাদা “scale factor”, যাতে মানগুলো ছোট পরিসরের মাঝখানে বসে।
মূল কৌশলটি হলো mixed precision: গুণ হয় ছোট format-এ, কিন্তু হাজার হাজার গুণফলের যোগফল জমা হয় FP32-এ, আর ওজনের “master copy”-ও প্রায়ই FP32-এ থাকে। নইলে অনেক ছোট সংখ্যা একটি বড় যোগফলে যোগ করলে ছোটগুলো হারিয়ে যেত — ১৬৭৭৭২১৬ + ১-এর মতো। Hardware-এর লাভ বিশাল: অর্ধেক বিট মানে অর্ধেক মেমরি ও bandwidth, আর ৮-বিট significand-এর গুণক ২৪-বিটের চেয়ে বহুগুণ ছোট ও কম শক্তি খায়। তাই NVIDIA-র tensor core, Google-এর TPU আর বহু AI accelerator ছোট format-কে কেন্দ্রে রেখে নকশা করা হয়েছে।
মূল উপলব্ধি: “কত বিট দরকার” প্রশ্নের উত্তর নির্ভর করে সমস্যা কোন ভুল সহ্য করে তার ওপর। AI পরিসর চায়, নির্ভুলতা কম চায় — আর format নকশাকারীরা ঠিক সেই অনুপাতে বিট ভাগ করেছেন।
প্র ০৪ব্যাংক, মোবাইল ব্যাংকিং আর শেয়ারবাজারের সফটওয়্যার কেন binary float এড়িয়ে চলে, আর তার বদলে কী ব্যবহার করে?
টাকার হিসাবের সমস্যা দুটি। প্রথমটি প্রতিনিধিত্বের: ৳০.১০, ৳০.০১ বা ৳১০৫০.৭৫ — প্রায় কোনো পয়সার মান binary float-এ হুবহু নেই, কারণ হরে ৫ আছে। প্রতিটি লেনদেনে ভুল খুব ছোট (double-এ ১০−১৭-এর মতো আপেক্ষিক), কিন্তু হিসাবরক্ষণে “প্রায় সমান” গ্রহণযোগ্য নয় — দিনশেষে খতিয়ান পয়সা পর্যন্ত মিলতে হবে। আর float-এ বড় অঙ্কের সাথে ছোট অঙ্ক যোগ করলে ছোটটি হারিয়ে যেতে পারে: single precision-এ ২ কোটি টাকার কাছে ধাপ ২ টাকা!
দ্বিতীয় সমস্যা আইনের: সুদ, কর, VAT বা কমিশনের হিসাবে কোন অঙ্কে কীভাবে গোল করতে হবে, তা নিয়ম বা চুক্তি দিয়ে ঠিক করা — প্রায়ই দশমিক অঙ্কে “অর্ধেক হলে ওপরে” বা ব্যাংকারের rounding. Binary float দশমিক অঙ্কের সীমানাই চেনে না, তাই এই নিয়ম সঠিকভাবে প্রয়োগ করা যায় না। একটি প্রচলিত শিক্ষামূলক ঘটনা হলো ১৯৮০-র দশকের Vancouver Stock Exchange-এর সূচক: নতুন সূচক ১০০০ থেকে শুরু হয়ে প্রায় দুই বছরে প্রায় অর্ধেকে নেমে গিয়েছিল বলে বর্ণনা পাওয়া যায় — বাজার নয়, প্রতিবার হিসাবের পরে গোল না করে কেটে ফেলা (truncation)-ই ছিল কারণ। সংশোধনের পরে সূচক এক রাতেই প্রায় দ্বিগুণ হয়।
তাই শিল্পের পুরোনো আর নতুন দুই ধারাই দশমিক বা integer-এ চলে। COBOL আর mainframe দশকের পর দশক packed decimal (BCD-র আত্মীয়, পর্ব ০৪) ব্যবহার করেছে; IBM-এর কিছু processor-এ আজও decimal arithmetic-এর hardware আছে। আধুনিক অ্যাপে সবচেয়ে সাধারণ উপায় হলো ক্ষুদ্রতম এককে ৬৪-বিটের integer (পয়সা, বা শেয়ারের দামের জন্য আরও ছোট একক), আর database-এ DECIMAL(18,2)-এর মতো নির্দিষ্ট-দশমিক type. Java-র BigDecimal, Python-এর decimal আর C#-এর decimal একই উদ্দেশ্যে।
মূল উপলব্ধি: floating point পরিমাপের জন্য — যেখানে ভুল আগেই আছে (সেন্সর, পদার্থবিজ্ঞান); টাকা গণনার জন্য — যেখানে প্রতিটি একক গোনা যায়। গণনাযোগ্য জিনিস integer বা decimal-এ রাখুন।
প্র ০৫−0, NaN ≠ NaN আর NaN-এর payload — এই “অদ্ভুত” নিয়মগুলো কি নকশার ভুল, নাকি ইচ্ছাকৃত?
সবগুলোই ইচ্ছাকৃত, আর প্রতিটির পেছনে একটি ব্যবহারিক যুক্তি আছে। NaN ≠ NaN দিয়ে শুরু করি। IEEE 754 যখন লেখা হচ্ছিল, তখন অনেক ভাষায় isnan()-এর মতো কোনো ফাংশন ছিল না। “NaN নিজের সমান নয়” নিয়মটি থাকলে যেকোনো ভাষায় x != x লিখেই NaN চেনা যায় — কোনো নতুন ফাংশন ছাড়া। এর গাণিতিক যুক্তিও আছে: দুটি NaN দুটি ভিন্ন অর্থহীন হিসাব থেকে আসতে পারে (০/০ আর ∞ − ∞), তাদের “সমান” বলার মানে নেই। কিন্তু এর মূল্যও আছে: NaN থাকা array-তে sort ভেঙে যায়, কারণ তুলনা আর transitive থাকে না; hash table-এ NaN key খুঁজে পাওয়া যায় না। IEEE 754-2008 তাই আলাদা একটি totalOrder ক্রিয়া সংজ্ঞায়িত করেছে, যা −0, +0 আর NaN-সহ সব bit pattern-কে একটি নির্দিষ্ট ক্রমে সাজায় — অংশ ৭-এর integer কৌশলের সাথে প্রায় মেলে।
−0-এর যুক্তি আসে underflow আর জটিল সংখ্যা থেকে। একটি খুব ছোট ঋণাত্মক ফল underflow হলে −0 দিক-তথ্যটি রেখে দেয়, ফলে 1/x সঠিক চিহ্নের ∞ দেয়। জটিল log বা বর্গমূলের মতো ফাংশনে negative real অক্ষ বরাবর একটি “কাটা” আছে, আর তার কোন পাশে বিন্দুটি — তা ঠিক করে শূন্যের চিহ্ন। আবার -0 == +0 সত্য রাখা হয়েছে, যাতে সাধারণ কোড অবাক না হয়।
Payload ছিল নির্ণয়ের জন্য: NaN-এর fraction-এ কোথায় ভুলটি জন্মেছিল, তার একটি কোড রাখা যাবে — এমন ভাবনা। বাস্তবে hardware-গুলো payload বয়ে নেওয়ার নিয়মে একমত হয়নি (RISC-V তো সব NaN-কে canonical করে দেয়), কিন্তু সফটওয়্যার এটিকে অন্য কাজে লাগিয়েছে: JavaScript engine আর LuaJIT-এর মতো interpreter NaN-boxing দিয়ে একটি ৬৪-বিট শব্দে হয় একটি double, নয়তো NaN-এর ভেতরে একটি pointer বা integer রাখে — মেমরি আর গতি দুটিই বাঁচে।
মূল উপলব্ধি: যে নিয়ম প্রথমে “অদ্ভুত” লাগে, তা প্রায়ই একটি বাস্তব সমস্যার সবচেয়ে সস্তা সমাধান। কিন্তু প্রতিটি সমাধানের পার্শ্বপ্রতিক্রিয়া আছে — তাই NaN আর −0 নিয়ে সচেতন থাকা প্রতিটি পেশাদার প্রোগ্রামারের দায়িত্ব।
অনুশীলন
কাগজ-কলম নিন — প্রথম তিনটি হাতে করার, শেষটি আউটপুট অনুমান আর ছোট কোডের।
-
Encode: (ক) ১২.৩৭৫ আর (খ) −০.৭৫-কে IEEE 754 single precision-এ লিখুন — field ভাগ করে আর hex-এ।
(ক) ১২ = 1100, ০.৩৭৫ × ২ = ০.৭৫ (0), × ২ = ১.৫ (1), ০.৫ × ২ = ১.০ (1) → .011. তাই ১২.৩৭৫ = 1100.011 = 1.100011 × ২৩. s = 0, E = ৩ + ১২৭ = ১৩০ = 1000 0010, f = 1000 1100 0…0. জোড়া: 0 10000010 10001100000000000000000 → 0100 0001 0100 0110 0000 … =
0x41460000.(খ) ০.৭৫ = 0.11 = 1.1 × ২−১. s = 1, E = −১ + ১২৭ = ১২৬ = 0111 1110, f = 1000…0. জোড়া: 1 01111110 10000000000000000000000 → 1011 1111 0100 0000 … =
0xBF400000. -
Decode: নিচের single-precision pattern-গুলোর মান বা শ্রেণি বলুন: (ক)
0xC1480000(খ)0x447A0000(গ)0x7F800001(ঘ)0x00000000আর0x80000000.(ক) 1100 0001 0100 1000 … → s = 1, E = 1000 0010 = ১৩০ → e = ৩, f = 1001 0…0 → 1.1001 × ২৩ = 1100.1 = ১২.৫. মান −১২.৫.
(খ) 0100 0100 0111 1010 … → s = 0, E = 1000 1000 = ১৩৬ → e = ৯, f = 1111 0100 0…0 → 1.1111010 × ২৯ = 11111010002 = ৫১২ + ২৫৬ + ১২৮ + ৬৪ + ৩২ + ৮ = ১০০০.
(গ) E = সব 1 (২৫৫), f = 1 (অশূন্য) → NaN; fraction-এর সবচেয়ে বড় বিট 0, তাই ২০০৮-এর সুপারিশ অনুযায়ী এটি signalling NaN.
(ঘ) E = ০, f = ০ → +0 আর −0; শুধু চিহ্ন-বিট আলাদা।
==তুলনায় দুটি সমান। -
পরিসর আর epsilon (হিসাব): একটি কাল্পনিক ১২ বিটের IEEE-ধাঁচের format ধরুন: ১ sign, ৫ exponent (bias ১৫), ৬ fraction. বের করুন: (ক) সবচেয়ে বড় finite মান, (খ) সবচেয়ে ছোট ধনাত্মক normal, (গ) সবচেয়ে ছোট ধনাত্মক subnormal, (ঘ) machine epsilon আর প্রায় কত দশমিক অঙ্ক। (ঙ) IEEE single-এ ১০ লাখ (১০৬)-এর কাছে ULP কত?
(ক) সবচেয়ে বড় normal E = ৩০ (৩১ সংরক্ষিত), e = ১৫, f সব 1: (২ − ২−৬) × ২১৫ = ১.৯৮৪৩৭৫ × ৩২৭৬৮ = ৬৫০২৪.
(খ) E = ১ → ২১−১৫ = ২−১৪ ≈ ৬.১ × ১০−৫. (গ) E = ০, f = 000001 → ২−৬ × ২−১৪ = ২−২০ ≈ ৯.৫৪ × ১০−৭.
(ঘ) ε = ২−৬ = ০.০১৫৬২৫; নির্ভুলতা ৭ বিট (hidden সহ), তাই ৭ × log১০২ ≈ ২.১ দশমিক অঙ্ক.
(ঙ) ২১৯ = ৫২৪২৮৮ ≤ ১০৬ < ২২০, তাই e = ১৯ আর ULP = ২১৯−২৩ = ২−৪ = ০.০৬২৫ — float-এ ১০ লাখ টাকার কাছে সবচেয়ে ছোট ধাপ সোয়া ছয় পয়সা।
-
আউটপুট অনুমান আর কোড: x86-64 Linux-এ gcc দিয়ে চালালে প্রতিটি লাইন কী ছাপবে? তারপর
isnanব্যবহার না করে একটিint my_isnan(float x)লিখুন, যা বিট দেখে NaN চেনে।float f = 16777217; printf("%.1f\n", f); printf("%d\n", 0.1f == 0.1); float z = -0.0f; printf("%d %g\n", z == 0.0f, 1.0f / z); float n = NAN; printf("%d\n", n == n);(১)
16777216.0— ২২৪ + ১ ঠিক মাঝখানে, ties to even নিচে নামায়। (২)0—0.1f(০.১০০০০০০০১৪৯…) double-এ রূপান্তরিত হয়ে double-এর ০.১ (০.১০০০০০০০০০০০০০০০০৫৫…)-এর সাথে তুলনা হয়; দুটি ভিন্ন। (৩)1 -inf— −0 আর +0 সমান, কিন্তু ১/−0 = −∞. (৪)0— NaN নিজের সমানও নয়।int my_isnan(float x) { uint32_t b; memcpy(&b, &x, sizeof b); return ((b >> 23) & 0xFFu) == 0xFFu && (b & 0x7FFFFFu) != 0; }শর্ত দুটি: exponent সব 1 এবং fraction অশূন্য (শুধু প্রথমটি দেখলে ∞-কেও NaN বলত)। চিহ্ন-বিট দেখার দরকার নেই — −NaN-ও NaN. কম্পাইলারের
-ffast-mathচালু থাকলেx != xকৌশল অপ্টিমাইজ হয়ে মুছে যেতে পারে, কিন্তু বিট-দেখা এই রূপটি নিরাপদ থাকে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি দশমিক সংখ্যাকে হাতে IEEE 754 single precision-এ encode করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- চিহ্ন-বিট ঠিক করা, তারপর শুধু মান নিয়ে কাজ
- পূর্ণ অংশ আর ভগ্নাংশকে binary-তে রূপান্তর
- Normalize করে 1.f × 2e রূপে আনা
- e-এর সাথে ১২৭ যোগ করে ৮ বিটের exponent
- hidden 1 বাদ দিয়ে fraction-কে ২৩ বিটে কাটা ও গোল করা
- তিন field জোড়া দিয়ে hex-এ লেখা
IEEE 754 single precision-এ exponent-এর bias কত?
- ১২৮
- ১২৭
- ১০২৩
- ২৫৫
৩২-বিটের pattern 0xC0000000-কে IEEE 754 single precision হিসেবে পড়লে মান কত?
- ২.০
- −১.০
- −২.০
- −0
IEEE 754-এ exponent field-এর সব বিট 0 আর fraction অশূন্য হলে সংখ্যাটি কী?
- NaN
- ±0
- ±∞
- Subnormal (denormal) সংখ্যা
C-র DBL_EPSILON — অর্থাৎ double precision-এ ১ আর তার পরের double-এর দূরত্ব — প্রায় কত?
- ২−৫২ ≈ ২.২২ × ১০−১৬
- ২−২৩ ≈ ১.১৯ × ১০−৭
- ≈ ২.২২ × ১০−৩০৮
- ২−৫৩ ≈ ১.১১ × ১০−১৬
IEEE 754 single precision (২৪ বিট নির্ভুলতা)-এ নিচের কোন integer হুবহু রাখা যায় না?
- ১৬৭৭৭২১৬
- ১৬৭৭৭২১৭
- ১৬৭৭৭২১৮
- ১৬৭৭৭২১৫
AI training-এ ব্যবহৃত bfloat16 (১ sign, ৮ exponent, ৭ fraction)-এর সাথে IEEE FP16 (১, ৫, ১০)-এর তুলনায় কোনটি সঠিক?
- bfloat16-এর নির্ভুলতা FP16-এর চেয়ে বেশি
- দুটি একই format-এর দুই নাম
- bfloat16-এর পরিসর float-এর প্রায় সমান, কিন্তু নির্ভুলতা কম
- FP16-এর পরিসর bfloat16-এর চেয়ে বড়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- মান = (−1)s × 1.f × 2E − bias; single = ১/৮/২৩, bias ১২৭; double = ১/১১/৫২, bias ১০২৩; hidden 1 বিনা খরচে একটি বিট দেয়।
- Encode-এর পথ: চিহ্ন → binary → normalize → E = e + bias → ২৩ বিট fraction (গোল করে) → hex. −১৩.৬২৫ =
0xC15A0000,0x40490FDB≈ π. - Exponent টেবিল: E = ০ → ±0 বা subnormal (0.f × ২−১২৬); E = সব 1 → ±∞ বা NaN; NaN ≠ NaN, আর −0 == +0.
- নির্ভুলতা: ε = ২−২৩ / ২−৫২; ≈ ৭.২২ / ১৫.৯৫ অঙ্ক; আপেক্ষিক ভুল ≤ ε/২; ULP মানের সাথে বাড়ে — ২২৪ + ১ float-এ নেই।
- বাস্তবে: ০.১ হুবহু থাকে না (ties to even-এ গোল); float == দিয়ে তুলনা নয়; টাকা পয়সার integer বা decimal-এ; AI-তে bfloat16/FP8 পরিসর রাখে, নির্ভুলতা ছাড়ে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৪ · Floating point-এর হিসাব — যোগ, গুণ, rounding আর ভুলের ফাঁদপরবর্তী পর্বfloat দিয়ে হিসাব কীভাবে হয়, আর কোথায় ভুল জমে।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বtransistor থেকে multicore পর্যন্ত পুরো রোডম্যাপ।
- পর্ব ০৪ · সংখ্যা পদ্ধতি — binary, octal, hexadecimal আর রূপান্তরভিত্তিভগ্নাংশের রূপান্তর আর কেন ০.১ binary-তে শেষ হয় না।
- C · ডেটা কীভাবে থাকে — বিট, integer, float আর লেখাপাশের ট্র্যাকC কোডে float-এর ফাঁদ আর টাকার হিসাবের অভ্যাস।
- Python · সংখ্যা ও হিসাব — int, float-এর ফাঁদ আর Decimalপ্রয়োগPython-এ Decimal দিয়ে নিখুঁত টাকার হিসাব।