Foundationপ্রথম নীতি থেকে
LEVEL 1লেসন ৭/১৪কঠিন৫৫ মিনিট

Fixed-Point বনাম Floating-Point — দশমিক সংখ্যা bit-এ কীভাবে বাঁচে

Fixed-Point vs Floating-Point Representation

সংখ্যার ভগ্নাংশ আর বিশাল dynamic range ধারণ করতে দুইটা পথ — fixed-point একটা স্থির scale-এর integer, floating-point একটা 'ভাসমান বিন্দুর' scientific notation; দুটোর মধ্যেকার trade-off আর IEEE 754-এর জন্মকথা।

এই লেসন শেষে আপনি পারবেন

  • Q-format (যেমন Q16.16, Q8.8) ব্যবহার করে যেকোনো দশমিক সংখ্যাকে fixed-point representation-এ হাতে রূপান্তর করতে পারবেন
  • Fixed-point সংখ্যায় addition ও multiplication হাতে হিসাব করতে পারবেন, আর multiplication-এ কেন shift লাগে তা ব্যাখ্যা করতে পারবেন
  • Fixed-point-এর range আর precision-এর মধ্যে trade-off বিশ্লেষণ করে একটা নির্দিষ্ট প্রয়োগের জন্য সঠিক Q-format বেছে নিতে পারবেন
  • কেন 0.1-এর মতো সাধারণ দশমিক ভগ্নাংশ বাইনারিতে কখনো সসীম আকারে লেখা যায় না, সেটা গাণিতিকভাবে প্রমাণ করতে পারবেন
  • Floating-point-এর 'ভাসমান বিন্দু' ধারণাটা scientific notation-এর সাথে মিলিয়ে ব্যাখ্যা করতে পারবেন
  • কোন বাস্তব সিস্টেমে fixed-point আজও floating-point-এর চেয়ে ভালো পছন্দ, তা যুক্তি দিয়ে বলতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

আগের লেসনে আমরা integer overflow দেখেছি — একটা fixed সংখ্যক bit-এ কতদূর পর্যন্ত গোনা যায় তার একটা কঠোর সীমা আছে। কিন্তু integer-এর আরেকটা সীমাবদ্ধতা আছে, আরও মৌলিক: integer দিয়ে ভগ্নাংশ লেখাই যায় না।

3 / 2 যদি integer division হয়, উত্তর 1 — বাকি অর্ধেকটা কোথায় গেল? পদার্থবিজ্ঞানের একটা মৌলিক ধ্রুবক লিখতে চান?

ইলেকট্রনের ভর  ≈ 0.000000000000000000000000000000911 kg
অ্যাভোগাড্রো সংখ্যা ≈ 602,000,000,000,000,000,000,000

দুটোই বৈধ সংখ্যা, কিন্তু ম্যাগনিচিউডে ৫৬ ডিজিটের ব্যবধান। কোনো ৩২-বিট integer এই দুটোকেই একই সাথে অর্থবহভাবে ধরে রাখতে পারবে না — একটা প্রায় শূন্য দেখাবে, আরেকটা overflow করবে।

তাহলে প্রশ্ন দুইটা, আলাদা কিন্তু সম্পর্কিত:

  1. ভগ্নাংশ কীভাবে bit-এ লেখা যায়?
  2. অবিশ্বাস্য রকম ছোট আর অবিশ্বাস্য রকম বড় — দুটোই একই fixed bit বাজেটে কীভাবে ধরা যায়?

এই লেসনে আমরা দুইটা উত্তর দেখব — fixed-point, যেটা প্রথম প্রশ্নের একটা সরল, সস্তা সমাধান কিন্তু দ্বিতীয় প্রশ্নে ব্যর্থ হয়; আর floating-point, যেটা দুটোই সমাধান করে কিন্তু একটা নতুন সমস্যা নিয়ে আসে — যেটা লেসন ৯-এ আমরা পুরোপুরি খুলে দেখব।

মূল ধারণা

Fixed-point — সবচেয়ে সরল উত্তর

ধারণাটা প্রায় প্রতারণামূলক রকম সরল: আপনি যা-ই সংখ্যা চান, সেটাকে একটা বড় ধ্রুবক দিয়ে গুণ করে integer বানিয়ে ফেলুন, আর মনে মনে মনে রাখুন সেই গুণকটা কত ছিল।

উদাহরণ — টাকা-পয়সার হিসাব। ১৯.৯৯ টাকা লিখতে চান? গুণ করুন ১০০ দিয়ে, রাখুন ১৯৯৯ — মানে পয়সায়। CPU-র চোখে এটা এখন নিছক একটা integer, কিন্তু আপনার program জানে এই integer-কে ১০০ দিয়ে ভাগ করলে আসল মান পাওয়া যাবে। এই ১০০-টাই scale factor — bit pattern নিজে সেটা বহন করে না, সফটওয়্যার এটা জানে, ঠিক যেমন গত লেসনের bit pattern-এর interpretation convention-এর মতো।

Binary computer-এ decimal ১০০-এর বদলে ২-এর ঘাত ব্যবহার করা হয় — কারণ তখন গুণ-ভাগ শুধু bit shift, যেটা CPU-র জন্য প্রায় বিনামূল্যে (এক cycle)।

Q-format — সেই ধারণাটার আনুষ্ঠানিক নাম

Qm.n notation বলে: m বিট পূর্ণসংখ্যা অংশের জন্য (sign সহ), n বিট ভগ্নাংশ অংশের জন্য। মোট bit সংখ্যা m + n

stored value=round(x×2n)\text{stored value} = \text{round}(x \times 2^n)

actual value=stored value2n\text{actual value} = \frac{\text{stored value}}{2^n}

Q16.16 মানে: ৩২-বিট signed integer, যার নিচের ১৬ বিট ভগ্নাংশ, উপরের ১৬ বিট পূর্ণসংখ্যা (sign সহ)। Scale factor 2^16 = 65536

বিট:     31                      16 15                       0
        ┌────────────────────────┬──────────────────────────┐
        │   পূর্ণসংখ্যা অংশ (16)   │    ভগ্নাংশ অংশ (16)         │
        │   sign সহ, two's        │    weight: 2⁻¹...2⁻¹⁶     │
        │   complement            │                            │
        └────────────────────────┴──────────────────────────┘

মান = raw_int32 / 65536
Q16.16 — একটা 32-bit integer-কে দুইভাগে ভাগ করে পড়া, hardware-এ কোনো বাড়তি বিট লাগে না।
Q-formatমোট bitপূর্ণসংখ্যা bitভগ্নাংশ bitRangePrecision
Q8.81688−128.0 … 127.9961/256 ≈ 0.0039
Q16.16321616−32768.0 … 32767.999981/65536 ≈ 0.0000153
Q1.3132131−1.0 … 0.99999999951/2³¹ ≈ 4.7×10⁻¹⁰
Q4.2832428−8.0 … 7.9999999961/2²⁸ ≈ 3.7×10⁻⁹

লক্ষ্য করুন Q1.31 — এটাই audio DSP-র প্রিয় format। Audio sample প্রায় সবসময় −1.0-থেকে-1.0-এর মধ্যে normalize করা থাকে, তাই পুরো ৩১ বিটই precision-এ ঢালা যায়, একটা বিটও পূর্ণসংখ্যা অংশে নষ্ট হয় না।

বাস্তব DSP library-তে এই নামগুলো (Q15, Q31 ইত্যাদি) প্রায়ই আরেকটা convention-এ লেখা হয় — শুধু ভগ্নাংশ bit সংখ্যা, কারণ পূর্ণসংখ্যা অংশ ধরে নেওয়া হয় সবসময় 1 (শুধু sign)। ARM-এর নিজস্ব CMSIS-DSP library, বা পুরনো Texas Instruments TMS320 codec — দুটোই এই একই সংক্ষিপ্ত নামকরণ ব্যবহার করে:

সংক্ষিপ্ত নামপূর্ণ Q-formatব্যবহার
Q15Q1.15 (১৬ বিট মোট)16-bit audio codec, ADC/DAC sample
Q31Q1.31 (৩২ বিট মোট)high-precision audio, sensor fusion
Q7Q1.7 (৮ বিট মোট)8-bit microcontroller-এর সস্তা sensor filter

Round করা বনাম কেটে ফেলা (truncation) — এনকোডিং-এর একটা সিদ্ধান্ত

উপরের সূত্রে \text{round}(x \times 2^n) লেখা হয়েছে — কিন্তু “round” ঠিক কীভাবে? এখানেই fixed-point ডিজাইনের একটা সূক্ষ্ম কিন্তু গুরুত্বপূর্ণ সিদ্ধান্ত লুকানো, যেটা এই লেসনের শেষ দিকে (এবং পরের module-এর লেসনগুলোয়) বারবার ফিরে আসবে।

দুইটা সহজ বিকল্প:

  • Truncate (chop): x \times 2^n-এর ভগ্নাংশ অংশ শুধু ফেলে দেওয়া — C-তে (int)(x * ONE) লিখলে এটাই ঘটে (ধনাত্মক সংখ্যায় শূন্যের দিকে কাটা)।
  • Round-to-nearest: নিকটতম integer-এ যাওয়া — `(int)(x * ONE
    • 0.5)` (ধনাত্মকের জন্য)।

পার্থক্যটা তুচ্ছ মনে হতে পারে — প্রতিটা conversion-এ error সর্বোচ্চ এক ULP (1/2^n)। কিন্তু দিকটা গুরুত্বপূর্ণ: truncation-এর error সবসময় একই দিকে (ধনাত্মক সংখ্যায় সবসময় নিচের দিকে), যেখানে round-to-nearest-এর error দুই দিকেই সমান সম্ভাবনায় ছড়ায়।

Q-format library ডিজাইন করার সময় তাই round-to-nearest (এবং আদর্শভাবে, tie-case-এ round-to-even — IEEE 754-এর পরের লেসনে পুরোপুরি আলোচনা হবে) ডিফল্ট হিসেবে বেছে নেওয়া হয়, যদি না performance-এর কারণে সচেতনভাবে truncation বেছে নেওয়া হচ্ছে।

Arithmetic — শুধুই integer arithmetic, একটা বাড়তি নিয়মসহ

যোগ-বিয়োগ: যদি দুটো সংখ্যার scale একই হয়, শুধু raw integer দুটো যোগ করলেই হয় — scale factor আপনা-আপনি মিলে যায়।

a2n+b2n=a+b2n\frac{a}{2^n} + \frac{b}{2^n} = \frac{a+b}{2^n}

গুণ: এখানেই সাবধান হতে হয়। দুটো Q-format সংখ্যা গুণ করলে scale factor-ও গুণ হয়ে যায় — 2^n × 2^n = 2^(2n), তাই ফলাফলকে আবার n বিট ডানে shift করে সঠিক scale-এ ফেরাতে হয়:

(a2n)×(b2n)=a×b22n    ফলাফল=(a×b)n2n\left(\frac{a}{2^n}\right) \times \left(\frac{b}{2^n}\right) = \frac{a \times b}{2^{2n}} \implies \text{ফলাফল} = \frac{(a \times b) \gg n}{2^n}

আর a × b সাময়িকভাবে মূল bit width-এর দ্বিগুণ জায়গা লাগতে পারে — তাই বাস্তব implementation-এ সবসময় একটা চওড়া intermediate type (যেমন int64_t) ব্যবহার করা হয়, শেষে shift করে আবার আসল width-এ নামিয়ে আনা হয়।

ভাগ: উল্টো দিকে — আগে numerator-কে n বিট বাঁয়ে shift করে (precision হারানো আটকাতে), তারপর ভাগ করুন:

ফলাফল=a×2nb\text{ফলাফল} = \frac{a \times 2^n}{b}

Saturating arithmetic — overflow-কে wraparound না করে থামানো

আগের লেসনে আমরা দেখেছি integer overflow-এর তিনটা policy — checked, wrapping, saturating। Fixed-point যেহেতু ভেতরে আসলে integer-ই, এই একই তিনটা policy এখানেও প্রযোজ্য — আর audio/DSP-তে saturating policy-ই সবচেয়ে বেশি ব্যবহৃত হয়।

কেন wrapping বিপজ্জনক এখানে? ধরুন একটা audio sample Q1.31 format-এ সর্বোচ্চ মান (0.999...) ছাড়িয়ে গেছে (দুইটা জোরে শব্দ যোগ হয়ে)। যদি wraparound হয় (আগের লেসনের মতো), মান হঠাৎ সর্বোচ্চ ধনাত্মক থেকে সর্বোচ্চ ঋণাত্মকে লাফ দেবে — speaker-এ এটা শোনাবে একটা কর্কশ, আকস্মিক “পপ” শব্দ হিসেবে।

Saturating arithmetic-এ বদলে মান সীমার (min/max) মধ্যে আটকে (clamp) রাখা হয় — শব্দ বিকৃত (distorted) হয়, কিন্তু আকস্মিক বিপরীত মেরুতে লাফ দেয় না। মানুষের কান বিকৃতি সহ্য করতে পারে, আকস্মিক পোলারিটি-পরিবর্তন নয়।

#include <stdint.h>
#include <limits.h>

int32_t fx_add_saturating(int32_t a, int32_t b) {
    int64_t wide = (int64_t)a + (int64_t)b;   /* wide intermediate — এই লেসনের বারবার আসা নীতি */
    if (wide > INT32_MAX) return INT32_MAX;
    if (wide \< INT32_MIN) return INT32_MIN;
    return (int32_t)wide;
}

ARM-এর কিছু DSP-নির্দিষ্ট instruction set extension (যেমন Cortex-M-এর DSP instruction, বা পুরনো ARM SIMD QADD) হার্ডওয়্যারেই saturating add/subtract সরাসরি সমর্থন করে — একটা single cycle-এ, কোনো software branch ছাড়াই, ঠিক এই কারণে যে audio/video/sensor processing-এ এই pattern এত সাধারণ যে হার্ডওয়্যার-এ বেক করে দেওয়াই লাভজনক।

কেন fixed-point যথেষ্ট না

Fixed-point-এর সমস্যাটা তার সংজ্ঞাতেই লুকানো — scale factor স্থির, compile-time-এ ঠিক করা। তার মানে range আর precision একটা zero-sum trade-off:

  • বেশি integer bit → বড় range, কম precision
  • বেশি fractional bit → বেশি precision, ছোট range

Q16.16-এ সর্বোচ্চ মান ৩২৭৬৭.99998 — অ্যাভোগাড্রো সংখ্যার ধারেকাছেও নেই। বিট বাড়িয়ে Q48.16 বানালে অ্যাভোগাড্রো ধরা যাবে, কিন্তু ইলেকট্রনের ভরের মতো ছোট সংখ্যার জন্য আরও অনেক বেশি ভগ্নাংশ bit লাগবে — একই সংখ্যায় দুটো প্রান্তিক magnitude ধরার কোনো fixed bit budget নেই।

Scientific notation — উত্তরটা ইতিমধ্যে স্কুলে শেখা

পদার্থবিজ্ঞানে অ্যাভোগাড্রো সংখ্যা কীভাবে লেখা হয়?

6.022×10236.022 \times 10^{23}

লক্ষ্য করুন এটা কী করছে — সংখ্যাটাকে দুই ভাগে ভেঙেছে:

  • significand (বা mantissa): 6.022 — precision বহন করে
  • exponent: 23 — magnitude (scale) বহন করে

একই কৌশলে ইলেকট্রনের ভর: 9.11 × 10⁻³¹একই সংখ্যক significant digit (9.11 — তিনটা digit) দিয়ে দুটো সংখ্যাই লেখা গেল, যদিও তারা magnitude-এ ৫৬ ডিজিট দূরে। কারণ precision আর magnitude আলাদা করে এনকোড হচ্ছে।

Floating-point ঠিক এই ধারণাটাই বাইনারিতে প্রয়োগ করে:

value=(1)sign×significand×2exponent\text{value} = (-1)^{\text{sign}} \times \text{significand} \times 2^{\text{exponent}}

sign, significand (বা mantissa), আর exponent — তিনটা আলাদা bit-field, প্রতিটার নিজস্ব কাজ।

“ভাসমান বিন্দু” নামটা কোথা থেকে এলো

Fixed-point-এ radix point (binary-এ “দশমিক বিন্দু”-র সমতুল্য) একটা নির্দিষ্ট bit position-এ পেরেক মারা — Q16.16-এ সবসময় বিট ১৬-১৭-এর মাঝে।

Floating-point-এ radix point-এর অবস্থান exponent দিয়ে নিয়ন্ত্রিত — exponent বদলালে বিন্দুটা কার্যত বাঁয়ে-ডানে “ভাসতে” থাকে:

1.101₂ × 2⁰   =  1.101₂           (বিন্দু এখানে)
1.101₂ × 2²   =  110.1₂           (বিন্দু ডানে সরল)
1.101₂ × 2⁻²  =  0.01101₂         (বিন্দু বাঁয়ে সরল)

একই bit pattern (1101), ভিন্ন exponent, ভিন্ন কার্যকর বিন্দু-অবস্থান। এই “ভাসা”-টাই নামের উৎস — floating radix point, সংক্ষেপে floating-point।

ভেতরে কী ঘটছে

যেখানে দুটো representation-ই একইভাবে মিথ্যা বলে

এখানেই সবচেয়ে গুরুত্বপূর্ণ, প্রায়ই ভুল বোঝা সত্যটা — fixed হোক বা floating, কোনোটাই দশমিক ভগ্নাংশের সমস্যা “সমাধান” করে না। দুটোই বাইনারি (base-2), আর সমস্যাটা base conversion-এর, representation scheme-এর নয়।

কোন দশমিক ভগ্নাংশ বাইনারিতে সসীম?

গণিতের লেসনে (number systems) আমরা base conversion দেখেছি। এখানে সেই একই সত্যের একটা কঠোর result: একটা দশমিক ভগ্নাংশ p/q-এর (lowest terms-এ) বাইনারি representation সসীম হবে শুধুমাত্র যদি q-এর সব prime factor 2 হয়।

কেন? 1/2ᵏ কে বাইনারিতে লিখলে ঠিক k বিট পরে শেষ হয় — 1/2 = 0.1₂, 1/4 = 0.01₂, 1/8 = 0.001₂। কিন্তু q-এ যদি 5 (বা অন্য যেকোনো মৌলিক সংখ্যা 2 ছাড়া) থাকে, তাহলে কখনো 2-এর কোনো ঘাত দিয়ে ভাগ শেষ হয় না — infinite repeating fraction।

ভগ্নাংশDecimal-এBinary-এসসীম?
1/20.50.1₂
1/40.250.01₂
3/80.3750.011₂
1/100.10.0(0011)₂ (repeating)
1/50.20.(0011)₂ (repeating)
1/30.333... (নিজেই decimal-এ অসীম)0.(01)₂ (repeating)

আরও কয়েকটা সাধারণ ভগ্নাংশ দেখলে প্যাটার্নটা আরও স্পষ্ট হয় — আর একটা চমৎকার connection number theory-র (Level 0) modular arithmetic লেসনের সাথে:

ভগ্নাংশহর-এর মৌলিক উৎপাদকBinary-এ সসীম?পুনরাবৃত্তির দৈর্ঘ্য
1/2{2}
1/4{2}
1/6{2, 3}2 বিট (0.0(01)₂)
1/7{7}3 বিট
1/9{3}6 বিট
1/10{2, 5}4 বিট
1/12{2, 2, 3}2 বিট

লক্ষ্য করুন 1/12-এর হর-এ 2 আছে (12 = 4 \times 3), তবু এটা সসীম নয় — কারণ সব prime factor 2 হতে হবে, শুধু কিছু নয়। 12-এর 3 factor-টাই যথেষ্ট এটাকে infinite করতে দিতে।

পুনরাবৃত্তির দৈর্ঘ্যটাও এলোমেলো নয় — এটা ঠিক 2-এর [[modular-arithmetic|multiplicative order]] mod q' (যেখানে q' হলো q-এর 2-বহির্ভূত অংশ) — সংখ্যা তত্ত্বের একটা প্রমিত ফলাফল, ঠিক যেমন 1/7-এর দশমিক আবর্তন দৈর্ঘ্য 6 হয় কারণ 10-এর multiplicative order mod 7 হলো 6। এখানে base 10-এর বদলে base 2, বাকি গণিত অভিন্ন।

1/10-এর বাইনারি expansion হাতে বের করা যাক — বারবার দিয়ে গুণ করে integer অংশ তুলে নেওয়া (long division-এর বাইনারি সংস্করণ):

0.1 × 2 = 0.2   → বিট 0
0.2 × 2 = 0.4   → বিট 0
0.4 × 2 = 0.8   → বিট 0
0.8 × 2 = 1.6   → বিট 1   (এখান থেকে চক্র শুরু)
0.6 × 2 = 1.2   → বিট 1
0.2 × 2 = 0.4   → বিট 0   (remainder 0.2 আগে দেখা গেছে — চক্র পুনরাবৃত্তি)
0.4 × 2 = 0.8   → বিট 0
0.8 × 2 = 1.6   → বিট 1
...

0.1₁₀ = 0.0001100110011001100110011...₂চিরকাল চলতে থাকবে, ঠিক যেমন 1/3 decimal-এ 0.3333... চিরকাল চলে।

Fixed-point-এর নিজস্ব leak — static commitment

Floating-point-এর সমস্যাটা universal (base conversion)। কিন্তু fixed-point-এর একটা বাড়তি সমস্যা আছে যেটা floating-point-এর নেই — range compile-time-এ স্থির করতে হয়

Q16.16 বেছে নিলে, আপনার প্রোগ্রাম কখনো ৩২৭৬৮-এর বেশি কোনো মান হ্যান্ডেল করতে পারবে না — চুপচাপ wrap around করবে, ঠিক আগের লেসনের integer overflow-এর মতোই (কারণ ভেতরে এটা তো আসলে integer-ই)। যদি একটা physics simulation-এর মধ্যবর্তী হিসাবে হঠাৎ ৫০,০০০-এর মতো মান আসে (হয়তো একটা বাগের কারণে, হয়তো একটা legitimate edge case-এর কারণে), fixed-point চুপচাপ ভুল উত্তর দেবে — কোনো warning, কোনো crash ছাড়াই।

একটা concrete দৃশ্যকল্প ভাবুন — একটা গেম physics engine বল-বেগ (velocity) Q16.16-এ রাখছে। স্বাভাবিক gameplay-তে বেগ কখনো \pm 500 ছাড়ায় না, তাই ডেভেলপার নিশ্চিন্ত। কিন্তু একটা bug-এ (ধরুন দুইটা বল একই ফ্রেমে বারবার collide করে বেগ exponentially বাড়িয়ে ফেলে) বেগ কয়েক ফ্রেমেই ৩৫,০০০-এ পৌঁছে যায় — Q16.16-এর সীমা (৩২,৭৬৭.99998) ছাড়িয়ে। ফলাফল: bit pattern wrap করে একটা negative বেগ দেখায় (ঠিক এই লেসনের experiment অংশে ৪০,০০০-এর wraparound-এর মতো) — বলটা হঠাৎ উল্টো দিকে ছুটতে শুরু করে, ডেভেলপারের কাছে মনে হয় এটা physics engine-এর কোনো “অদ্ভুত bug”, যখন প্রকৃত কারণ শুধু একটা representation সীমা নীরবে অতিক্রম হয়েছে।

Floating-point-এর exponent field এই নির্দিষ্ট সমস্যাটা সমাধান করে — range dynamically বদলায়, প্রতিটা সংখ্যা তার নিজের প্রয়োজন অনুযায়ী exponent বেছে নেয়। কিন্তু বিনিময়ে (পরের দুই লেসনে দেখব) precision-টা সংখ্যার magnitude-এর উপর নির্ভরশীল হয়ে যায় — একটা নতুন, সূক্ষ্মতর সমস্যা।

উদাহরণ

হাতে-কলমে — একটা সম্পূর্ণ Q8.8 হিসাব

ধাপ ১ — এনকোড করা: 3.75-কে Q8.8-এ রূপান্তর করুন।

3.75×28=3.75×256=9603.75 \times 2^8 = 3.75 \times 256 = 960

960 দশমিকে, বাইনারিতে ১৬ বিট: 0000 0011 1100 0000

হেক্সে: 0x03C0

বিট-বিভাজন যাচাই করি — উপরের বাইট (পূর্ণসংখ্যা অংশ) 00000011 = 3; নিচের বাইট (ভগ্নাংশ অংশ) 11000000 = 128 + 64 = 192, আর 192/256 = 0.75। মোট: 3 + 0.75 = 3.75

ধাপ ২ — গুণ করা: 2.5 × 1.5 — প্রত্যাশিত উত্তর 3.75

এনকোড করি: 2.5 × 256 = 640 (0x0280), 1.5 × 256 = 384 (0x0180)।

Raw integer গুণ করি — লক্ষ্য করুন এটা Q8.8 × Q8.8, তাই ফলাফল সাময়িকভাবে Q16.16-এর মতো scale-এ থাকে:

640×384=245,760640 \times 384 = 245,760

এই মধ্যবর্তী মান 245760 ইতিমধ্যে ১৬-বিট signed integer-এর সীমা (32767) ছাড়িয়ে গেছে — তাই বাস্তব implementation-এ এই গুণটা অবশ্যই একটা চওড়া (৩২-বিট) intermediate-এ করতে হবে, নাহলে overflow হয়ে ভুল উত্তর আসবে। এখন 8 বিট ডানে shift করে আবার Q8.8-এ ফেরাই:

245,7608=960245,760 \gg 8 = 960

960 কে decode করলে 960 / 256 = 3.75 ✓ — মিলে গেল।

ধাপ ৩ — ভাগ করা: 7.0 ÷ 3.0 — প্রত্যাশিত উত্তর 2.333... (অসীম repeating, 1/3-এর মতোই — এই লেসনের “hood” অংশ মনে করুন)।

এনকোড করি: 7.0 \times 256 = 1792 (0x0700), 3.0 \times 256 = 768 (0x0300)।

এই লেসনের arithmetic নিয়ম অনুযায়ী, ভাগে আগে numerator-কে 8 বিট বাঁয়ে shift করতে হয় (নাহলে ভাগফলে সব precision হারিয়ে যেত — চেষ্টা করে দেখুন 1792 / 768 সরাসরি করলে কী হয়):

1792×28768=458,752768=597 (integer division-এ, ভাগশেষ বাদ)\frac{1792 \times 2^8}{768} = \frac{458{,}752}{768} = 597 \text{ (integer division-এ, ভাগশেষ বাদ)}

597-কে decode করি: 597 / 256 = 2.33203125

প্রকৃত মান 7/3 = 2.3333...। পার্থক্য \approx 0.0013 — এই লেসনের আগের “প্রত্যাশিত output” অংশে আমরা যেমন দেখেছি, Q8.8 precision (1/256 \approx 0.0039) থেকেই এই ধরনের ছোট error আসে। 7/3-এর বাইনারি expansion নিজেই অসীম (3-এর মৌলিক উৎপাদক 2 নয়), তাই কোনো সসীম Q-format সেটা exactly ধরতে পারবে না — আরও বেশি fractional bit দিলে error ছোট হবে, শূন্য হবে না।

ধাপ ৪ — floating-point-এর ধারণাগত সমতুল্য (bit-level এনকোডিং পরের লেসনে):

3.75-কে scientific-notation স্টাইলে বাইনারিতে normalize করি — significand-এর প্রথম বিট সবসময় 1 হবে এভাবে:

3.7510=11.112=1.1112×213.75_{10} = 11.11_2 = 1.111_2 \times 2^1

তুলনা করুন 6.022 \times 10^{23}-এর সাথে — গঠনটা অভিন্ন: sign = +, significand = 1.111, exponent = 1 (base 2-তে)। পরের লেসনে আমরা এই তিনটা টুকরোকে ঠিক কতগুলো বিটে, কীভাবে এনকোড করা হয় সেটা bit-by-bit দেখব — এবং কেন 0.1-এর মতো সংখ্যা এনকোড করলে যা জমা থাকে সেটা ঠিক 0.1 নয়

নিজে চালিয়ে দেখুন

EXPERIMENT

Q16.16 fixed-point লাইব্রেরি — Python ও C দুই ভাষাতেই

Python 3, বা GCC· ১৫ মিনিট
SCALE = 16
ONE = 1 << SCALE           # 65536

def to_fixed(x: float) -> int:
    return round(x * ONE)

def from_fixed(v: int) -> float:
    return v / ONE

def fx_add(a: int, b: int) -> int:
    return a + b            # scale একই, তাই শুধু যোগ

def fx_mul(a: int, b: int) -> int:
    # মধ্যবর্তী পূর্ণ 64-bit precision-এ, তারপর নামিয়ে আনা
    return (a * b) >> SCALE

def fx_div(a: int, b: int) -> int:
    return (a << SCALE) // b


a = to_fixed(2.5)
b = to_fixed(1.5)
print(f"a = {a} ({from_fixed(a)})")
print(f"b = {b} ({from_fixed(b)})")

prod = fx_mul(a, b)
print(f"a * b = {prod} ({from_fixed(prod)})")

s = fx_add(a, b)
print(f"a + b = {s} ({from_fixed(s)})")

quot = fx_div(a, b)
print(f"a / b = {quot} ({from_fixed(quot)})")

# overflow দেখানো — Python-এর int নিজে কখনো overflow করে না
# (arbitrary precision), তাই আমরা নিজেই int32 wraparound simulate করি —
# ঠিক যেমন C/hardware-এ ঘটত (আগের লেসনের two's complement নিয়মে)।
def to_int32(v: int) -> int:
    v &= 0xFFFFFFFF
    if v >= 0x80000000:
        v -= 0x100000000
    return v

raw = to_fixed(40000.0)              # Q16.16-এর সীমা (32767.99998) ছাড়িয়ে গেছে
wrapped = to_int32(raw)
print(f"to_fixed(40000.0) raw     = {raw}")
print(f"to_fixed(40000.0) as int32 = {wrapped}  → decode: {from_fixed(wrapped)}")

প্রত্যাশিত output:

a = 163840 (2.5)
b = 98304 (1.5)
a * b = 245760 (3.75)
a + b = 262144 (4.0)
a / b = 109226 (1.666656494140625)
to_fixed(40000.0) raw     = 2621440000
to_fixed(40000.0) as int32 = -1673527296  → decode: -25536.0

শেষ লাইনটাই এই লেসনের “hood” অংশে বলা static-range-commitment সমস্যার সরাসরি প্রদর্শন — ৪০,০০০ লিখতে চেয়েছিলাম, কিন্তু Q16.16-এর ৩২,৭৬৭.99998 সীমা ছাড়িয়ে যাওয়ায় int32-তে চুপচাপ wrap করে দাঁড়াল −25536.0 — সম্পূর্ণ ভুল sign, সম্পূর্ণ ভুল magnitude, কোনো error বা warning ছাড়াই।

লক্ষ্য করুন a / b-এর ফলাফল 1.666656... — সঠিক উত্তর 1.6666...-এর কাছাকাছি কিন্তু হুবহু নয়। Q16.16-এর precision মাত্র 1/65536 ≈ 0.0000153, তাই 1/6-এর মতো অসসীম ভগ্নাংশ এখানেও সসীম আকারে চাপা পড়ে — fixed-point-ও সব সংখ্যা exactly ধরতে পারে না, শুধু সীমাটা floating-point-এর চেয়ে আলাদা জায়গায়

C-তে একই লাইব্রেরি — লক্ষ্য করুন int64_t intermediate কেন বাধ্যতামূলক:

#include <stdio.h>
#include <stdint.h>

#define SCALE 16
#define ONE   (1 \<\< SCALE)

int32_t to_fixed(double x)  { return (int32_t)(x * ONE + (x >= 0 ? 0.5 : -0.5)); }
double  from_fixed(int32_t v) { return (double)v / ONE; }

int32_t fx_mul(int32_t a, int32_t b) {
    int64_t wide = (int64_t)a * (int64_t)b;   /* 64-bit না হলে overflow */
    return (int32_t)(wide >> SCALE);
}

int32_t fx_div(int32_t a, int32_t b) {
    int64_t wide = (int64_t)a \<\< SCALE;
    return (int32_t)(wide / b);
}

int main(void) {
    int32_t a = to_fixed(2.5);
    int32_t b = to_fixed(1.5);
    int32_t prod = fx_mul(a, b);
    printf("a * b = %d (%f)\n", prod, from_fixed(prod));

    /* যদি int64_t intermediate না ব্যবহার করতাম: */
    int32_t bad_prod = a * b;   /* সরাসরি int32_t গুণ — এখানেই overflow */
    printf("bad (no wide intermediate): %d\n", bad_prod);
    return 0;
}
gcc -O2 -o fixed fixed.c && ./fixed

দ্বিতীয় লাইনটা (bad_prod) দেখাবে কেন wide intermediate ছাড়া fixed-point multiplication ভুল — 163840 × 98304 int32_t-এর সীমা (2147483647) ছাড়িয়ে যায় (161,061,273,600 — প্রায় 75 গুণ বড়), silent overflow ঘটবে।

এটা কী প্রমাণ করে

Fixed-point arithmetic আসলে বিশুদ্ধ integer arithmetic, শুধু একটা implied scale factor সহ — আর multiplication-এ shift না করলে scale ভুল হয়ে যায়।

নিজে বানান

BUILD IT

একটা সম্পূর্ণ Fixed-Point Vector Math Library

C অথবা Python · ●●○○○
  1. একটা fixed_t টাইপ ঠিক করুন (int32_t, Q16.16)
  2. to_fixed / from_fixed conversion function লিখুন, round-to-nearest সহ
  3. fx_add, fx_sub, fx_mul, fx_div — চারটা arithmetic operation লিখুন
  4. একটা 2D vector struct বানান (x, y দুটোই fixed_t), তার উপর dot product আর magnitude approximation লিখুন
  5. floating-point দিয়ে একই হিসাব করে দুটো ফলাফলের পার্থক্য measure করুন

উদ্দেশ্য: আজকের অনেক embedded/DSP/game-engine কোডবেসে এভাবেই math library শুরু হয়। শুরু করুন core operation দিয়ে:

#include <stdint.h>

typedef int32_t fx_t;
#define FX_SCALE 16
#define FX_ONE   ((fx_t)1 \<\< FX_SCALE)

static inline fx_t fx_from_float(float x) {
    return (fx_t)(x * FX_ONE + (x >= 0 ? 0.5f : -0.5f));
}
static inline float fx_to_float(fx_t v) {
    return (float)v / FX_ONE;
}
static inline fx_t fx_add(fx_t a, fx_t b) { return a + b; }
static inline fx_t fx_sub(fx_t a, fx_t b) { return a - b; }
static inline fx_t fx_mul(fx_t a, fx_t b) {
    return (fx_t)(((int64_t)a * (int64_t)b) >> FX_SCALE);
}
static inline fx_t fx_div(fx_t a, fx_t b) {
    return (fx_t)(((int64_t)a \<\< FX_SCALE) / b);
}

typedef struct { fx_t x, y; } fx_vec2;

fx_t fx_dot(fx_vec2 a, fx_vec2 b) {
    return fx_add(fx_mul(a.x, b.x), fx_mul(a.y, b.y));
}

/* magnitude-এর জন্য sqrt দরকার — এটাই আপনার চ্যালেঞ্জ:
   Newton-Raphson iteration দিয়ে fixed-point sqrt লিখুন,
   library-এর sqrt() ব্যবহার না করে। */
fx_t fx_sqrt_approx(fx_t v) {
    /* TODO: আপনি লিখুন — শুরুর অনুমান v/2, তারপর
       x_{n+1} = (x_n + v/x_n) / 2, ৪-৫ বার iterate করুন */
    return 0;
}

নিজে বাড়ান:

  1. fx_sqrt_approx সম্পূর্ণ করুন, math.sqrt / sqrtf-এর সাথে ১০০টা ভিন্ন মানে তুলনা করে max error বের করুন
  2. একটা fx_vec2 normalize করার function লিখুন (magnitude দিয়ে ভাগ করে unit vector বানানো) — খেয়াল রাখুন division-এ overflow না হয়
  3. একই সব operation float দিয়ে আবার লিখুন, ১০,০০০ বার পরপর যোগ-গুণ করে দুই সংস্করণের ফলাফল কতটা আলাদা হয় দেখুন
  4. Q16.16-এর বদলে Q8.24 ব্যবহার করে দেখুন — range কমে, precision বাড়ে; কোন কাজে কোনটা ভালো তা যুক্তি দিয়ে লিখুন

বাস্তব সিস্টেমে

Fixed-point যেখানে আজও রাজত্ব করে

DOOM (1993)-এর renderer। id Software-এর DOOM পুরো ৩D rendering pipeline fixed_t (৩২-বিট, ১৬.১৬ format) দিয়ে লেখা — m_fixed.h-এ সংজ্ঞায়িত। তখনকার x86 CPU-তে floating-point operation হয় ধীর ছিল, নয়তো একদমই ছিল না (486SX-এ কোনো FPU ছিল না) — তাই deterministic, দ্রুত fixed-point-ই একমাত্র practical পথ ছিল।

Embedded microcontroller, FPU ছাড়া। ARM Cortex-M0/M3-এর মতো সস্তা microcontroller-এ hardware FPU নেই। PID motor controller, sensor filtering — এসবের firmware আজও fixed-point-এ লেখা হয়, কারণ software floating-point emulation cycle-এর তুলনায় অনেক ধীর, আর real-time deadline মিস করা মানে motor নিয়ন্ত্রণ হারানো।

Audio DSP hardware। পুরনো Texas Instruments TMS320-সিরিজ DSP চিপ (মোবাইল ফোনের প্রথম যুগের codec, MP3/AAC decoder-এ ব্যবহৃত) fixed-point core — audio sample প্রায় সবসময় Q15/Q1.31 format-এ থাকে, কারণ −1.0 থেকে 1.0-এর মধ্যেই সব সীমাবদ্ধ, তাই পুরো bit budget precision-এ যায়।

GPS coordinate — Android-এর পুরনো API। প্রাথমিক Android mapping API (com.google.android.maps.GeoPoint) latitude/longitude কে “microdegrees” — degree × 10⁶ — একটা int-এ রাখত (getLatitudeE6())। কারণ compact, deterministic, আর serialization/network transfer-এ সস্তা।

RTS game-এর lockstep multiplayer। Age of Empires-এর মতো real-time strategy game multiplayer synchronization-এ “lockstep” মডেল ব্যবহার করে — প্রতিটা player-এর মেশিন একই simulation independently চালায়, শুধু input পাঠানো হয়। যদি simulation-এ floating-point থাকত, ভিন্ন CPU/compiler/optimization-flag-এ সামান্য ভিন্ন rounding হতে পারত (বিশেষত পুরনো x87 80-bit extended-precision registers-এর যুগে), আর একটা মেশিনও যদি এক বিট আলাদা হিসাব করত, পুরো game state desync হয়ে যেত। তাই বহু RTS-এর simulation core deterministic fixed-point-এ লেখা হয়েছে (দেখুন Paul Bettner-এর GDC talk “1500 Archers on a 28.8”)।

Financial/accounting সফটওয়্যার। টাকার amount কখনো binary float-এ রাখা হয় না — SQL-এর DECIMAL/NUMERIC type, বা integer cents, বা Python-এর decimal.Decimal — সবই মূলত decimal fixed-point। লেসন ৯-এ আমরা দেখব ঠিক কেন এটা আলোচনার বিষয় নয়, বাধ্যতামূলক নিয়ম।

Blockchain / smart contract। Solidity (Ethereum-এর ভাষা)-এ কোনো native floating-point type নেই — ইচ্ছাকৃতভাবে। সব টোকেন amount integer (সাধারণত ১৮ decimal place স্কেল করা, ERC-20 স্ট্যান্ডার্ড) — কারণ blockchain-এ প্রতিটা node-কে বিট-বিট identical ফলাফল পেতে হয়, আর floating-point-এর platform-নির্ভর সূক্ষ্ম পার্থক্য (দেখুন উপরের RTS উদাহরণ) consensus ভেঙে দিতে পারে।

Old-school GPU fixed-function pipeline। ৩D graphics hardware-এর প্রথম প্রজন্মে (১৯৯০-এর দশক) texture coordinate interpolation প্রায়ই fixed-point-এ হতো — perspective-correct floating-point interpolation তখনকার silicon budget-এ ব্যয়বহুল ছিল।

Bresenham-এর line drawing algorithm (১৯৬২)। IBM-এর Jack Bresenham একটা algorithm ডিজাইন করেছিলেন যা একটা সরলরেখা আঁকতে শুধু integer addition, subtraction, আর bit shift ব্যবহার করে — কোনো division বা floating-point multiplication ছাড়াই। মূল কৌশলটা কার্যত fixed-point-এর একটা বিশেষ রূপ — error term-টাকে একটা scaled integer হিসেবে বহন করা, প্রতি ধাপে যোগ করা, আর একটা threshold পার হলে সংশোধন করা। ১৯৬০-এর দশকের hardware-এ (যেখানে floating-point multiply শত শত cycle লাগত) এই কৌশলটাই ২D/৩D graphics-কে ব্যবহারিক করে তুলেছিল, আর আজও প্রতিটা modern GPU driver-এর কোথাও না কোথাও এর কোনো না কোনো রূপ টিকে আছে।

Block floating-point — fixed আর floating-এর মাঝামাঝি একটা হাইব্রিড। Radar signal processing, কিছু আধুনিক audio codec, আর পুরনো FFT hardware-এ একটা তৃতীয় কৌশল দেখা যায় — একগুচ্ছ (block) সংখ্যাকে একটাই common exponent ভাগাভাগি করতে দেওয়া, কিন্তু প্রতিটার নিজস্ব fixed-point mantissa রাখা। ধারণাটা: block-এর মধ্যে সবচেয়ে বড় সংখ্যার magnitude অনুযায়ী একটা exponent বেছে নেওয়া হয়, বাকি সবাই সেই একই scale-এ fixed-point হিসেবে store হয়। ফলাফল — প্রতিটা সংখ্যার জন্য আলাদা exponent store করার (পুরো floating-point-এর) খরচ ছাড়াই, pure fixed-point-এর চেয়ে বেশি dynamic range। এটাই দেখায় fixed আর floating-point কোনো কঠোর দ্বৈত (binary choice) না — এই বর্ণালীর মাঝে আরও অনেক design point সম্ভব, নির্দিষ্ট ব্যবহারের প্রয়োজন অনুযায়ী।

যে ভুলগুলো সবাই করে

“Floating-point সবসময় fixed-point-এর চেয়ে বেশি accurate।”

উল্টো হতে পারে — নির্দিষ্ট, সীমিত range-এর জন্য fixed-point প্রায়ই বেশি precise, কারণ প্রতিটা bit সরাসরি precision-এ যায়, কোনো bit exponent-এর জন্য “নষ্ট” হয় না।

Q1.31-এ (audio) প্রতিটা প্রতিনিধিত্বকারী মানের মধ্যে দূরত্ব সবসময় ঠিক 2⁻³¹ — সব magnitude-এ সমান। কিন্তু ৩২-বিট floating-point-এ (পরের লেসনে বিস্তারিত) precision ধ্রুবক নয় — সংখ্যা যত বড় হয়, ধারাবাহিক দুইটা representable মানের মধ্যে ফাঁক তত বড় হয়। −1.0-থেকে-1.0 সীমায় সীমাবদ্ধ audio sample-এর জন্য, সেই “বিশাল range ধরার” ক্ষমতাটাই অপ্রয়োজনীয় — আর fixed-point সেই অপ্রয়োজনীয় bit budgetটা precision-এ ঢেলে দেয়।

নিয়ম: যদি range আগে থেকে জানা এবং সীমিত থাকে, fixed-point প্রায়ই ভালো পছন্দ — বেশি precision, দ্রুত hardware (কোনো FPU লাগে না), আর deterministic।

“Fixed-point এখন 'পুরনো প্রযুক্তি', সব জায়গায় FPU আছে এখন।”

Desktop/server CPU-তে হ্যাঁ, প্রায় সবখানে FPU আছে। কিন্তু বিশ্বের বেশিরভাগ microcontroller (গাড়ি, IoT sensor, হোম অ্যাপ্লায়েন্স, industrial control) এখনো FPU ছাড়া চিপে চলে — কারণ FPU সিলিকন এরিয়া আর power খরচ করে, আর $০.৫০ মূল্যের একটা চিপে সেটা অপচয়।

আর যেখানে determinism critical (blockchain, lockstep multiplayer, regulatory-compliant financial calculation), সেখানে FPU থাকলেও ইচ্ছাকৃতভাবে fixed-point বেছে নেওয়া হয় — কারণ সমস্যাটা speed নয়, reproducibility

“Binary-তে দশমিক বিন্দু বসানো মানেই যেকোনো দশমিক সংখ্যা লেখা যায়, ঠিক যেমন কাগজে লিখি।”

Hardware-এ আক্ষরিক অর্থে কোনো “বিন্দু” নেই — শুধু bit-এর সারি। 0.75-কে fixed-point-এ লিখলে raw bit 11000000 (Q8.8-এর নিচের বাইট) — এই bit pattern নিজে কোথাও ”.” চিহ্ন বহন করে না। বিন্দুর অবস্থানটা সম্পূর্ণভাবে সফটওয়্যার convention — CPU জানেও না এটা fixed-point সংখ্যা, নাকি একটা সাধারণ integer, নাকি দুটো ৮-বিট flag। এটাই আগের লেসনের bit-pattern-interpretation থিসিসের আরেকটা উদাহরণ।

আর — বিন্দু বসালেও 0.1-এর মতো অনেক সংখ্যা binary-তে কখনোই সসীম আকারে লেখা যায় না, ঠিক যেমনটা এই লেসনের “hood” অংশে দেখানো হয়েছে। এটা decimal-এ 1/3 লেখার সমস্যার সমতুল্য — বিন্দু বসানো সমস্যাটা সমাধান করে না, শুধু জায়গা তৈরি করে।

“Fixed-point-এ ঋণাত্মক সংখ্যা হ্যান্ডেল করতে আলাদা, বিশেষ hardware লাগে।”

একেবারেই না — এটাই fixed-point-এর সবচেয়ে সুন্দর সুবিধাগুলোর একটা। যেহেতু ভেতরে এটা নিছক একটা integer (একটা implied scale factor সহ), negation-এর জন্য আগের লেসনের two’s complement নিয়মই হুবহু কাজ করে — bit উল্টে 1 যোগ। কোনো নতুন circuit, কোনো নতুন instruction লাগে না, বিদ্যমান integer ALU-ই যথেষ্ট (এই লেসনের Question ২-এ এই হিসাবটাই হাতে-কলমে দেখানো হয়েছে)।

এটাই fixed-point বনাম floating-point-এর একটা গভীর পার্থক্য — floating-point-এর negation এক অর্থে “সহজ” (শুধু sign bit flip করা, IEEE 754-এর পরের লেসনে দেখব), কিন্তু floating-point-এর addition/multiplication-এর জন্য সম্পূর্ণ নতুন, বিশেষায়িত hardware circuit লাগে (exponent align করা, mantissa শিফট করা, normalize করা, round করা)। Fixed-point ঠিক উল্টো ট্রেড-অফ করে — negation ঠিক ততটাই সহজ, কিন্তু বাকি সব operation-ই সাধারণ integer ALU দিয়ে চলে, কোনো বিশেষ circuit ছাড়াই।

বুঝেছেন কি না দেখুন

1

Q8.8 format-এ ক্ষুদ্রতম ধনাত্মক সংখ্যা আর সর্বোচ্চ সংখ্যা কত? হিসাব দেখিয়ে বলুন।

যুক্তি

Q8.8 মানে মোট ১৬ বিট, উপরের ৮ বিট (sign-সহ two’s complement) পূর্ণসংখ্যা, নিচের ৮ বিট ভগ্নাংশ। Scale factor 2⁸ = 256

ক্ষুদ্রতম ধনাত্মক সংখ্যা: raw integer 1 (সবচেয়ে ছোট অ-শূন্য মান)।

1256=0.00390625\frac{1}{256} = 0.00390625

সর্বোচ্চ সংখ্যা: ১৬-বিট signed integer-এর সর্বোচ্চ মান 32767 (2^{15} - 1, কারণ MSB sign bit)।

32767256=127.99609375\frac{32767}{256} = 127.99609375

সর্বনিম্ন সংখ্যা (প্রশ্নে না চাওয়া হলেও সম্পূর্ণতার জন্য): −32768 / 256 = −128.0

লক্ষ্য করুন সর্বনিম্ন মান একদম গোল সংখ্যায় শেষ হয় (−128.0) কিন্তু সর্বোচ্চ মান গোল হয় না (127.996..., ঠিক 128.0 নয়) — এটা two’s complement-এর সেই পরিচিত অসামঞ্জস্য, আগের integer overflow লেসনে যেটা দেখা গেছে (negative range সবসময় positive range-এর চেয়ে একটা মান বেশি ধরে)।

2

−2.5-কে Q8.8 two’s complement fixed-point-এ এনকোড করুন। হেক্সে চূড়ান্ত উত্তর দিন।

প্রয়োগ

ধাপ ১ — ধনাত্মক সংস্করণ এনকোড করুন:

2.5×256=6402.5 \times 256 = 640

640 ষোল বিটে বাইনারি: 0000 0010 1000 0000

ধাপ ২ — two’s complement negate করুন (আগের লেসনের কৌশল: সব বিট উল্টান, তারপর যোগ করুন):

   0000 0010 1000 0000   (640)
~  1111 1101 0111 1111   (bit-invert)
+  0000 0000 0000 0001   (1 যোগ)
=  1111 1101 1000 0000

ফলাফল: 1111 1101 1000 0000 = হেক্সে 0xFD80

যাচাই: 0xFD80 কে signed ১৬-বিট হিসেবে পড়লে −640 (দশমিকে), আর −640 / 256 = −2.5

এই পুরো প্রক্রিয়াটা লক্ষ্য করুন — negate করার জন্য fixed-point কোনো নতুন নিয়ম আনেনি, শুধু raw integer-এর উপর আগের লেসনের two’s complement নিয়মটাই প্রয়োগ হয়েছে। এটাই fixed-point-এর সবচেয়ে বড় সুবিধা: এটা কোনো নতুন hardware লজিক দাবি করে না, বিদ্যমান integer ALU-ই যথেষ্ট।

3

Q16.16-এ দুইটা সংখ্যা 100.0 আর 50.0 গুণ করলে raw integer গুণফল কত হবে, আর এটা কেন সরাসরি ৩২-বিট signed integer-এ রাখা যায় না?

প্রয়োগ

এনকোড করি: 100.0 × 65536 = 6,553,600 আর 50.0 × 65536 = 3,276,800

Raw গুণফল:

6,553,600×3,276,800=21,474,836,480,0006,553,600 \times 3,276,800 = 21,474,836,480,000

এই মান 2.1 \times 10^{13} — যেখানে ৩২-বিট signed integer-এর সর্বোচ্চ ধারণক্ষমতা মাত্র 2,147,483,647 (~2.1 \times 10^9)। গুণফলটা সীমার প্রায় ১০,০০০ গুণ বড়!

এই overflow ঘটার কারণ স্পষ্ট — দুটো Q16.16 সংখ্যা গুণ করলে scale factor-ও গুণ হয়ে যায় (2^{16} \times 2^{16} = 2^{32}), তাই মধ্যবর্তী ফলাফলের জন্য কমপক্ষে সমতুল্য চওড়া space লাগে। এই কারণেই বাস্তব fixed-point লাইব্রেরি (এই লেসনের Experiment ও BuildIt অংশে দেখা) সবসময় int64_t-এর মতো একটা চওড়া intermediate type ব্যবহার করে, শেষে >> 16 করে আবার Q16.16-এ নামিয়ে আনে —

21,474,836,480,00016=327,680,00021,474,836,480,000 \gg 16 = 327,680,000

327,680,000/65536=5000.0327,680,000 / 65536 = 5000.0

যা সঠিক — 100 \times 50 = 5000। কিন্তু মধ্যবর্তী পদক্ষেপে যদি ৩২-বিট-এ আটকে থাকা হতো, সেই পদক্ষেপেই silent wraparound হয়ে একটা সম্পূর্ণ ভুল উত্তর আসত।

4

DOOM-এর fixed-point renderer কেন যেকোনো মেশিনে বিট-বিট identical ফলাফল দিতে পারে, কিন্তু একটা আধুনিক floating-point-ভিত্তিক physics engine-এ একই replay দুই ভিন্ন মেশিনে সামান্য ভিন্ন ফল দিতে পারে?

যুক্তি

মূল পার্থক্যটা কোথায় “মান” থাকে তাতে — fixed-point-এ প্রতিটা সংখ্যা আসলে একটা সাধারণ integer, আর integer arithmetic (যোগ, বিয়োগ, গুণ, shift) সংজ্ঞা অনুযায়ীই exact — কোনো rounding নেই, কোনো ambiguity নেই। যেকোনো CPU, যেকোনো compiler, যেকোনো optimization level — 640 + 384 সবসময় এবং সর্বত্র ঠিক 1024

Floating-point-এর গল্পটা আলাদা (এই module-এর পরের দুই লেসনে বিস্তারিত): IEEE 754 standard নিজে deterministic, কিন্তু বাস্তব execution পথ একাধিক জায়গায় ভিন্ন হতে পারে —

  • Intermediate precision: পুরনো x86 x87 FPU internally ৮০-বিট extended precision ব্যবহার করত, ফলাফল ৬৪-বিট-এ নামানোর আগে extra বিট থাকত। SSE/SSE2 (আধুনিক x86 default) ৬৪-বিট-এই থাকে। একই source code, ভিন্ন instruction set target করলে ভিন্ন rounding হতে পারে।
  • Compiler optimization: -ffast-math-এর মতো flag associativity বদলে দিতে পারে ((a+b)+c কে a+(b+c)-এ পাল্টানো) — floating-point addition associative নয় (পরের লেসনে দেখব), তাই ভিন্ন গ্রুপিং ভিন্ন উত্তর দিতে পারে।
  • Fused multiply-add (FMA): কিছু CPU-তে a*b+c একটা single instruction-এ, extra precision-সহ, হিসাব হয় (কোনো intermediate rounding ছাড়াই) — অন্য CPU-তে সেটা দুই ধাপে (আলাদা rounding সহ) হয়।

fixed-point-এ এই কোনোটাই প্রাসঙ্গিক নয় — এটা এতটাই “boring” (শুধু integer shift-add-multiply) যে কোনো hardware/compiler-এর কাছে optimize করার মতো ambiguity অবশিষ্ট থাকে না। ঠিক এই “বিরক্তিকর predictability”-টাই DOOM-এর মতো বিট-নির্ভুল রেন্ডারার আর AoE-এর মতো lockstep multiplayer game-এর জন্য মূল্যবান।

5

আপনি একটা সস্তা microcontroller (FPU নেই)-এর firmware লিখছেন, যেটা একটা temperature sensor থেকে 0.0 °C থেকে 125.0 °C রেঞ্জের রিডিং নেয়, 0.01 °C precision দরকার, আর একটা ২০-স্যাম্পল moving average রাখতে হবে। Fixed নাকি floating-point ব্যবহার করবেন, আর কোন Q-format?

ডিজাইন

Fixed-point-ই সঠিক পছন্দ — তিনটা কারণে:

  1. FPU নেই — software floating-point emulation প্রতিটা operation-এ বহু cycle লাগাবে, real-time sensor loop-এ যেটা ব্যয়বহুল।
  2. Range আগে থেকেই জানা এবং সীমিত (0 থেকে 125) — fixed-point-এর সবচেয়ে বড় দুর্বলতা (dynamic range না থাকা) এখানে কোনো সমস্যাই না, কারণ dynamic range দরকারই নেই।
  3. Determinism দরকার — sensor reading-এর moving average যদি প্রতিবার সামান্য ভিন্ন rounding দেয়, downstream logic (যেমন alarm threshold) অস্থির আচরণ করতে পারে।

Q-format বাছাই:

Precision দরকার 0.01, range [0, 125]

ভগ্নাংশ বিট: 2^{-n} \le 0.01 সমাধান করলে n \ge 7 (2^{-7} = 0.0078125, যথেষ্ট মিহি)। নিরাপদ margin-এর জন্য n = 8 নিলে precision 1/256 ≈ 0.0039 — দাবির চেয়ে ভালো।

পূর্ণসংখ্যা বিট: 125-কে ধরতে (এবং negative reading বা sensor glitch handle করার জন্য একটু headroom সহ) m = 8 যথেষ্ট (signed 8-bit → −128 থেকে 127)।

সিদ্ধান্ত: Q8.8 (মোট ১৬ বিট, একটা int16_t)।

Moving average-এর জন্য বিশেষ সতর্কতা: ২০টা Q8.8 sample যোগ করলে সাময়িকভাবে মান 20 \times 125 \times 256 = 640,000 পর্যন্ত যেতে পারে — এটা int16_t-এর সীমা ছাড়িয়ে যায় (সর্বোচ্চ 32767), তাই running sum রাখতে হবে একটা চওড়া টাইপে (int32_t), শুধু final average বের করার সময় int16_t-এ narrow করে ফেরত আনা — এই লেসনের multiplication উদাহরণের মতোই নীতি: intermediate সবসময় চওড়া রাখুন, শেষে narrow করুন।

যদি range অজানা বা পরিবর্তনশীল হতো (ধরুন sensor একই firmware দিয়ে −40°C থেকে 1200°C পর্যন্ত ভিন্ন মডেলে ব্যবহার হয়), তখন floating-point-এর dynamic range-এর সুবিধাটা কাজে লাগত — কিন্তু এই নির্দিষ্ট সমস্যায় সেটা অপ্রয়োজনীয় জটিলতা এবং FPU-বিহীন hardware-এ সরাসরি performance খরচ।

6

“hood” অংশের টেবিলে 1/7-এর বাইনারি expansion-এর পুনরাবৃত্তি দৈর্ঘ্য বিট দেখানো হয়েছে। হাতে দেখান কেন, আর তারপর বলুন 1/14-এর পুনরাবৃত্তি দৈর্ঘ্য কত হবে।

যুক্তি

1/7-এর expansion হাতে বের করি:

1/7 × 2 = 2/7   → বিট 0   (remainder 2/7)
2/7 × 2 = 4/7   → বিট 0   (remainder 4/7)
4/7 × 2 = 8/7   → বিট 1   (remainder 1/7 — শুরুতে ফেরত!)

মাত্র ৩ ধাপেই remainder 1/7-এ ফিরে এসেছে — তাই পুনরাবৃত্তি দৈর্ঘ্য ঠিক : 1/7 = 0.(001)_2

কেন ঠিক ৩, অন্য কোনো সংখ্যা নয়: পুনরাবৃত্তি দৈর্ঘ্য সবসময় সেই ক্ষুদ্রতম k যার জন্য 2^k \equiv 1 \pmod{7} (এটাই 2-এর multiplicative order mod 7, Level 0-এর modular arithmetic লেসনের সরাসরি প্রয়োগ)। 2^1=2, 2^2=4, 2^3=8 \equiv 1 \pmod 7 — তাই order = 3, আর remainder-চক্রও ঠিক ৩ ধাপে ফিরে আসে, কাকতালীয় নয়।

1/14-এর জন্য: 14 = 2 \times 7। এই 2-এর factor-টা শুধু একটা non-repeating prefix যোগ করে (ঠিক যেমন 1/6 = 1/2 \times 1/3 এই লেসনের টেবিলে একটা prefix বিট পেয়েছিল), পুনরাবৃত্তি অংশের দৈর্ঘ্য বদলায় না — কারণ পুনরাবৃত্তি দৈর্ঘ্য নির্ভর করে শুধু q-এর odd part-এর (এখানে 7) উপর।

হাতে যাচাই করলে:

1/14 × 2 = 1/7   → বিট 0   (এখান থেকে ঠিক 1/7-এর চক্রই শুরু হয়ে যায়)
1/7  × 2 = 2/7   → বিট 0
2/7  × 2 = 4/7   → বিট 0
4/7  × 2 = 8/7   → বিট 1   (remainder 1/7 — এই বিন্দু থেকে চক্র শুরু)

1/14 = 0.0(001)_2 — একটা prefix বিট 0, তারপর সেই একই (001) চক্র যা 1/7-এ ছিল, দৈর্ঘ্য এখনো

সাধারণ নিয়ম: কোনো ভগ্নাংশ 1/q-এর বাইনারি পুনরাবৃত্তি দৈর্ঘ্য নির্ধারিত হয় শুধু q-এর odd (২-বহির্ভূত) অংশের দ্বারা — q-এর 2-এর ঘাত অংশটা শুধু একটা non-repeating prefix-এর দৈর্ঘ্য ঠিক করে, চক্রের দৈর্ঘ্য নয়। এটাই ব্যাখ্যা করে কেন Q-format-এ যত বেশি fractional bit-ই দিন না কেন, 1/7-জাতীয় ভগ্নাংশ কখনো exact হবে না — শুধু চক্রের আরও বেশি পুনরাবৃত্তি capture হবে, precision বাড়বে কিন্তু error শূন্যে পৌঁছাবে না।

এরপর কী

এরপর কী — bit-level-এ নেমে যাওয়া

এই লেসনে আমরা floating-point-এর ধারণা দেখেছি — sign, significand, exponent, আর কেন এই তিনটা টুকরো দরকার। কিন্তু এখনো একটা প্রশ্নের উত্তর দেওয়া হয়নি: ঠিক কতগুলো বিট কোন অংশে যায়, আর সেই বিটগুলো ঠিক কীভাবে সংখ্যায় রূপান্তরিত হয়?

পরের লেসন — IEEE 754 — এই module-এর সবচেয়ে গুরুত্বপূর্ণ লেসন। আমরা সেখানে দেখব:

  • ৩২-বিট আর ৬৪-বিট float-এর সঠিক bit layout, বিট-বাই-বিট
  • Exponent কেন two’s complement-এ নয়, biased — একটা চমৎকার ডিজাইন সিদ্ধান্ত যেটা float-দুটোকে সরাসরি integer-এর মতো তুলনা করা সম্ভব করে
  • একটা “লুকানো” বিট যেটা কখনো store হয় না, তবু precision বাড়ায়
  • 0.1-কে ৩২-বিট float-এ হাতে এনকোড করে দেখব আসল stored মান ঠিক 0.1 নয় — সেই মানটাই এই module-এর মূল প্রশ্নের চূড়ান্ত প্রমাণ

আর তারপর, লেসন ৯-এ, আমরা দেখব সেই ছোট্ট error কীভাবে arithmetic-এর মধ্য দিয়ে জমে, বাতিল হয়, বা বিপর্যয়কর হয়ে ওঠে — আর কীভাবে একটা ০.১-এর ২৪-বিট truncated approximation, ১৯৯১ সালে, প্রায় ১০০ ঘণ্টা জমতে জমতে একটা ক্ষেপণাস্ত্র প্রতিরক্ষা ব্যবস্থাকে তার লক্ষ্য থেকে বঞ্চিত করেছিল — এই লেসনের “concept” অংশের rounding বনাম truncation আলোচনার ঠিক সেই সতর্কবাণীর বাস্তব রূপ।

সংক্ষেপে — আজ আমরা শিখলাম কেন floating-point দরকার। পরের দুই লেসনে আমরা শিখব কীভাবে এটা বানানো হয়েছে, আর সেই নির্মাণে কোথায় কোথায় সাবধান থাকতে হয়।

আরও পড়ুন

  • Fixed-Point Arithmetic: An Introduction — Randy Yates · Q-format, scaling, overflow — DSP-এর প্রেক্ষাপটে fixed-point-এর প্রামাণ্য introduction
  • IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019) — IEEE Computer Society · যে standard পরের লেসনে বিস্তারিত আলোচনা হবে — এখানে এর জন্মের ঐতিহাসিক প্রেক্ষাপট
  • Game Engine Black Book: DOOM — Fabien Sanglard · id Software কেন renderer-এ 16.16 fixed-point বেছেছিল — বাস্তব ইঞ্জিনিয়ারিং সিদ্ধান্ত