এই পর্বে যা শিখবেন
- হাতে binary long division, আর ভাজ্য = ভাজক × ভাগফল + ভাগশেষ সূত্রের গুরুত্ব
- Restoring division-এর hardware আর পরীক্ষার ধাঁচের টেবিল — ৪-বিট আর ৮-বিট উদাহরণ, সারি ধরে ধরে
- Non-restoring division কেন কাজ করে (ছোট্ট একটি বীজগণিত), শেষের সংশোধন, আর ধাপ গোনার তুলনা
- Signed ভাগ, ভাগশেষের চিহ্নের তিন নিয়ম, শূন্য দিয়ে ভাগ আর INT_MIN ÷ −1 — x86, ARM, RISC-V আর C-তে কী হয়
- দ্রুত ভাগের কৌশল (SRT, Newton–Raphson, Goldschmidt) আর compiler-এর magic number — প্রমাণ আর যাচাইসহ
১ · ভাগ মানে কী — ভাজ্য, ভাজক, ভাগফল, ভাগশেষ আর হাতে binary ভাগ
পর্ব ২০-এ যোগ-বিয়োগ আর পর্ব ২১-এ গুণ দেখেছেন (Integer যোগ-বিয়োগ, গুণ)। চার মৌলিক গাণিতিক কাজের শেষটি হলো ভাগ — আর hardware-এর কাছে এটিই সবচেয়ে কঠিন। কেন কঠিন, সেটা বোঝাই এই পর্বের প্রথম লক্ষ্য। শুরু করা যাক চারটি নাম দিয়ে:
- ভাজ্য (dividend) — যাকে ভাগ করা হচ্ছে, যেমন ৭৪।
- ভাজক (divisor) — যা দিয়ে ভাগ করা হচ্ছে, যেমন ৮।
- ভাগফল (quotient) — কতবার ভাজক পুরোপুরি ঢোকে, এখানে ৯।
- ভাগশেষ (remainder) — যা বাকি থাকে, এখানে ২।
রাজশাহীর একজন আম ব্যবসায়ীর কাছে ৭৪টি আম আছে। প্রতিটি ঝুড়িতে ৮টি করে ভরবেন। তিনি ৯টি ঝুড়ি পুরো ভরতে পারবেন (৯ × ৮ = ৭২), হাতে থাকবে ২টি আম। ২টি আম দিয়ে আরেকটি ঝুড়ি ভরে না, তাই ভাগশেষ সবসময় ভাজকের চেয়ে ছোট। আবার ঝুড়ির সংখ্যা আর বাকি আম মিলিয়ে আসল সংখ্যা ফেরত পাওয়া যায়: ৯ × ৮ + ২ = ৭৪. কম্পিউটারের প্রতিটি ভাগ এই দুটি শর্ত মানতে বাধ্য।
এই দুটি শর্ত মিলেই ভাগের সংজ্ঞা। একে বলে division-এর invariant (যে সম্পর্ক সবসময় সত্য থাকে):
ধনাত্মক সংখ্যায় এই দুই শর্ত মানে এমন ভাগফল আর ভাগশেষ একটিই আছে। ছোট্ট প্রমাণ: ধরুন a = d·q1 + r1 = d·q2 + r2, দুটি ভাগশেষই 0 থেকে d − 1-এর মধ্যে। বিয়োগ করলে d·(q1 − q2) = r2 − r1. ডান পাশের মান −(d − 1) থেকে d − 1-এর মধ্যে, অথচ বাঁ পাশ d-এর গুণিতক। d-এর একমাত্র গুণিতক যা এই সীমার ভেতরে পড়ে তা হলো 0. তাই q1 = q2 আর r1 = r2. ঋণাত্মক সংখ্যা এলে দ্বিতীয় শর্তটি নিয়ে মতভেদ শুরু হয় — সেটি অংশ ৪-এর গল্প।
দশমিকে আর binary-তে long division
স্কুলে শেখা long division (লম্বা ভাগ)-এর কথা মনে করুন। দশমিকে প্রতিটি ধাপে আপনাকে আন্দাজ করতে হয়, ভাজক কতবার ঢুকবে — ০ থেকে ৯-এর মধ্যে কোনটি। ভুল আন্দাজ করলে মুছে আবার করতে হয়। Binary-তে এই সমস্যা নেই: প্রতিটি ধাপে ভাগফলের অঙ্ক শুধু ০ অথবা ১. প্রশ্ন একটিই — “এখনকার আংশিক ভাগশেষ থেকে ভাজক বিয়োগ করা যায় কি না?” যায় তো ১ লিখে বিয়োগ করুন, না যায় তো ০ লিখে পরের বিট নামিয়ে আনুন।
প্রথম উদাহরণ সবচেয়ে ছোট: ৭ ÷ ২, binary-তে 111 ÷ 10.
011 <- quotient (3)
------
10 ) 111
1 1 < 10 -> q bit 0
11 11 >= 10 -> q bit 1, 11 - 10 = 1
-10
11 bring down 1: 11 >= 10 -> q bit 1
-10
1 <- remainder (1) check: 10 x 11 + 1 = 111 (2 x 3 + 1 = 7)
দ্বিতীয় উদাহরণ একটু বড়: আমের হিসাব ৭৪ ÷ ৮, অর্থাৎ 1001010 ÷ 1000. নিচের stepper-এ প্রতিটি বিট নামিয়ে আনা আর তুলনা ধাপে ধাপে দেখুন। “পরে” চাপুন:
dividend 1 0 0 1 0 1 0 divisor 1000 take 1 : 1 < 1000 -> q = 0 take 10 : 10 < 1000 -> q = 00 take 100 : 100 < 1000 -> q = 000
take 1001 : 1001 >= 1000 -> q = 0001 1001 - 1000 ------ 1 partial remainder
bring down 0 : 10 < 1000 -> q = 00010 bring down 1 : 101 < 1000 -> q = 000100
bring down 0 : 1010 >= 1000 -> q = 0001001 1010 - 1000 ------ 10 remainder quotient = 0001001 = 9 remainder = 10 = 2
1000 x 1001 = 1001000 (8 x 9 = 72)
+ 10 (+ 2)
= 1001010 (74) OK
and 0 <= 10 < 1000 (0 <= 2 < 8) OK
- শুরু: বাঁ থেকে বিট নেওয়া। ভাজ্যের প্রথম বিট 1, প্রথম দুই বিট 10, প্রথম তিন বিট 100 — তিনটিই ভাজক 1000 (৮)-এর চেয়ে ছোট। তাই ভাগফলের প্রথম তিনটি বিট 0. দশমিকের মতো আন্দাজ করার দরকার হলো না; একটি তুলনাই যথেষ্ট।
- প্রথম বিয়োগ। চার বিট 1001 (৯) এবার ভাজকের সমান বা বড়। তাই ভাগফলের বিট 1, আর 1001 − 1000 = 1. এই 1-ই এখনকার আংশিক ভাগশেষ (partial remainder).
- দুটি বিট নামানো। পরের বিট 0 নামালে আংশিক ভাগশেষ 10 (২) — ছোট, ভাগফলে 0. তারপর 1 নামালে 101 (৫) — এখনো ছোট, ভাগফলে আরেকটি 0. দশমিকে যেমন “ঢোকে না, শূন্য বসাই”, এখানেও তাই।
- শেষ বিট আর ফল। শেষ 0 নামালে 1010 (১০) ≥ 1000, তাই বিট 1 আর 1010 − 1000 = 10. ভাগফল 0001001 = ৯, ভাগশেষ 10 = ২ — আমের হিসাবের সাথে মিলে গেল।
- Invariant দিয়ে যাচাই। 1000 × 1001 = 1001000 (binary-তে 1000, অর্থাৎ ৮ দিয়ে গুণ মানে তিন ঘর বাঁয়ে সরানো), তার সাথে 10 যোগ করলে 1001010 = ৭৪. আর ভাগশেষ ২ ভাজক ৮-এর চেয়ে ছোট। দুই শর্তই পূরণ — উত্তর নিশ্চিতভাবে ঠিক।
Binary ভাগ = বারবার “বাঁয়ে সরানো, বিয়োগ করে দেখা”। প্রতিটি ধাপে আংশিক ভাগশেষ দ্বিগুণ হয় (একটি বিট নামানো মানে ×২ + নতুন বিট), তারপর ভাজকের সাথে একটি তুলনা। আগের ভাগশেষ ভাজকের চেয়ে ছোট ছিল, তাই নতুন মান ভাজকের দ্বিগুণের চেয়ে ছোট — ভাজক তাতে বড়জোর একবার ঢোকে। এই কারণেই binary-তে প্রতিটি ভাগফল-বিট ০ বা ১, আর আন্দাজের দরকার নেই।
শেষ কথাটি গুরুত্বপূর্ণ, আর গুণের সাথে ভাগের বড় পার্থক্যও এখানেই। গুণে (পর্ব ২১) সবগুলো আংশিক গুণফল একে অপরের থেকে স্বাধীন — একসাথে বানিয়ে carry-save adder-এর গাছে যোগ করা যায়। ভাগে প্রতিটি ভাগফল-বিট জানতে আগের ধাপের ভাগশেষ লাগে। ধাপগুলো শিকলের মতো একটির পর একটি — এটাই ভাগ ধীর হওয়ার মূল কারণ।
২ · Restoring division — বিয়োগ করে দেখা, ঋণাত্মক হলে ফিরিয়ে আনা
Hardware-এ “তুলনা করে তারপর বিয়োগ” আলাদা দুটি কাজ না করে একটি কৌশল নেওয়া হয়: আগে বিয়োগ করে ফেলা, তারপর ফলের চিহ্ন দেখা। ফল ধনাত্মক বা শূন্য হলে বিয়োগ ঠিক ছিল — ভাগফল-বিট ১. ঋণাত্মক হলে বিয়োগ করা উচিত ছিল না — ভাজক আবার যোগ করে আগের মান ফিরিয়ে আনা (restore) আর ভাগফল-বিট ০. এই কারণে নাম restoring division.
Hardware-এ লাগে তিনটি register আর একটি adder-subtractor (পর্ব ১৩-এর ALU-ই যথেষ্ট):
| Register | প্রস্থ (n-বিট ভাগে) | শুরুতে | শেষে |
|---|---|---|---|
| M (divisor) | n বিট | ভাজক | অপরিবর্তিত |
| Q | n বিট | ভাজ্য | ভাগফল |
| A (remainder) | n + 1 বিট | 0 | ভাগশেষ |
A আর Q পাশাপাশি জোড়া লাগানো একটি ২n-বিটের shift register হিসেবে কাজ করে (A:Q). প্রতিবার বাঁয়ে সরালে Q-এর সবচেয়ে বাঁয়ের বিট A-তে ঢোকে — ঠিক long division-এ “পরের বিট নামিয়ে আনা”-র মতো — আর Q-এর ডান দিকে একটি খালি ঘর তৈরি হয়, যেখানে নতুন ভাগফল-বিট বসবে। ভাজ্যের বিটগুলো এক এক করে বেরিয়ে যায়, আর তাদের জায়গা ভাগফলের বিট দখল করে। A-তে একটি বাড়তি বিট রাখা হয় চিহ্ন দেখার জন্য, কারণ বিয়োগের ফল ঋণাত্মক হতে পারে (পর্ব ০৫-এর two's complement-এ)। অ্যালগরিদম:
A = 0; Q = dividend; M = divisor
repeat n times:
shift A:Q left by one bit # next dividend bit enters A
A = A - M # trial subtraction
if A < 0: q0 = 0; A = A + M # restore
else: q0 = 1
quotient = Q; remainder = A
এবার পরীক্ষার সবচেয়ে প্রিয় উদাহরণ: ৪-বিটে 0111 ÷ 0010 (7 ÷ 2). A-কে ৫ বিটে দেখানো হয়েছে। প্রতিটি ধাপে কোন লাইন চলছে আর register-এ কী আছে, মিলিয়ে দেখুন:
1A = 0; Q = dividend; M = divisor2repeat n times:3 shift A:Q left by 14 A = A - M5 if A < 0: q0 = 0; A = A + M6 else: q0 = 17quotient = Q; remainder = A
| iteration | |
| A (৫ বিট) | |
| Q | |
| যোগ-বিয়োগ মোট |
- শুরুর অবস্থা। A = 00000, Q = 0111 (ভাজ্য ৭), M = 00010 (ভাজক ২). n = ৪, তাই লুপ চলবে ৪ বার। এখনো কোনো যোগ-বিয়োগ হয়নি।
- Iteration ১: shift আর বিয়োগ। A:Q = 00000 0111 বাঁয়ে সরলে Q-এর বাঁয়ের বিট 0 A-তে ঢোকে: A = 00000, Q = 111_ (ডানের ঘর খালি)। তারপর A − M = 0 − 2 = −2 = 11110 (৫-বিট two's complement). চিহ্ন-বিট ১ — ঋণাত্মক।
- Iteration ১: restore. ঋণাত্মক মানে ভাজক এখানে ঢোকে না। তাই খালি ঘরে q0 = 0 (Q = 1110), আর A + M = −2 + 2 = 0 করে আগের মান ফিরিয়ে আনা হলো। এই একটি iteration-এই দুটি যোগ-বিয়োগ খরচ হলো।
- Iteration ২: shift আর বিয়োগ। A:Q = 00000 1110 বাঁয়ে সরলে Q-এর বাঁয়ের ১ A-তে আসে: A = 00001, Q = 110_. A − M = 1 − 2 = −1 = 11111 — আবার ঋণাত্মক।
- Iteration ২: আবার restore. q0 = 0 (Q = 1100), A = −1 + 2 = 1. এখন পর্যন্ত মোট ৪টি যোগ-বিয়োগ, অথচ ভাগফলের মাত্র দুটি বিট (দুটিই ০) পাওয়া গেছে।
- Iteration ৩: shift আর বিয়োগ। A:Q = 00001 1100 বাঁয়ে সরে A = 00011 (৩), Q = 100_. A − M = 3 − 2 = 1 = 00001 — ধনাত্মক।
- Iteration ৩: q0 = 1. ভাজক ঢুকেছে, তাই ফল যেমন আছে রেখে দেওয়া হলো, শুধু q0 = 1: Q = 1001. কোনো restore নেই, তাই এই iteration-এ একটিই বিয়োগ।
- Iteration ৪: shift আর বিয়োগ। A:Q = 00001 1001 বাঁয়ে সরে A = 00011 (৩), Q = 001_. A − M = 1 — ধনাত্মক।
- Iteration ৪ আর ফল। q0 = 1, Q = 0011. লুপ শেষ: ভাগফল Q = 0011 = ৩, ভাগশেষ A = 00001 = ১. যাচাই: 2 × 3 + 1 = 7 ✓. মোট ৬টি যোগ-বিয়োগ — ৪টি বিয়োগ আর ২টি restore.
এই অ্যালগরিদম কেন সবসময় ঠিক উত্তর দেয়? প্রতিটি iteration-এর শুরুতে একটি invariant সত্য থাকে: 0 ≤ A < M, আর A হলো এ পর্যন্ত নামানো ভাজ্য-বিটগুলোর ভাগশেষ। Shift করলে A হয় 2A + (পরের বিট), যা 2M − 1-এর চেয়ে বড় হতে পারে না। তাই M বড়জোর একবার ঢোকে, আর বিয়োগ (প্রয়োজনে restore) করলে আবার 0 ≤ A < M. n বার পরে সব বিট নামানো শেষ, তাই A পুরো ভাজ্যের ভাগশেষ আর Q পুরো ভাগফল। এই একই যুক্তি দেখায়, কেন A-তে n + 1 বিট লাগে: 2A + 1 সর্বোচ্চ 2M − 1, যা n বিটে না-ও ধরতে পারে (M-এর MSB ১ হলে)।
৮-বিটের একটি উদাহরণ: 1001 0110 ÷ 0000 1011 (150 ÷ 11)
পরীক্ষায় প্রায়ই ৮-বিটের একটি টেবিল চাওয়া হয়। নিয়ম একই, শুধু ৮টি iteration. প্রতিটি iteration-এর শেষে A আর Q কী দাঁড়াল, তা নিচে (A ৯ বিট):
| Iteration | Shift-এর পর A | A − M | সিদ্ধান্ত | শেষে A | শেষে Q |
|---|---|---|---|---|---|
| ০ | — | — | শুরু | 0 0000 0000 | 1001 0110 |
| ১ | 1 | −10 | restore, q0 = 0 | 0 0000 0001 (1) | 0010 1100 |
| ২ | 2 | −9 | restore, q0 = 0 | 0 0000 0010 (2) | 0101 1000 |
| ৩ | 4 | −7 | restore, q0 = 0 | 0 0000 0100 (4) | 1011 0000 |
| ৪ | 9 | −2 | restore, q0 = 0 | 0 0000 1001 (9) | 0110 0000 |
| ৫ | 18 | 7 | q0 = 1 | 0 0000 0111 (7) | 1100 0001 |
| ৬ | 15 | 4 | q0 = 1 | 0 0000 0100 (4) | 1000 0011 |
| ৭ | 9 | −2 | restore, q0 = 0 | 0 0000 1001 (9) | 0000 0110 |
| ৮ | 18 | 7 | q0 = 1 | 0 0000 0111 (7) | 0000 1101 |
ফল: ভাগফল 0000 1101 = ১৩, ভাগশেষ ৭. যাচাই: 11 × 13 + 7 = 143 + 7 = 150 ✓. এখানে ৮টি বিয়োগ আর ৫টি restore — মোট ১৩টি যোগ-বিয়োগ। লক্ষ করুন, ভাগফলের যতগুলো বিট ০, ঠিক ততগুলো restore. সবচেয়ে খারাপ ক্ষেত্রে (ভাগফল ০) n বিয়োগ + n restore = 2n; গড়ে মোটামুটি 1.5n.
পাঠ্যবইয়ের দুটি রূপ। Patterson–Hennessy প্রথমে একটি সরল রূপ দেখান: ভাজক থাকে একটি ২n-বিট register-এর বাঁ অর্ধেকে, প্রতি ধাপে ডানে সরে, ALU-ও ২n বিটের, আর লুপ চলে n + 1 বার। তারপর “উন্নত” (refined) রূপে ভাজক register আর ALU n বিটের হয়ে যায়, আর ভাগশেষ ও ভাগফল একটিই ২n-বিট register ভাগ করে নেয় — ভাগশেষ বাঁ অর্ধেকে, ভাগফল ডান অর্ধেকে, পুরোটা বাঁয়ে সরে। ওপরের A:Q নকশা মূলত সেই উন্নত রূপ (Hamacher-সহ অনেক বইয়ে এভাবেই লেখা)। বইভেদে টেবিলের সারির সংখ্যা আর shift-এর মুহূর্ত একটু আলাদা হতে পারে; পরীক্ষায় যে বই অনুসরণ করা হয়, তার ধাপের ক্রম মেনে চলুন — উত্তর একই আসবে।
৩ · Non-restoring division — ফিরিয়ে না এনে পরের ধাপে যোগ
Restoring-এর অপচয়টি স্পষ্ট: ঋণাত্মক হলে একটি যোগ করে আগের মান ফেরানো, তারপর পরের ধাপে shift করে আবার বিয়োগ। দুটি কাজকে একটিতে নামানো যায় কি? ছোট্ট একটি বীজগণিত দেখুন। ধরুন বিয়োগের পর A ঋণাত্মক হলো। Restoring পরের ধাপে যা করে:
অর্থাৎ restore না করে সরাসরি shift করে M যোগ করলেই একই মান আসে! এটাই non-restoring division-এর মূল চিন্তা। নিয়ম:
A = 0; Q = dividend; M = divisor
repeat n times:
if A >= 0: shift A:Q left; A = A - M
else: shift A:Q left; A = A + M
q0 = 1 if A >= 0 else 0
if A < 0: A = A + M # final correction of the remainder
quotient = Q; remainder = A
প্রতি iteration-এ ঠিক একটি যোগ বা বিয়োগ, তাই মোট n টি, আর শেষে A ঋণাত্মক থাকলে একটি সংশোধন — সর্বোচ্চ n + 1. ভাগফল-বিটের নিয়ম বদলায়নি: নতুন A ধনাত্মক হলে ১, ঋণাত্মক হলে ০. শেষের সংশোধন কেন লাগে? শেষ ধাপে A ঋণাত্মক থাকলে পরের কোনো ধাপ নেই যেখানে “যোগ” দিয়ে তা পুষিয়ে নেওয়া যাবে, তাই restoring-এর মতো একবার M যোগ করে আসল ভাগশেষ ফেরাতে হয়। ভাগফল তখনো ঠিক থাকে, কারণ ঋণাত্মক A-তে q0 = 0 ইতিমধ্যে বসানো আছে।
নিচের stepper-এ একই 7 ÷ 2-এর দ্বিতীয় iteration দেখুন — যেখানে আগের ধাপের ফল ঋণাত্মক, আর restoring হলে কী হতো তার সাথে তুলনা:
A:Q = 00000 0111 M = 00010 shift -> A = 00000, Q = 111_ A = A - M -> A = 11110 (-2) negative q0 = 0 -> Q = 1110 no restore! A stays -2
A:Q = 11110 1110 shift -> A = 11101 (-3), Q = 110_ A = A + M -> A = 11111 (-1) still negative q0 = 0 -> Q = 1100 operations so far: 2
restoring: restore 11110 + 00010 = 00000 (0)
shift 00000 -> 00001 (1)
subtract 00001 - 00010 = 11111 (-1)
non-rest.: shift 11110 -> 11101 (-3), add -> 11111 (-1)
same A, same q0 -- restoring used 3 ops, non-restoring 2
A = -2, M = 2, next dividend bit = 1 restoring : 2(A + M) - M + 1 = 2(0) - 2 + 1 = -1 non-restoring: 2A + M + 1 = -4 + 2 + 1 = -1 (+1 = the dividend bit that entered A during the shift)
iter 3: A = -1 < 0 -> shift A = 11111 (-1), A + M = 00001 (1), q0 = 1 iter 4: A = 1 >= 0 -> shift A = 00011 (3), A - M = 00001 (1), q0 = 1 A >= 0 at the end -> no correction needed Q = 0011 (3), R = 00001 (1) add/sub: non-restoring 4 vs restoring 6
- Iteration ১ — বিয়োগে ঋণাত্মক। শুরুতে A = 0 (ধনাত্মক), তাই shift করে বিয়োগ: A = −2. ভাগফল-বিট 0. Restoring হলে এখন M যোগ করে A = 0 করত; non-restoring A = −2 রেখেই পরের ধাপে চলে যায়।
- Iteration ২ — ঋণাত্মক থেকে শুরু, তাই যোগ। A:Q = 11110 1110 বাঁয়ে সরে A = 11101 (−৩): −2 × 2 = −4, তার সাথে Q থেকে আসা বিট ১. A ঋণাত্মক ছিল, তাই এবার বিয়োগ নয়, যোগ: −3 + 2 = −1. এখনো ঋণাত্মক, তাই q0 = 0. মোট খরচ এ পর্যন্ত ২টি কাজ।
- Restoring-এর সাথে তুলনা। Restoring একই জায়গায় তিনটি কাজ করত: restore (−2 + 2 = 0), shift (0 থেকে 1), বিয়োগ (1 − 2 = −1). শেষে A = −1 — non-restoring-এর মানের সাথে হুবহু এক, ভাগফল-বিটও এক। পার্থক্য শুধু একটি বাড়তি যোগ।
- বীজগণিত। Restoring করে 2(A + M) − M + b, আর non-restoring করে 2A + M + b, যেখানে b হলো shift-এ ঢোকা ভাজ্য-বিট। খুলে লিখলে দুটিই 2A + M + b. এখানে A = −2, M = 2, b = 1: দুই দিকেই −1. এই এক লাইনের সমীকরণই পুরো অ্যালগরিদমের প্রমাণ।
- শেষ পর্যন্ত। Iteration ৩-এ A = −1 ঋণাত্মক, তাই shift করে যোগ: −1 × 2 + 1 = −1, তারপর −1 + 2 = 1 → q0 = 1. Iteration ৪-এ A ধনাত্মক, তাই বিয়োগ: 3 − 2 = 1 → q0 = 1. শেষে A ধনাত্মক, সংশোধন লাগল না। ভাগফল 0011 (৩), ভাগশেষ ১ — restoring-এর সমান, কিন্তু ৬-এর বদলে ৪টি যোগ-বিয়োগে।
নিচের simulator দুটি অ্যালগরিদমকেই একই ইনপুটে চালায় — ৪-বিটের 7 ÷ 2 আর ৮-বিটের 150 ÷ 11 — আর পরীক্ষার টেবিলের মতো কলাম ছাপে: iteration, ধাপ, quotient (Q), divisor (M) আর remainder (A). Run চাপুন, তারপর নিজের সংখ্যা বসিয়ে দেখুন:
// Restoring vs non-restoring division, n-bit unsigned. A has n+1 bits (sign).
const b = (v, w) => (v >>> 0).toString(2).padStart(32, "0").slice(-w);
function divide(N, M, n, restoring) {
let A = 0, Q = N, ops = 0;
const row = (it, step) => console.log(String(it).padEnd(5) + step.padEnd(18) +
b(Q, n).padEnd(10) + b(M, n + 1).padEnd(11) + b(A, n + 1) + ` (${A})`);
console.log((restoring ? "RESTORING" : "NON-RESTORING") +
"\niter step quotient divisor remainder A");
row(0, "initial");
for (let i = 1; i <= n; i++) {
const wasNeg = A < 0;
A = (A << 1) | ((Q >> (n - 1)) & 1); // shift A:Q left by one
Q = (Q << 1) & ((1 << n) - 1);
if (restoring || !wasNeg) { A -= M; ops++; row(i, "shl, A = A - M"); }
else { A += M; ops++; row(i, "shl, A = A + M"); }
if (A >= 0) { Q |= 1; row(i, "A>=0: q0 = 1"); }
else if (restoring) { A += M; ops++; row(i, "A<0: restore"); }
else row(i, "A<0: q0 = 0");
}
if (!restoring && A < 0) { A += M; ops++; row("end", "fix: A = A + M"); }
console.log(`=> Q = ${Q}, R = ${A}, add/sub operations = ${ops}\n`);
}
for (const [N, M, n] of [[7, 2, 4], [150, 11, 8]]) {
console.log(`=== ${N} / ${M} as ${n}-bit: ${b(N, n)} / ${b(M, n)} ===`);
divide(N, M, n, true);
divide(N, M, n, false);
}আউটপুট (নির্ধারিত — প্রতিবার একই):
=== 7 / 2 as 4-bit: 0111 / 0010 ===
RESTORING
iter step quotient divisor remainder A
0 initial 0111 00010 00000 (0)
1 shl, A = A - M 1110 00010 11110 (-2)
1 A<0: restore 1110 00010 00000 (0)
2 shl, A = A - M 1100 00010 11111 (-1)
2 A<0: restore 1100 00010 00001 (1)
3 shl, A = A - M 1000 00010 00001 (1)
3 A>=0: q0 = 1 1001 00010 00001 (1)
4 shl, A = A - M 0010 00010 00001 (1)
4 A>=0: q0 = 1 0011 00010 00001 (1)
=> Q = 3, R = 1, add/sub operations = 6
NON-RESTORING
iter step quotient divisor remainder A
0 initial 0111 00010 00000 (0)
1 shl, A = A - M 1110 00010 11110 (-2)
1 A<0: q0 = 0 1110 00010 11110 (-2)
2 shl, A = A + M 1100 00010 11111 (-1)
2 A<0: q0 = 0 1100 00010 11111 (-1)
3 shl, A = A + M 1000 00010 00001 (1)
3 A>=0: q0 = 1 1001 00010 00001 (1)
4 shl, A = A - M 0010 00010 00001 (1)
4 A>=0: q0 = 1 0011 00010 00001 (1)
=> Q = 3, R = 1, add/sub operations = 4
=== 150 / 11 as 8-bit: 10010110 / 00001011 ===
RESTORING
iter step quotient divisor remainder A
0 initial 10010110 000001011 000000000 (0)
1 shl, A = A - M 00101100 000001011 111110110 (-10)
1 A<0: restore 00101100 000001011 000000001 (1)
2 shl, A = A - M 01011000 000001011 111110111 (-9)
2 A<0: restore 01011000 000001011 000000010 (2)
3 shl, A = A - M 10110000 000001011 111111001 (-7)
3 A<0: restore 10110000 000001011 000000100 (4)
4 shl, A = A - M 01100000 000001011 111111110 (-2)
4 A<0: restore 01100000 000001011 000001001 (9)
5 shl, A = A - M 11000000 000001011 000000111 (7)
5 A>=0: q0 = 1 11000001 000001011 000000111 (7)
6 shl, A = A - M 10000010 000001011 000000100 (4)
6 A>=0: q0 = 1 10000011 000001011 000000100 (4)
7 shl, A = A - M 00000110 000001011 111111110 (-2)
7 A<0: restore 00000110 000001011 000001001 (9)
8 shl, A = A - M 00001100 000001011 000000111 (7)
8 A>=0: q0 = 1 00001101 000001011 000000111 (7)
=> Q = 13, R = 7, add/sub operations = 13
NON-RESTORING
iter step quotient divisor remainder A
0 initial 10010110 000001011 000000000 (0)
1 shl, A = A - M 00101100 000001011 111110110 (-10)
1 A<0: q0 = 0 00101100 000001011 111110110 (-10)
2 shl, A = A + M 01011000 000001011 111110111 (-9)
2 A<0: q0 = 0 01011000 000001011 111110111 (-9)
3 shl, A = A + M 10110000 000001011 111111001 (-7)
3 A<0: q0 = 0 10110000 000001011 111111001 (-7)
4 shl, A = A + M 01100000 000001011 111111110 (-2)
4 A<0: q0 = 0 01100000 000001011 111111110 (-2)
5 shl, A = A + M 11000000 000001011 000000111 (7)
5 A>=0: q0 = 1 11000001 000001011 000000111 (7)
6 shl, A = A - M 10000010 000001011 000000100 (4)
6 A>=0: q0 = 1 10000011 000001011 000000100 (4)
7 shl, A = A - M 00000110 000001011 111111110 (-2)
7 A<0: q0 = 0 00000110 000001011 111111110 (-2)
8 shl, A = A + M 00001100 000001011 000000111 (7)
8 A>=0: q0 = 1 00001101 000001011 000000111 (7)
=> Q = 13, R = 7, add/sub operations = 8
৪-বিটের ভাগে দুই অ্যালগরিদমই দেয় Q = 3, R = 1, কিন্তু restoring-এর ৬টি কাজের জায়গায় non-restoring করে ৪টি। Restoring টেবিলে “A<0: restore” সারির পর A আবার ধনাত্মক, আর non-restoring টেবিলে সেই জায়গায় A ঋণাত্মক রয়ে যায় (−2, −1), পরের সারিতে “A = A + M”. ৮-বিটের 150 ÷ 11-এ পার্থক্য আরও বড়: restoring ১৩টি কাজ (৮টি বিয়োগ + ৫টি restore — ভাগফল 00001101-এর পাঁচটি ০-এর জন্য), non-restoring ঠিক ৮টি। দুই ক্ষেত্রেই শেষ A ধনাত্মক, তাই “fix” সারি আসেনি; 13 ÷ 3 বসিয়ে চালালে শেষে fix: A = A + M সারিটি দেখতে পাবেন।
| বিষয় | Restoring | Non-restoring |
|---|---|---|
| প্রতি iteration-এ কাজ | ১টি বিয়োগ, ঋণাত্মক হলে আরও ১টি যোগ | ঠিক ১টি — যোগ বা বিয়োগ |
| মোট যোগ-বিয়োগ (n বিট) | n থেকে 2n (গড়ে প্রায় 1.5n) | n, আর দরকার হলে ১টি সংশোধন |
| কোন কাজ, কীভাবে ঠিক হয় | নতুন ফলের চিহ্ন দেখে | আগের A-এর চিহ্ন দেখে |
| শেষের সংশোধন | লাগে না | শেষ A ঋণাত্মক হলে A + M |
| Hardware | সরল; ধাপগুলো বোঝা সহজ | প্রায় একই; adder-subtractor-এর mode আগের চিহ্ন-বিট থেকে আসে |
সাধারণ ভুল: (১) Non-restoring-এ শেষের সংশোধন ভুলে যাওয়া — তখন ভাগশেষ ঋণাত্মক আসে (13 ÷ 3-এ −2), যদিও ভাগফল ঠিক থাকে। (২) A-কে n বিটে রাখা — ভাজকের MSB ১ হলে (যেমন ৪-বিটে 1111 ÷ 1001) shift-এর পর A উপচে পড়ে আর চিহ্ন ভুল দেখায়; এক বিট বাড়তি লাগে, বা n-বিট adder-এর carry-out দেখে চিহ্ন বুঝতে হয়। (৩) Non-restoring-এ ভাগফল-বিট আগের A-এর চিহ্ন থেকে বসানো — ভাগফল-বিট আসে নতুন A-এর চিহ্ন থেকে, আগের A শুধু ঠিক করে যোগ না বিয়োগ।
৪ · Signed ভাগ — চিহ্ন আলাদা, আর ভাগশেষের তিন নিয়ম
ওপরের দুটি অ্যালগরিদম unsigned সংখ্যার জন্য। ঋণাত্মক সংখ্যা ভাগ করার সবচেয়ে সরল (আর বহু hardware-এ ব্যবহৃত) পদ্ধতি হলো চিহ্ন আলাদা করে রাখা:
- দুটি সংখ্যার মান (magnitude) নিয়ে unsigned ভাগ করা: |a| ÷ |d| = q′, ভাগশেষ r′.
- ভাগফলের চিহ্ন = দুটি চিহ্ন-বিটের XOR: চিহ্ন আলাদা হলে ভাগফল ঋণাত্মক।
- ভাগশেষের চিহ্ন = ভাজ্যের চিহ্ন (এই নিয়মে, যা C আর RISC-V মানে)।
যেমন −7 ÷ 2: 7 ÷ 2 = 3, ভাগশেষ 1. চিহ্ন আলাদা, তাই ভাগফল −3; ভাজ্য ঋণাত্মক, তাই ভাগশেষ −1. যাচাই: 2 × (−3) + (−1) = −7 ✓. একটি খুঁটিনাটি: ৩২-বিটে |INT_MIN| = 231, যা signed-এ ধরে না কিন্তু ৩২-বিট unsigned-এ ধরে — তাই magnitude-কে unsigned হিসেবে রাখলে সমস্যা নেই। (Two's complement-এ সরাসরি ভাগের non-restoring রূপও আছে, কিন্তু ধারণা বোঝার জন্য এই পথটিই সহজ।)
ভাগশেষের চিহ্ন — তিনটি প্রচলিত নিয়ম
ঋণাত্মক সংখ্যায় অংশ ১-এর invariant-এর প্রথম অংশ (a = d·q + r) সবাই মানে, কিন্তু “ভাগশেষ কোন সীমায় থাকবে” নিয়ে তিনটি প্রচলিত নিয়ম আছে। প্রতিটি নিয়ম ভাগফলকে কোন দিকে গোল (round) করে, সেটাই পার্থক্য:
Truncated (শূন্যের দিকে)
q = a / d-এর দশমিক অংশ কেটে ফেলা। ভাগশেষের চিহ্ন ভাজ্যের মতো। C99 থেকে C, C++11 থেকে C++, Java, C#, Go, Rust-এর / ও %; RISC-V-এর div/rem, x86-এর idiv.
Floored (নিচের দিকে)
q = ⌊a / d⌋. ভাগশেষের চিহ্ন ভাজকের মতো। Python-এর // ও %, Ruby, আর Java-র Math.floorDiv/Math.floorMod.
Euclidean
ভাগশেষ সবসময় 0 ≤ r < |d|, ভাজক যেমনই হোক। গণিতের সংখ্যাতত্ত্বে স্বাভাবিক; Rust-এর div_euclid/rem_euclid-এ পাওয়া যায়।
±7 ÷ ±2-এর চারটি জোড়ায় তিন নিয়ম পাশাপাশি (প্রতিটি সারিতে d × q + r = a যাচাই করা):
| a ÷ d | Truncated (C, Java, RISC-V) | Floored (Python) | Euclidean |
|---|---|---|---|
| 7 ÷ 2 | q = 3, r = 1 | q = 3, r = 1 | q = 3, r = 1 |
| −7 ÷ 2 | q = −3, r = −1 | q = −4, r = 1 | q = −4, r = 1 |
| 7 ÷ −2 | q = −3, r = 1 | q = −4, r = −1 | q = −3, r = 1 |
| −7 ÷ −2 | q = 3, r = −1 | q = 3, r = −1 | q = 4, r = 1 |
লক্ষ করুন: দুটি সংখ্যাই ধনাত্মক হলে তিন নিয়মই এক। ভাজক ধনাত্মক হলে floored আর Euclidean এক। আর তিন নিয়মেই |r| < |d| — পার্থক্য শুধু r-এর চিহ্নে, আর সেই অনুযায়ী q এক কম-বেশি। একটি দরকারি সম্পর্ক: truncated-এর ভাগশেষ r ≠ 0 হলে আর r-এর চিহ্ন d-এর উল্টো হলে, floored পেতে q থেকে 1 বাদ দিন আর r-এর সাথে d যোগ করুন। −7 ÷ 2: (−3, −1) থেকে (−4, 1).
নিচের প্রোগ্রাম তিন নিয়মকেই কয়েকটি ± জোড়ায় চালায় আর প্রতিটির জন্য invariant যাচাই করে। সাথে RISC-V-এর দুটি বিশেষ ক্ষেত্র — শূন্য দিয়ে ভাগ আর INT_MIN ÷ −1 — spec-এর টেবিলের নিয়ম হুবহু কোডে লিখে (এগুলো অংশ ৫-এর বিষয়):
// Three conventions for signed integer division. Each must give a = d*q + r.
const trunc = (a, d) => { const q = Math.trunc(a / d) || 0; return [q, a - d * q]; }; // C, Java, RISC-V
const floor = (a, d) => { const q = Math.floor(a / d); return [q, a - d * q]; }; // Python // and %
const eucl = (a, d) => { // r is never negative
const m = Math.abs(d), r = ((a % m) + m) % m; return [(a - r) / d, r];
};
const cell = ([q, r]) => `q=${q} r=${r}`.padEnd(11);
console.log(" a d | truncated | floored | Euclidean | a = d*q + r, |r| < |d| ?");
for (const [a, d] of [[7, 2], [-7, 2], [7, -2], [-7, -2], [-7, 3], [6, -3]]) {
const res = [trunc(a, d), floor(a, d), eucl(a, d)];
const ok = res.every(([q, r]) => d * q + r === a && Math.abs(r) < Math.abs(d));
console.log(`${String(a).padStart(3)} ${String(d).padStart(3)} | ${res.map(cell).join("| ")}| ${ok}`);
}
// RISC-V "M" extension, RV32: the spec's table for the special cases (no trap!)
const INT_MIN = -2147483648;
function rvDiv(a, d) { // div / rem (signed)
if (d === 0) return [-1, a]; // quotient: all bits set; remainder: the dividend
if (a === INT_MIN && d === -1) return [INT_MIN, 0]; // overflow case
return trunc(a, d);
}
const rvDivu = (a, d) => (d === 0 ? [0xFFFFFFFF, a] : [Math.floor(a / d), a % d]);
const hex = v => "0x" + (v >>> 0).toString(16).toUpperCase().padStart(8, "0");
console.log("\nRISC-V rule (from the spec table) quotient remainder");
for (const [a, d] of [[7, 0], [-7, 0], [INT_MIN, -1], [-7, 2]]) {
const [q, r] = rvDiv(a, d);
console.log(`div/rem ${String(a).padStart(11)} / ${String(d).padStart(2)} ${String(q).padStart(11)} (${hex(q)}) ${r}`);
}
const [uq, ur] = rvDivu(7, 0);
console.log(`divu/remu 7 / 0 ${String(uq).padStart(11)} (${hex(uq)}) ${ur}`);আউটপুট:
a d | truncated | floored | Euclidean | a = d*q + r, |r| < |d| ?
7 2 | q=3 r=1 | q=3 r=1 | q=3 r=1 | true
-7 2 | q=-3 r=-1 | q=-4 r=1 | q=-4 r=1 | true
7 -2 | q=-3 r=1 | q=-4 r=-1 | q=-3 r=1 | true
-7 -2 | q=3 r=-1 | q=3 r=-1 | q=4 r=1 | true
-7 3 | q=-2 r=-1 | q=-3 r=2 | q=-3 r=2 | true
6 -3 | q=-2 r=0 | q=-2 r=0 | q=-2 r=0 | true
RISC-V rule (from the spec table) quotient remainder
div/rem 7 / 0 -1 (0xFFFFFFFF) 7
div/rem -7 / 0 -1 (0xFFFFFFFF) -7
div/rem -2147483648 / -1 -2147483648 (0x80000000) 0
div/rem -7 / 2 -3 (0xFFFFFFFD) -1
divu/remu 7 / 0 4294967295 (0xFFFFFFFF) 7
প্রথম টেবিলের প্রতিটি সারির শেষে true — তিন নিয়মেই a = d·q + r আর |r| < |d| সত্য, অর্থাৎ তিনটিই “বৈধ” ভাগ; পার্থক্য শুধু গোল করার দিকে। −7 ÷ 3-এ truncated দেয় q=-2 r=-1 আর floored দেয় q=-3 r=2. 6 ÷ −3-এর মতো নিঃশেষে বিভাজ্য হলে তিনটি এক। দ্বিতীয় অংশ RISC-V spec-এর নিয়ম: শূন্য দিয়ে ভাগে div-এর ভাগফল −1 (0xFFFFFFFF, সব বিট ১) আর ভাগশেষ ভাজ্য নিজেই (৭ বা −৭); divu-তেও ভাগফল 0xFFFFFFFF (unsigned-এ 4294967295). INT_MIN ÷ −1-এ ভাগফল INT_MIN নিজেই (0x80000000), ভাগশেষ 0 — আর কোনো ক্ষেত্রেই trap নেই। শেষ সারি দেখায় সাধারণ ক্ষেত্রে RISC-V truncated: −7 ÷ 2 = −3, ভাগশেষ −1.
বাস্তবের বাগ: C বা Java-তে “x বিজোড় কি না” পরীক্ষা x % 2 == 1 লিখলে ঋণাত্মক বিজোড় সংখ্যায় ভুল হয়, কারণ −7 % 2 = −1. ঠিক লেখা x % 2 != 0. একইভাবে hash-এর মান ঋণাত্মক হলে h % size ঋণাত্মক সূচক দিতে পারে — array-র বাইরে লেখা। Python থেকে C-তে কোড অনুবাদের সময় (বা AI দিয়ে অনুবাদ করালে) এই পার্থক্য প্রায়ই হারিয়ে যায়।
৫ · শূন্য দিয়ে ভাগ আর INT_MIN ÷ −1 — কোন CPU কী করে
দুটি ভাগের কোনো “ঠিক” উত্তর n-বিট integer-এ নেই। প্রথমটি শূন্য দিয়ে ভাগ — গণিতেই সংজ্ঞাহীন। দ্বিতীয়টি signed overflow: ৩২-বিটে INT_MIN = −2,147,483,648, আর INT_MIN ÷ −1 = +2,147,483,648, যা ৩২-বিট signed-এর সর্বোচ্চ (2,147,483,647)-এর চেয়ে এক বেশি। (এটি ভাগের একমাত্র overflow — অন্য সব ক্ষেত্রে |ভাগফল| ≤ |ভাজ্য|।) ভিন্ন ISA এদের ভিন্নভাবে সামলায়:
| যেখানে | x ÷ 0 | INT_MIN ÷ −1 |
|---|---|---|
x86 / x86-64 (div, idiv) | #DE (divide error) exception | #DE — ভাগফল register-এ ধরে না বলে একই exception |
RISC-V M extension (div/rem, divu/remu) | trap নেই: ভাগফল সব বিট ১ (signed-এ −1), ভাগশেষ = ভাজ্য | trap নেই: ভাগফল = INT_MIN, ভাগশেষ = 0 |
ARM AArch64 (sdiv, udiv) | trap নেই: ফল 0 | trap নেই: ফল INT_MIN |
| C / C++ ভাষা | Undefined behavior | Undefined behavior (INT_MIN % -1-ও) |
| Java | ArithmeticException | ফল Integer.MIN_VALUE, exception নেই |
| Python | ZeroDivisionError | সমস্যা নেই — Python-এর int-এর আকার সীমাহীন |
RISC-V-এর সারিটি spec-এর (unprivileged ISA manual-এর M extension অধ্যায়) একটি টেবিল থেকে নেওয়া, আর এটি ISA-র অংশ — যেকোনো সঠিক RISC-V core এই মানই দেবে। ARM-এর সারিটি AArch64-এর জন্য; পুরোনো ৩২-বিট ARM-এর অনেক core-এ ভাগের instruction-ই ছিল না, আর কিছু Cortex-R/M core-এ শূন্য দিয়ে ভাগে trap চালু করার একটি নিয়ন্ত্রণ-বিট আছে বলে জানা যায় — নির্দিষ্ট chip-এর manual দেখে নিন। Linux-এ x86-এর #DE ধরা পড়লে kernel প্রোগ্রামকে SIGFPE signal পাঠায়, আর terminal-এ লেখা আসে “Floating point exception” — integer ভাগ হলেও! নামটি ঐতিহাসিক, বিভ্রান্তিকর।
RISC-V কেন trap রাখেনি
RISC-V spec নিজেই এর যুক্তি লিখে রেখেছে (ভাবার্থ): trap রাখলে এটি হতো standard ISA-র একমাত্র গাণিতিক trap — floating-point-এর ভুলও trap করে না, শুধু flag বসিয়ে একটি নির্ধারিত মান লেখে। ভাষার মানদণ্ড যদি শূন্য দিয়ে ভাগে exception চায় (যেমন Java), compiler ভাগের পাশে একটি শাখা (beqz) বসিয়ে দিলেই হয়; সেই শাখা প্রায় কখনো নেওয়া হয় না, তাই branch predictor সহজে ঠিক অনুমান করে আর খরচ খুব কম। আর “সব বিট ১” মানটি বেছে নেওয়া হয়েছে কারণ ভাজক ০ হলে অংশ ২-এর অ্যালগরিদম প্রতিটি বিয়োগে সফল হয় (A − 0 ≥ 0), ফলে প্রতিটি ভাগফল-বিট আপনা থেকেই ১ আর A-তে ভাজ্য রয়ে যায় — divider-এর circuit-এ বাড়তি কিছু লাগে না। নিজে যাচাই করে দেখুন: অ্যালগরিদমে M = 0 বসান।
Exception কীভাবে pipeline-এ সামলানো হয় — অর্থাৎ x86-এর #DE ঠিক কোন instruction-এ থামে, আগের-পরের instruction-এর কী হয় — তা পর্ব ৩৮-এর বিষয় (exception আর interrupt). C-তে undefined behavior কেন বিপজ্জনক, compiler কীভাবে “এটা ঘটবে না” ধরে নিয়ে কোড বদলে দেয়, তা দেখুন C প্রোগ্রামিং কোর্সের undefined behavior পর্বে। নিরাপদ C কোডে ভাগের আগে d == 0 আর a == INT_MIN && d == -1 দুটিই পরীক্ষা করুন।
৬ · দ্রুত ভাগ — SRT, Newton–Raphson আর Goldschmidt
Restoring বা non-restoring প্রতি ধাপে ভাগফলের ১টি বিট দেয়। ৬৪-বিট ভাগে তাহলে ৬৪টি ধাপ, প্রতিটিতে একটি পূর্ণ ৬৪-বিট যোগ-বিয়োগ, যার carry পুরো প্রস্থ পেরোয়। দ্রুত করার দুটি বড় পথ আছে: প্রতি ধাপে বেশি বিট বের করা (digit recurrence, যেমন SRT), অথবা ভাগকে গুণে রূপান্তর করা (Newton–Raphson, Goldschmidt).
SRT division — redundant অঙ্ক আর lookup table
SRT division-এর নাম তিন গবেষকের নামের আদ্যক্ষর থেকে — Sweeney, Robertson আর Tocher, যারা ১৯৫০-এর দশকের শেষ দিকে প্রায় একই সময়ে আলাদাভাবে এই ধারণায় পৌঁছান বলে জানা যায়। মূল কৌশল দুটি:
- Redundant ভাগফল-অঙ্ক: radix-2 SRT-এ প্রতিটি অঙ্ক {−1, 0, 1}, radix-4-এ {−2, −1, 0, 1, 2}. একই সংখ্যা একাধিক উপায়ে লেখা যায় — radix-4-এ দুই অঙ্কের (1, −2) আর (0, 2) দুটিই 2 বোঝায়, কারণ 1 × 4 − 2 = 0 × 4 + 2. তাই একটি ধাপে অঙ্ক একটু বড় বা ছোট বাছলেও পরের ধাপের অঙ্ক তা পুষিয়ে দিতে পারে। ফলে প্রতি ধাপে পুরো প্রস্থের তুলনা লাগে না — আংশিক ভাগশেষের প্রথম কয়েকটি বিট আর ভাজকের প্রথম কয়েকটি বিট দেখে আন্দাজ করলেই চলে। Non-restoring আসলে radix-2-এর একটি বিশেষ রূপ, যেখানে অঙ্ক {−1, +1}.
- Lookup table আর carry-save: অঙ্ক বাছাইয়ের সেই আন্দাজ আসে একটি ছোট lookup table (PLA বা ROM) থেকে। আংশিক ভাগশেষ রাখা হয় carry-save রূপে, তাই প্রতি ধাপে carry ছড়ানোর অপেক্ষাও নেই। Radix-4 প্রতি ধাপে ২টি বিট, radix-16 ৪টি বিট দেয়।
দামটি হলো জটিলতা, আর সেই জটিলতার বিখ্যাত শিকার Intel-এর প্রথম Pentium — ঘটনাটি Pentium FDIV bug নামে পরিচিত। ১৯৯৪ সালে গণিতবিদ Thomas Nicely লক্ষ করেন, কিছু বিরল ভাজক-ভাজ্যের জোড়ায় Pentium-এর floating-point ভাগ ভুল উত্তর দেয়। পরে জানা যায়, radix-4 SRT divider-এর lookup table-এর কয়েকটি ঘর (সাধারণত বলা হয় ৫টি) ভুলবশত ফাঁকা ছিল। Intel শেষ পর্যন্ত chip বদলে দেওয়ার প্রস্তাব দেয়, আর এর জন্য প্রায় ৪৭৫ মিলিয়ন ডলারের একটি খরচ (charge) দেখায় বলে প্রতিবেদনে আসে। টেবিলের ভুল ঠিক কীভাবে ভুল ফল দিত, তা নিচের ভাবনার প্রশ্ন ২-এ; floating-point ভাগের বাকি ধাপ (exponent বিয়োগ, normalise আর rounding) পর্ব ২৪-এ (Floating point-এর হিসাব)।
Newton–Raphson — গুণ দিয়ে 1/d
দ্বিতীয় পথ ভিন্ন: a ÷ d = a × (1/d). আর 1/d-কে শুধু গুণ আর বিয়োগ দিয়ে বের করা যায় Newton–Raphson পদ্ধতিতে। ফাংশন f(x) = 1/x − d-এর শূন্য হলো x = 1/d. Newton-এর সূত্র xk+1 = xk − f(xk)/f′(xk), আর f′(x) = −1/x2. বসালে:
সুন্দর ব্যাপার হলো, এতে কোনো ভাগ নেই — দুটি গুণ আর একটি বিয়োগ। কত দ্রুত মেলে? ভুলকে ek = 1 − d·xk ধরলে:
অর্থাৎ প্রতিটি ধাপে ভুল বর্গ হয় — ভুল 10−2 থেকে 10−4, তারপর 10−8. সঠিক অঙ্কের সংখ্যা প্রতি ধাপে দ্বিগুণ — একে বলে quadratic convergence. শর্ত একটি: শুরুর ভুল |e0| < 1 হতে হবে। নিচের stepper-এ 1/3 দেখুন, শুরুর অনুমান x0 = 0.3:
d = 3, target 1/3 = 0.3333333333333333 x0 = 0.3 e0 = 1 - 3 x 0.3 = 0.1 (1 correct digit)
d*x0 = 0.9, 2 - 0.9 = 1.1 x1 = 0.3 x 1.1 = 0.33 e1 = 1 - 0.99 = 0.01 = e0^2 (2 digits)
d*x1 = 0.99, 2 - 0.99 = 1.01 x2 = 0.33 x 1.01 = 0.3333 e2 = 1 - 0.9999 = 0.0001 = e1^2 (4 digits)
x3 = 0.33333333 e3 = 1e-8 = e2^2 (8 digits)
x4 = 0.3333333333333333
e4 = 2.22e-16 (about 1e-16 = e3^2; double has ~16 digits)
4 iterations: 1 -> 2 -> 4 -> 8 -> 16 digits
- শুরুর অনুমান। x0 = 0.3, যা 1/3 থেকে বেশি দূরে নয়। ভুল e0 = 1 − 3 × 0.3 = 0.1 — মোটামুটি এক অঙ্ক ঠিক। বাস্তব hardware-এ এই প্রথম অনুমান আসে একটি ছোট lookup table থেকে, ভাজকের প্রথম কয়েকটি বিট দেখে।
- প্রথম iteration. d·x0 = 0.9, 2 − 0.9 = 1.1, x1 = 0.3 × 1.1 = 0.33. নতুন ভুল 1 − 0.99 = 0.01 — ঠিক 0.1-এর বর্গ। দুই অঙ্ক ঠিক হলো।
- দ্বিতীয় iteration. d·x1 = 0.99, 2 − 0.99 = 1.01, x2 = 0.33 × 1.01 = 0.3333. ভুল 0.0001 = (0.01)2. চার অঙ্ক।
- তৃতীয় iteration. x3 = 0.3333 × 1.0001 = 0.33333333 — আট অঙ্ক, ভুল 10−8. দেখুন, প্রতিবার ঠিক অঙ্কের সংখ্যা দ্বিগুণ হচ্ছে।
- চতুর্থ iteration — শেষ। x4 = 0.3333333333333333, ভুল প্রায় 2 × 10−16, যা double-এর নিজস্ব নির্ভুলতার সীমা (প্রায় ১৬ দশমিক অঙ্ক)। মাত্র ৪টি iteration, প্রতিটিতে ২টি গুণ — অথচ বিট-বাই-বিট পদ্ধতিতে ৫৩-বিট mantissa-র জন্য ৫৩টি ধাপ লাগত।
Goldschmidt division একই গণিতের আরেক রূপ। a/d-এর ওপরে-নিচে একই গুণক F = 2 − D দিয়ে বারবার গুণ করা হয়: N ← N·F, D ← D·F. D = 1 − ε হলে নতুন D = (1 − ε)(1 + ε) = 1 − ε2 — এখানেও ভুল বর্গ হয়। D যখন ১-এ পৌঁছায়, N হয়ে যায় a/d. সুবিধা: প্রতি ধাপের দুটি গুণ একে অপরের ওপর নির্ভর করে না, তাই pipelined multiplier-এ পাশাপাশি চলে। অসুবিধা: Newton–Raphson “নিজেকে শুধরে নেয়” (প্রতি ধাপে আসল d ব্যবহার হয়, তাই আগের ধাপের rounding ভুল মুছে যায়), Goldschmidt-এ rounding ভুল জমতে থাকে, তাই শেষে বাড়তি সংশোধন লাগে। IBM System/360 Model 91 (১৯৬০-এর দশক) এই পদ্ধতির প্রথম দিকের ব্যবহারকারী হিসেবে উল্লেখিত, আর পরেও কিছু AMD প্রসেসরের floating-point ভাগে এ ধরনের গুণ-ভিত্তিক পদ্ধতি ব্যবহারের কথা প্রকাশিত হয়েছে।
নিচের প্রোগ্রাম দুটি পদ্ধতিই চালায়: 1/3 (x0 = 0.3) আর 1/0.75 — দ্বিতীয়টিতে ভাজককে floating-point mantissa-র মতো [0.5, 1) সীমায় রেখে বহুল ব্যবহৃত সরলরেখা-অনুমান x0 = 48/17 − (32/17)·d দিয়ে শুরু:
// Newton-Raphson for 1/d: x(k+1) = x(k) * (2 - d*x(k)). The error e = 1 - d*x squares each step.
function newton(d, x0, steps) {
console.log(`1/${d} = ${1 / d} (start x0 = ${x0.toFixed(6)})`);
console.log("k x(k) e = 1 - d*x(k) correct digits");
let x = x0;
for (let k = 0; k <= steps; k++) {
const e = 1 - d * x;
const digits = e === 0 ? "all (double)" : (-Math.log10(Math.abs(e))).toFixed(1);
console.log(`${k} ${x.toPrecision(16).padEnd(21)}${e.toExponential(2).padEnd(17)}${digits}`);
x = x * (2 - d * x); // one multiply-subtract-multiply step
}
console.log("");
}
newton(3, 0.3, 4);
const d = 0.75; // divisor scaled into [0.5, 1), like a mantissa
newton(d, 48 / 17 - (32 / 17) * d, 4); // classic straight-line first guess
// Goldschmidt: multiply N and D by F = 2 - D; D -> 1 and N -> N/D.
let N = 1, D = d;
console.log("Goldschmidt for 1/0.75\nk F N D");
for (let k = 1; k <= 4; k++) {
const F = 2 - D;
N *= F; D *= F; // the two multiplies are independent
console.log(`${k} ${String(F).padEnd(20)}${N.toPrecision(16).padEnd(21)}${D.toPrecision(16)}`);
}আউটপুট:
1/3 = 0.3333333333333333 (start x0 = 0.300000)
k x(k) e = 1 - d*x(k) correct digits
0 0.3000000000000000 1.00e-1 1.0
1 0.3300000000000000 1.00e-2 2.0
2 0.3333000000000000 1.00e-4 4.0
3 0.3333333300000000 1.00e-8 8.0
4 0.3333333333333333 2.22e-16 15.7
1/0.75 = 1.3333333333333333 (start x0 = 1.411765)
k x(k) e = 1 - d*x(k) correct digits
0 1.411764705882353 -5.88e-2 1.2
1 1.328719723183391 3.46e-3 2.5
2 1.333317369284372 1.20e-5 4.9
3 1.333333333142195 1.43e-10 9.8
4 1.333333333333333 0.00e+0 all (double)
Goldschmidt for 1/0.75
k F N D
1 1.25 1.250000000000000 0.9375000000000000
2 1.0625 1.328125000000000 0.9960937500000000
3 1.00390625 1.333312988281250 0.9999847412109375
4 1.0000152587890625 1.333333333022892 0.9999999997671694
1/3-এর টেবিলে ভুলের কলাম 1.00e-1, 1.00e-2, 1.00e-4, 1.00e-8, তারপর 2.22e-16 — প্রতি ধাপে বর্গ, আর “correct digits” 1 → 2 → 4 → 8 → 15.7 (double-এর সীমা)। 1/0.75-এ শুরুর ভুল −5.88e-2 (অনুমান একটু বেশি বড়), তারপর 3.46e-3, 1.20e-5, 1.43e-10 — অঙ্ক 1.2 → 2.5 → 4.9 → 9.8, প্রতিবার দ্বিগুণ; চতুর্থ ধাপে double-এ ভুল একেবারে 0. লক্ষ করুন, শুরুর ভুল ঋণাত্মক হলেও প্রথম ধাপের পরেই তা ধনাত্মক হয়ে যায়, কারণ বর্গ সবসময় ধনাত্মক। Goldschmidt-এ D চলেছে 0.9375 → 0.99609375 → 0.9999847412109375 → 0.9999999997671694, অর্থাৎ ১ থেকে দূরত্ব 2−4, 2−8, 2−16, 2−32 — আর N সেই হারে 1.333…-এর দিকে।
ভাগ কেন এখনো ধীর — latency আর pipelining
এত কৌশলের পরও আধুনিক CPU-তে ভাগ গুণের চেয়ে অনেক ধীর। সাধারণ চিত্র (মাইক্রো-আর্কিটেকচার আর operand-এর আকারভেদে অনেক বদলায় — Agner Fog-এর instruction টেবিলের মতো উৎস থেকে নির্দিষ্ট CPU-র সংখ্যা দেখে নিন):
| কাজ (আধুনিক x86-64, মোটামুটি) | Latency | নতুন কাজ শুরু করা যায় |
|---|---|---|
| ৬৪-বিট integer যোগ | ১ cycle | প্রতি cycle-এ কয়েকটি |
| ৬৪-বিট integer গুণ | প্রায় ৩ cycle | প্রতি cycle-এ একটি (fully pipelined) |
| ৩২/৬৪-বিট integer ভাগ | সাধারণত ১০ থেকে কয়েক দশ cycle; কিছু পুরোনো core-এ ৬৪-বিট ভাগে আরও অনেক বেশি | কয়েক cycle পরপর একটি — পুরোপুরি pipelined নয় |
ভাগ পুরোপুরি pipelined নয় কেন? গুণে প্রতিটি আংশিক গুণফল স্বাধীন, তাই Wallace tree-র মতো একটি বড় combinational গাছ বানিয়ে তাকে stage-এ কেটে ফেলা যায় — প্রতি cycle-এ নতুন গুণ ঢোকে। ভাগে প্রতিটি অঙ্ক আগের ভাগশেষের ওপর নির্ভর করে, তাই divider সাধারণত একটি ছোট circuit যা নিজেকেই বারবার ব্যবহার করে (iterative). একটি ভাগ চলার সময় পরেরটি অপেক্ষা করে। অনেক আধুনিক divider আরও একটি কৌশল নেয়: ভাজ্যের শুরুর শূন্যগুলো গুনে আগেভাগে থেমে যাওয়া (early termination), তাই ছোট সংখ্যার ভাগ দ্রুত শেষ হয় — latency তখন ডেটার ওপর নির্ভর করে।
আরও গভীরে: IEEE 754 floating-point ভাগের ফল “সঠিকভাবে rounded” হতে হয়, তাই Newton–Raphson-এর শেষ ধাপে ভাগশেষ a − d·q নির্ভুলভাবে হিসাব করে শেষ বিটটি ঠিক করতে হয় — fused multiply-add (FMA) এখানে অপরিহার্য, কারণ এটি মাঝখানে rounding ছাড়াই a − d·q দেয়। Intel-এর Itanium-এ floating-point ভাগের কোনো hardware instruction-ই ছিল না: একটি reciprocal অনুমান instruction (frcpa) আর FMA-ভিত্তিক কয়েকটি Newton ধাপ দিয়ে software-এ ভাগ হতো। অন্যদিকে বহু সাম্প্রতিক x86 core integer ভাগের জন্য radix-4 বা radix-16 ধরনের digit-recurrence divider ব্যবহার করে বলে প্রকাশিত বিশ্লেষণে জানা যায়। ভাগ আর বর্গমূল সাধারণত একই hardware ভাগ করে নেয়, কারণ দুটির recurrence প্রায় একই রকম।
৭ · Compiler ভাগ এড়িয়ে চলে — magic number, AND আর hash table
ভাগ এত দামি বলে compiler সুযোগ পেলেই একে সস্তা কাজে বদলে দেয় — একে বলে strength reduction. সবচেয়ে সাধারণ ক্ষেত্র তিনটি।
১. ২-এর ঘাত দিয়ে ভাগ: shift
Unsigned x / 2k = x >> k (logical shift). Signed-এ সাবধান: arithmetic shift নিচের দিকে (floor) গোল করে, অথচ C-র ভাগ শূন্যের দিকে কাটে। −7 >> 1 = −4, কিন্তু −7 / 2 = −3. তাই compiler ঋণাত্মক x-এ আগে 2k − 1 যোগ করে (bias), তারপর shift করে। এর বিস্তারিত আর প্রমাণ পর্ব ২০-এ (Integer যোগ-বিয়োগ)।
২. ধ্রুবক দিয়ে ভাগ: magic number দিয়ে গুণ
ভাজক যদি compile-এর সময়েই জানা থাকে (যেমন x / 3, x / 10), তাহলে ভাগকে একটি গুণ আর একটি shift-এ বদলানো যায়; সেই বিশেষ গুণকটিকে বলে magic number. এর ব্যাপক ব্যবহারের ভিত্তি Torbjörn Granlund আর Peter Montgomery-র ১৯৯৪ সালের গবেষণাপত্র “Division by Invariant Integers using Multiplication”. ধারণাটি দশমিকের মতোই সহজ: x / 3 = x × (1/3), আর 1/3-কে একটি binary ভগ্নাংশ M / 2s দিয়ে যথেষ্ট কাছাকাছি লিখলেই হয়। ৩২-বিট unsigned-এর জন্য:
কেন সব x-এ ঠিক (প্রমাণ): M = (233 + 1)/3 বসালে x·M / 233 = x/3 + x/(3·233). শেষ পদটি — ভুলের পরিমাণ — x < 232 হওয়ায় 232/(3·233) = 1/6-এর চেয়ে ছোট। এখন x/3 = q + r/3, যেখানে r ∈ {0, 1, 2}, তাই ভগ্নাংশ অংশ সর্বোচ্চ 2/3. মোট ভগ্নাংশ অংশ < 2/3 + 1/6 = 5/6 < 1, তাই floor নিলে ঠিক q আসে — কখনো পরের পূর্ণসংখ্যায় পৌঁছায় না। একই হিসাবে x / 10 = (x × 0xCCCCCCCD) >> 35: 0xCCCCCCCD × 10 = 235 + 2, ভুল < 232/(5·235) = 1/40, আর 9/10 + 1/40 < 1. লক্ষ করুন, গুণফল ৬৪ বিটের — x আর M দুটিই ৩২-বিট, তাই গুণফল 264-এর নিচে, উপচে পড়ে না।
সাম্প্রতিক gcc -O2 x86-64-এর জন্য unsigned f(unsigned x) { return x / 3; }-কে মোটামুটি এভাবে লেখে (সংস্করণভেদে সামান্য আলাদা হতে পারে):
# x86-64, Intel syntax (System V: x in edi, result in eax)
div3:
mov eax, edi
mov edx, 2863311531 # 0xAAAAAAAB
imul rax, rdx # 64-bit product
shr rax, 33
ret
# RV32IM (x in a0): mulhu gives the upper 32 bits = product >> 32
div3:
li a5, -1431655765 # 0xAAAAAAAB as a signed immediate
mulhu a0, a0, a5
srli a0, a0, 1 # total shift 32 + 1 = 33
retকোনো div instruction নেই! নিচের C প্রোগ্রাম এই দাবিটি যাচাই করে: সব ছোট সংখ্যা, একটি নির্ধারিত নিয়মে ছড়ানো ৪০ লাখের বেশি সংখ্যা আর সীমার কাছের মানগুলোতে magic-গুণ বনাম আসল /. সাথে একটি ভুল “magic” (0x55555555 >> 32, যা দেখতে 1/3-এর মতোই) আর ২-এর ঘাত দিয়ে ভাগশেষের AND-কৌশল:
#include <stdio.h>
#include <stdint.h>
/* Division by a constant without a divide instruction (unsigned 32-bit) */
static uint32_t div3(uint32_t x) { return (uint32_t)(((uint64_t)x * 0xAAAAAAABu) >> 33); }
static uint32_t div10(uint32_t x) { return (uint32_t)(((uint64_t)x * 0xCCCCCCCDu) >> 35); }
static uint32_t naive3(uint32_t x) { return (uint32_t)(((uint64_t)x * 0x55555555u) >> 32); }
static unsigned long tested, bad3, bad10, badnaive;
static void check(uint32_t x) {
tested++;
if (div3(x) != x / 3u) bad3++;
if (div10(x) != x / 10u) bad10++;
if (naive3(x) != x / 3u) badnaive++;
}
int main(void) {
const uint32_t edge[] = {0u, 1u, 2u, 3u, 9u, 10u, 11u, 0x7FFFFFFFu,
0x80000000u, 0xFFFFFFFDu, 0xFFFFFFFEu, 0xFFFFFFFFu};
for (unsigned i = 0; i < sizeof edge / sizeof edge[0]; i++) check(edge[i]);
for (uint32_t i = 0; i < (1u << 22); i++) {
check(i); /* all small numbers 0 .. 4194303 */
check(i * 2654435761u); /* odd multiplier: spread over 32 bits */
check(0xFFFFFFFFu - i); /* the top end */
}
printf("tested %lu values\n", tested);
printf("x*0xAAAAAAAB >> 33 vs x/3 : %lu mismatches\n", bad3);
printf("x*0xCCCCCCCD >> 35 vs x/10: %lu mismatches\n", bad10);
printf("x*0x55555555 >> 32 vs x/3 : %lu mismatches (e.g. 3 -> %u)\n",
badnaive, (unsigned)naive3(3u));
printf("1000003 %% 8 = %u, 1000003 & 7 = %u (unsigned)\n", 1000003u % 8u, 1000003u & 7u);
printf("-7 %% 8 = %d, -7 & 7 = %d (signed int: not the same!)\n", -7 % 8, -7 & 7);
return 0;
}আউটপুট:
tested 12582924 values
x*0xAAAAAAAB >> 33 vs x/3 : 0 mismatches
x*0xCCCCCCCD >> 35 vs x/10: 0 mismatches
x*0x55555555 >> 32 vs x/3 : 4194302 mismatches (e.g. 3 -> 0)
1000003 % 8 = 3, 1000003 & 7 = 3 (unsigned)
-7 % 8 = -7, -7 & 7 = 1 (signed int: not the same!)
সব মিলিয়ে ১,২৫,৮২,৯২৪টি মান পরীক্ষা হয়েছে — ১২টি সীমার মান আর তিনটি লুপের প্রতিটিতে 222টি করে। 0xAAAAAAAB >> 33 আর 0xCCCCCCCD >> 35 দুটিতেই অমিল শূন্য, ঠিক যেমন প্রমাণ বলেছিল। (এই পর্ব লেখার সময় আলাদা একটি script দিয়ে পুরো 232টি মানেও যাচাই করা হয়েছে — কোনো অমিল নেই।) দেখতে প্রায় একই “ভুল magic” 0x55555555 >> 32 কিন্তু ৪১,৯৪,৩০২টি মানে ভুল করে, আর প্রথম ভুলটিই x = 3, যেখানে ফল আসে 0. কারণ 0x55555555 / 232 সংখ্যাটি 1/3-এর চেয়ে সামান্য ছোট, তাই ৩-এর গুণিতকে floor এক কমে যায় — magic সংখ্যা সবসময় ওপরের দিকে গোল করতে হয়। শেষ দুই লাইন: unsigned-এ 1000003 % 8 আর 1000003 & 7 দুটিই 3, কিন্তু signed int-এ -7 % 8 = −7 অথচ -7 & 7 = 1 — AND-কৌশল শুধু অঋণাত্মক সংখ্যায় নিরাপদ।
৩. ২-এর ঘাত দিয়ে ভাগশেষ: AND
Unsigned x % 2k = x & (2k − 1) — শুধু নিচের k বিট রাখা, একটি bitmask দিয়ে। x % 8 = x & 7, এক cycle-এর কাজ। Signed int-এ এটি মেলে না (−7 % 8 = −7, কিন্তু −7 & 7 = 1), তাই signed %-এ compiler বাড়তি কয়েকটি instruction দিয়ে চিহ্ন সামলায়। পারফরম্যান্সের দিক থেকে ব্যবহারিক কয়েকটি পরামর্শ:
- যে সংখ্যা কখনো ঋণাত্মক হয় না, তা
unsignedরাখলে/আর%সস্তা হয় (চিহ্ন-সংশোধন লাগে না)। - লুপের ভেতরে একই চলক ভাজক দিয়ে বারবার ভাগ হলে (যেমন
i % n), সম্ভব হলে বাইরে একবার হিসাব করুন, বা গোনার কৌশল নিন (if (++j == n) j = 0;). - Runtime-এ জানা কিন্তু বারবার ব্যবহৃত ভাজকের জন্যও magic number একবার হিসাব করে রাখা যায় — libdivide-এর মতো library এই কাজ করে।
- অনুমানে নয়, মেপে সিদ্ধান্ত নিন: compiler ধ্রুবক ভাগ নিজেই বদলায়, হাতে “optimize” করে কোড জটিল করার দরকার নেই।
Hash table-এ এই কৌশলের সবচেয়ে চেনা ব্যবহার। Table-এর আকার ২-এর ঘাত (যেমন 1024) রাখলে bucket-এর সূচক h & (size − 1) — ভাগের বদলে একটি AND. Java-র HashMap এই কারণে আকার ২-এর ঘাত রাখে, আর hash-এর ওপরের বিটগুলো নিচে মিশিয়ে নেয় (কারণ AND শুধু নিচের বিট দেখে, আর দুর্বল hash-এ নিচের বিটগুলো একঘেয়ে হতে পারে)। মৌলিক সংখ্যার (prime) আকারের table-এ বণ্টন ভালো হয় কিন্তু % লাগে। Hash table-এর নকশা দেখুন DSA কোর্সের hash table পর্বে।
৮ · AI-এর যুগে ভাগের হিসাব
AI টুল restoring division-এর ব্যাখ্যা গুছিয়ে লিখতে পারে, কিন্তু টেবিলের সারি, চিহ্নের নিয়ম আর ISA-র বিশেষ ক্ষেত্রে প্রায়ই আত্মবিশ্বাসের সাথে ভুল করে। প্রচলিত কয়েকটি ভুল:
| AI-এর প্রচলিত ভুল | ঠিক কথা | কীভাবে যাচাই |
|---|---|---|
| “RISC-V শূন্য দিয়ে ভাগে exception তোলে” | তোলে না; ভাগফল সব বিট ১, ভাগশেষ = ভাজ্য | RISC-V unprivileged spec-এর M extension-এর টেবিল |
“C-তে %-এর ফল সবসময় ধনাত্মক” | C99 থেকে ভাগশেষের চিহ্ন ভাজ্যের মতো: −7 % 2 = −1 | ছোট একটি C প্রোগ্রাম চালিয়ে |
| Non-restoring-এ শেষের সংশোধন বাদ | শেষ A ঋণাত্মক হলে A + M | 13 ÷ 3 হাতে বা simulator-এ |
| Restoring টেবিলে A-কে n বিটে রাখা, বা restore-এর সংখ্যা ভুল গোনা | A-তে n + 1 বিট; restore-এর সংখ্যা = ভাগফলের ০-বিটের সংখ্যা | প্রতিটি সারিতে invariant 0 ≤ A < M দেখে |
| ভুল magic number বা shift (যেমন 0x55555555 >> 32 দিয়ে x/3) | 0xAAAAAAAB >> 33; ভুলের সীমা প্রমাণ করতে হয় | লাখো মানে আসল /-এর সাথে মিলিয়ে |
| Latency-র নির্দিষ্ট সংখ্যা বানিয়ে বলা (“ভাগে ঠিক ২৬ cycle”) | CPU, operand-এর আকার, এমনকি ডেটাভেদে বদলায় | নির্দিষ্ট CPU-র optimization manual বা মাপা টেবিল |
AI-কে ভাগ নিয়ে প্রশ্ন করলে প্রস্থ, অ্যালগরিদম আর নিয়ম স্পষ্ট বলে দিন, আর প্রতিটি সারি চেয়ে নিন:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Restoring আর non-restoring-এর পার্থক্য? — Restoring ঋণাত্মক হলে M যোগ করে আগের মান ফেরায় (সর্বোচ্চ 2n যোগ-বিয়োগ); non-restoring পরের ধাপে যোগ করে, কারণ 2(A + M) − M = 2A + M (n টি, আর হয়তো ১টি শেষ-সংশোধন)।
- 7 ÷ 2-এর restoring টেবিল ৪ বিটে? — ভাগফল 0011, ভাগশেষ 0001; প্রথম দুই iteration-এ restore, শেষ দুটিতে q0 = 1.
- C-তে −7 / 2 আর −7 % 2? — −3 আর −1 (শূন্যের দিকে কাটা); Python-এ −4 আর 1.
- RISC-V-তে শূন্য দিয়ে ভাগ করলে? — trap নেই; ভাগফল সব বিট ১, ভাগশেষ ভাজ্য। x86-এ #DE exception.
- Compiler x / 3 কীভাবে করে? — Magic সংখ্যা দিয়ে গুণ আর shift: (x × 0xAAAAAAAB) >> 33 (৩২-বিট unsigned).
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১গুণ এক cycle-এ একটি করে pipelined হয়, ভাগ কেন হয় না? কিছু CPU-তে ভাগের instruction-ই কেন ছিল না?
মূল কারণ নির্ভরতার গঠন। n-বিট গুণে n টি আংশিক গুণফল — প্রতিটি শুধু একটি AND-এর সারি, একে অপরের ওপর নির্ভর করে না। এদের একসাথে বানিয়ে carry-save adder-এর গাছে (Wallace বা Dadda tree, পর্ব ২১) log স্তরে যোগ করা যায়, তারপর গাছটিকে register দিয়ে কয়েক stage-এ কেটে ফেললেই প্রতি cycle-এ একটি নতুন গুণ ঢোকানো যায়। ভাগে ভাগফলের প্রতিটি অঙ্ক জানতে আগের ধাপের ভাগশেষ লাগে, আর পরের ভাগশেষ জানতে এই অঙ্ক লাগে — একটি শিকল। শিকলকে গাছ বানানো যায় না। SRT প্রতি ধাপে বেশি বিট দেয় আর carry-save রূপে ধাপটি ছোট করে, কিন্তু শিকল থেকেই যায়।
তাহলে কেউ চাইলে ৬৪টি ধাপকে ৬৪টি আলাদা hardware stage বানিয়ে pipelined divider বানাতে পারেন — কিন্তু তাতে বিশাল জায়গা আর শক্তি লাগে, অথচ সাধারণ প্রোগ্রামে ভাগ গুণের চেয়ে অনেক কম চলে। “Common case fast” নীতিতে নকশাকারেরা তাই একটি ছোট iterative divider রাখেন, আর জায়গাটা অন্য কাজে খরচ করেন।
এই হিসাব কোনো কোনো নকশাকারকে আরও এগিয়ে নিয়ে গেছে। Seymour Cray-র Cray-1 (১৯৭৬)-এ floating-point ভাগের instruction ছিল না বলে জানা যায়; ছিল একটি reciprocal অনুমানের unit, আর ভাগ হতো সেই অনুমানকে Newton ধাপে শুধরে গুণ করে — গতির জন্য সামান্য নির্ভুলতা ছাড়ার বিখ্যাত উদাহরণ। ৩২-বিট ARM-এর বহু প্রজন্মে integer ভাগের instruction ছিল না; compiler একটি library ফাংশন ডাকত, যা shift-বিয়োগের লুপ চালাত। পরে ARMv7-এর কিছু profile আর ARMv8-এ sdiv/udiv এসেছে। RISC-V-তেও গুণ-ভাগ মূল RV32I-এর অংশ নয়, আলাদা “M” extension — ছোট microcontroller ভাগের hardware বাদ দিয়ে জায়গা বাঁচাতে পারে।
Software-এর দিকে এর প্রভাব স্পষ্ট: পারফরম্যান্স-সচেতন কোডে ভাগ লুপের বাইরে সরানো হয়, ধ্রুবক ভাগ গুণে বদলায়, আর hash table-এর আকার ২-এর ঘাত রাখা হয়।
মূল উপলব্ধি: কোনো কাজ pipelined হবে কি না, তা ঠিক করে তার ভেতরের নির্ভরতার গঠন — স্বাধীন অংশ থাকলে গাছ, শিকল থাকলে লুপ। আর hardware-এ জায়গা দেওয়া হয় ব্যবহারের হার দেখে।
প্র ০২SRT-এর lookup table কয়েকটি ঘর ভুল হলে কী হয় — Pentium FDIV ঘটনা hardware যাচাইয়ের ধারণা কীভাবে বদলে দিল?
SRT-এর শক্তি তার redundancy-তে: ভাগফল-অঙ্ক বাছাইয়ে একটু ভুল আন্দাজ পরের ধাপে শুধরে নেওয়া যায়, তাই আংশিক ভাগশেষের মাত্র কয়েকটি বিট দেখে সিদ্ধান্ত নেওয়া চলে। কিন্তু এই স্বাধীনতার একটি সীমা আছে। ভাগশেষ ও ভাজকের কিছু সমন্বয়ে শুধু নির্দিষ্ট অঙ্কটিই (যেমন +2) বৈধ; সেখানে টেবিল অন্য কিছু দিলে আংশিক ভাগশেষ অনুমোদিত সীমার বাইরে চলে যায়, আর পরের কোনো ধাপ তা আর শোধরাতে পারে না।
Intel-এর প্রথম Pentium-এর floating-point unit-এ radix-4 SRT divider ছিল, যার অঙ্ক-বাছাইয়ের টেবিলে হাজারখানেক ঘর ছিল বলে প্রকাশিত বিশ্লেষণে বলা হয়। টেবিলটি বানানোর সময় কয়েকটি ঘর (প্রচলিত হিসাবে ৫টি) ফাঁকা থেকে যায় — সেখানে +2 থাকার কথা ছিল, পড়া হতো 0. এই ঘরগুলোতে পৌঁছানোর সম্ভাবনা খুব কম, তাই সাধারণ পরীক্ষায় ধরা পড়েনি। ১৯৯৪ সালে Lynchburg College-এর গণিতবিদ Thomas Nicely মৌলিক সংখ্যা নিয়ে হিসাবের সময় অসঙ্গতি পান এবং বিষয়টি প্রকাশ করেন। বিখ্যাত উদাহরণ 4195835 ÷ 3145727, যেখানে ত্রুটিপূর্ণ chip-এর উত্তর দশমিকের চতুর্থ ঘরেই ভুল হতো বলে উল্লেখ করা হয়।
Intel শুরুতে বলে, সাধারণ ব্যবহারকারীর জন্য এই ভুল প্রায় অপ্রাসঙ্গিক, আর শুধু যারা প্রমাণ দেখাতে পারবেন তাদের chip বদলে দেবে। জনমতের চাপ আর IBM-এর মতো বড় ক্রেতার আপত্তির পর Intel সবার জন্য বদলে দেওয়ার নীতি নেয়, আর প্রায় ৪৭৫ মিলিয়ন ডলারের খরচ দেখায় বলে প্রতিবেদনে আসে। ঘটনাটি chip শিল্পে একটি মোড় হিসেবে ধরা হয়: এর পর Intel-সহ বহু কোম্পানি floating-point আর গাণিতিক unit-এর formal verification (গাণিতিকভাবে প্রমাণ করা যে circuit সব ইনপুটে ঠিক) -এ অনেক বেশি বিনিয়োগ করে বলে ব্যাপকভাবে বলা হয়। কয়েকটি উদাহরণে চালানো test যথেষ্ট নয় — ৬৪-বিট ভাগের সব জোড়া কখনো চালিয়ে দেখা সম্ভব নয়।
Floating-point ভাগের বাকি ধাপগুলো (exponent বিয়োগ, normalise আর rounding) পর্ব ২৪-এ।
মূল উপলব্ধি: Redundant নকশা গতি দেয়, কিন্তু তার নিরাপত্তার সীমা নিখুঁতভাবে মানতে হয়। Exhaustive test অসম্ভব হলে প্রমাণই একমাত্র নিশ্চয়তা।
প্র ০৩শূন্য দিয়ে ভাগে x86 exception তোলে, RISC-V আর ARM তোলে না। কোন নকশাটি বেশি নিরাপদ, আর ভাষাগুলো কী করে?
x86-এর div/idiv শূন্য দিয়ে ভাগে আর ভাগফল ধরে না এমন ক্ষেত্রে (যেমন INT_MIN ÷ −1) #DE exception তোলে — এই আচরণ 8086-এর যুগ থেকে চলে আসছে। সুবিধা: ভুল কখনো নীরবে চলে যায় না, প্রোগ্রাম সঙ্গে সঙ্গে থামে (Linux-এ SIGFPE). অসুবিধা: exception মানে hardware-কে pipeline-এ নির্দিষ্ট instruction-এ থামার ব্যবস্থা রাখতে হয় (পর্ব ৩৮), আর যে ভাষা অন্য আচরণ চায় তাকে আগেভাগে পরীক্ষা করতে হয়।
RISC-V-এর নকশাকারেরা উল্টো পথ নেন। তাদের যুক্তি: standard ISA-তে এটি হতো একমাত্র গাণিতিক trap; IEEE 754 floating-point-ও শূন্য দিয়ে ভাগে trap না করে ∞ লিখে একটি flag বসায়। যে ভাষা exception চায়, তার compiler একটি beqz শাখা বসালেই হয় — প্রায় কখনো নেওয়া হয় না, তাই প্রায় বিনা খরচে। আর ভাগফল “সব বিট ১” মানটি divider-এর স্বাভাবিক আচরণ থেকেই আসে, বাড়তি circuit ছাড়া। ARM AArch64 একই দর্শনে ০ ফেরত দেয়।
ভাষাগুলোর আচরণ ভিন্ন। C আর C++-এ শূন্য দিয়ে ভাগ আর INT_MIN / −1 undefined behavior — compiler ধরে নিতে পারে এটি ঘটবেই না, আর সেই ধারণায় আশপাশের কোড বদলে দিতে পারে। তাই একই C প্রোগ্রাম x86-এ crash করে, RISC-V-তে নীরবে একটি মান দেয়, আর optimization-এ আরও অদ্ভুত কিছু করতে পারে। Java মানদণ্ড int-এ শূন্য দিয়ে ভাগে ArithmeticException চায় — তাই RISC-V বা ARM-এ JVM ভাগের আগে পরীক্ষা বসায়, আর x86-এ অনেক JVM hardware exception ধরে তাকে Java exception-এ রূপান্তর করে বলে জানা যায়। Python সবসময় ZeroDivisionError দেয়।
“নিরাপদ” তাই নির্ভর করে স্তরের ওপর। Hardware trap না করলে নিরাপত্তার দায় পড়ে ভাষা আর প্রোগ্রামারের ওপর; trap করলে hardware জটিল হয়, কিন্তু নীরব ভুল কম। এমবেডেড বা নিরাপত্তা-সংবেদনশীল কোডে সবচেয়ে ভালো অভ্যাস হলো ISA যা-ই হোক, ভাগের আগে ভাজক নিজে পরীক্ষা করা।
মূল উপলব্ধি: ISA, ভাষা আর প্রোগ্রাম — তিন স্তরের যেকোনো একটিতে শূন্য-ভাজক ধরতে হবে। RISC-V সেই কাজ সচেতনভাবে software-কে দিয়েছে, কারণ সেখানে তা সস্তা আর নমনীয়।
প্র ০৪Truncated, floored নাকি Euclidean — ভাগশেষের “সঠিক” নিয়ম কোনটি? C আর Python কেন আলাদা পথে গেল?
গণিতের দিক থেকে তিনটিই বৈধ — তিনটিই a = d·q + r আর |r| < |d| মানে। পার্থক্য কোন বৈশিষ্ট্য রক্ষা করা হচ্ছে তাতে। Truncated-এ (−a)/d = −(a/d): চিহ্ন উল্টালে ফলও শুধু চিহ্ন বদলায়, যা অনেকের কাছে স্বাভাবিক লাগে, আর sign-magnitude ধাঁচের hardware (মান ভাগ করে চিহ্ন বসানো) সরাসরি এটাই দেয়। Floored-এ ভাগশেষ ভাজকের চিহ্ন নেয়, ফলে d ধনাত্মক হলে a mod d সবসময় 0 থেকে d − 1 — ঘড়ির হিসাব, সপ্তাহের দিন, বৃত্তাকার buffer-এর সূচকে ঠিক এটাই দরকার। Donald Knuth তাঁর বইয়ে mod-কে floor দিয়েই সংজ্ঞায়িত করেছেন। Euclidean-এ ভাগশেষ সবসময় অঋণাত্মক; Raymond Boute ১৯৯২ সালে একটি প্রবন্ধে যুক্তি দেন, প্রোগ্রামিং ভাষার জন্য এটিই সবচেয়ে পরিচ্ছন্ন সংজ্ঞা।
C-র ইতিহাস কৌতূহলজনক। C89-এ ঋণাত্মক সংখ্যার ভাগ কোন দিকে গোল হবে তা implementation-এর ওপর ছাড়া ছিল, কারণ বিভিন্ন hardware বিভিন্ন কাজ করত। C99 তা truncation-এ বেঁধে দেয় — Fortran আর বেশিরভাগ hardware-এর সাথে মিল রেখে। C++11 একই পথে যায়, আর Java শুরু থেকেই truncation. ফলে hardware-এর idiv বা RISC-V-এর div সরাসরি ভাষার / হয়, বাড়তি instruction লাগে না।
Python-এর স্রষ্টা Guido van Rossum একটি লেখায় ব্যাখ্যা করেছেন কেন Python floor নিয়েছে: ব্যবহারিক কাজে, যেমন timestamp থেকে দিনের সময় বের করায়, ঋণাত্মক সংখ্যাতেও a % n-এর ফল 0 থেকে n − 1-এর মধ্যে থাকা বেশি কাজের, আর তা চাইলে // নিচের দিকে গোল করতেই হবে। দামটি সামান্য: hardware truncated দেয় বলে Python-এর interpreter বাড়তি একটি সংশোধন করে।
ব্যবহারিক শিক্ষা: এক ভাষার কোড অন্য ভাষায় নেওয়ার সময় (মানুষ বা AI — যে-ই করুক) ঋণাত্মক ভাগ আর % আলাদা করে পরীক্ষা করুন। C-তে সবসময় অঋণাত্মক ভাগশেষ চাইলে লিখুন ((a % n) + n) % n (n ধনাত্মক ধরে), বা Java-তে Math.floorMod.
মূল উপলব্ধি: “সঠিক” নিয়ম বলে কিছু নেই — আছে কোন বৈশিষ্ট্য দরকার। Hardware-এর সাথে মিল চাইলে truncated, চক্রাকার হিসাবে floored বা Euclidean.
প্র ০৫Compiler যদি ধ্রুবক দিয়ে ভাগকে গুণে বদলাতে পারে, তাহলে runtime-এ জানা ভাজকের কী হবে? Magic number-এর ধারণা কতদূর যায়?
ধ্রুবক দিয়ে ভাগকে গুণে বদলানোর ধারণা পুরোনো, কিন্তু সব ভাজক আর সব প্রস্থে নির্ভুলভাবে কাজ করে এমন সাধারণ পদ্ধতি আর তার প্রমাণ জনপ্রিয় করে Granlund ও Montgomery-র ১৯৯৪ সালের PLDI গবেষণাপত্র, আর gcc-তে এর বাস্তবায়ন। মূল কৌশল ওপরে দেখেছেন: 2s/d-এর ঠিক ওপরের পূর্ণসংখ্যা M নেওয়া, আর s এমনভাবে বাছা যাতে ভুল সব x-এ এক ভগ্নাংশ-একক পার না হয়। কিছু ভাজকে (যেমন ৩২-বিটে ৭) M ৩২ বিটে ধরে না — তখন একটি বাড়তি যোগ আর shift দিয়ে সামলানো হয়। Signed ভাগে চিহ্নের জন্য আরেকটি ছোট সংশোধন লাগে। Henry Warren-এর “Hacker's Delight” বইয়ে এই হিসাবগুলোর বিস্তারিত টেবিল আর প্রমাণ আছে।
Runtime ভাজকের ক্ষেত্রে হিসাব আলাদা। Magic number বের করতে নিজেই একটি ভাগ লাগে, তাই একবারের ভাগে লাভ নেই। কিন্তু একই ভাজক দিয়ে লাখো সংখ্যা ভাগ হলে (যেমন একটি hash table-এর আকার দিয়ে বারবার %, বা ছবি প্রক্রিয়ায় একই সংখ্যা দিয়ে প্রতিটি pixel ভাগ) একবার magic হিসাব করে বাকি সব গুণে করা যায়। libdivide-এর মতো open-source library ঠিক এটাই করে। Daniel Lemire ও সহকর্মীরা ২০১৯ সালে ভাগশেষ সরাসরি গুণ দিয়ে বের করার একটি কৌশল প্রকাশ করেন, আর range-এ সংখ্যা নামাতে (x × n) >> 32-এর মতো “ভাগহীন” কৌশলও hash table আর random number-এ ব্যবহৃত হয়।
Hardware-এর দিকেও চিন্তাটি দেখা যায়: Newton–Raphson আসলে runtime-এ 1/d-এর magic বানানো। আর GPU-তে integer ভাগ প্রায়ই floating-point reciprocal আর গুণ দিয়ে অনুকরণ করা হয় বলে জানা যায়, কারণ সেখানে বিশেষ integer divider রাখা লাভজনক নয়।
এখান থেকে প্রোগ্রামারের শিক্ষা: ধ্রুবক ভাগ হাতে optimize করবেন না — compiler আরও ভালো আর নির্ভুল করে। কিন্তু hot loop-এ পরিবর্তনশীল ভাজক থাকলে তা চিনে নিয়ে বাইরে সরানো, ২-এর ঘাতে আকার রাখা, বা libdivide-এর মতো সমাধান বিবেচনা করা যায় — মেপে দেখার পর।
মূল উপলব্ধি: ভাগ = গুণ × reciprocal. Reciprocal কখন আর কে হিসাব করে — compiler (ধ্রুবক), library (runtime, একবার), নাকি hardware (Newton–Raphson) — সেটাই বিভিন্ন কৌশলের পার্থক্য।
অনুশীলন
পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।
-
Restoring টেবিল (হাতে হিসাব): ৪-বিট restoring division-এ (A ৫ বিট, শুরুতে 0)
1011 ÷ 0011(11 ÷ 3)-এর পুরো টেবিল বানান — প্রতিটি iteration-এ shift ও বিয়োগের পর A, সিদ্ধান্ত আর Q. মোট কয়টি যোগ-বিয়োগ লাগল? Non-restoring হলে কয়টি লাগত?M = 00011.
iter shift: A A - M decision A after Q after 0 - - initial 00000 1011 1 00001 (1) 11110 (-2) restore, q0=0 00001 0110 2 00010 (2) 11111 (-1) restore, q0=0 00010 1100 3 00101 (5) 00010 (2) q0=1 00010 1001 4 00101 (5) 00010 (2) q0=1 00010 0011ভাগফল
0011= ৩, ভাগশেষ0010= ২. যাচাই: 3 × 3 + 2 = 11 ✓. যোগ-বিয়োগ: ৪টি বিয়োগ + ২টি restore = ৬টি. Non-restoring-এ: iteration ১-এ shift-এ A = 1, বিয়োগে −2 (q0 = 0); ২-এ shift-এ −4, যোগে −1 (q0 = 0); ৩-এ shift-এ −1, যোগে 2 (q0 = 1); ৪-এ shift-এ 5, বিয়োগে 2 (q0 = 1). শেষে A = 2 ধনাত্মক — সংশোধন নেই। মোট ৪টি যোগ-বিয়োগ। -
Non-restoring আর শেষের সংশোধন: ৪-বিট non-restoring division-এ
1101 ÷ 0011(13 ÷ 3) করুন। প্রতিটি iteration-এ যোগ না বিয়োগ হলো, নতুন A আর q0 লিখুন। শেষে সংশোধন লাগবে কি? সংশোধন না করলে কী ভুল উত্তর আসত?iter previous A operation new A q0 Q 1 0 (>= 0) shl, A - M 11110 (-2) 0 1010 2 -2 (< 0) shl, A + M 00000 (0) 1 0101 3 0 (>= 0) shl, A - M 11101 (-3) 0 1010 4 -3 (< 0) shl, A + M 11110 (-2) 0 0100 end -2 (< 0) fix: A + M 00001 (1)বিস্তারিত: iteration ২-এ shift-এর পর A = −2 × 2 + 1 = −3 (Q-এর বাঁয়ের বিট ১ ঢুকেছে), যোগে 0 → q0 = 1. Iteration ৪-এ shift-এ −3 × 2 + 1 = −5, যোগে −2. ভাগফল
0100= ৪, আর শেষ A = −2 ঋণাত্মক, তাই সংশোধন লাগবে: −2 + 3 = 1. ঠিক উত্তর 13 = 3 × 4 + 1 ✓. সংশোধন না করলে ভাগশেষ −2 আসত — তখনও 3 × 4 + (−2) = 10 ≠ 13, অর্থাৎ invariant ভেঙে যেত। মোট ৫টি যোগ-বিয়োগ (৪ + ১ সংশোধন); restoring-এ লাগত ৭টি (ভাগফলের তিনটি ০-বিটের জন্য ৩টি restore). -
আউটপুট অনুমান করুন: (ক) C প্রোগ্রামে
printf("%d %d %d %d\n", -7 / 2, -7 % 2, 7 / -2, 7 % -2);কী ছাপবে? (খ) Python-এprint(-7 // 2, -7 % 2, 7 // -2, 7 % -2)? (গ) RV32IM-এ নিচের instruction-গুলোর পর a2, a3, a4, t1, t2-তে কী থাকবে?li a0, -7 li a1, 0 div a2, a0, a1 rem a3, a0, a1 divu a4, a0, a1 li t0, 0x80000000 li t3, -1 div t1, t0, t3 rem t2, t0, t3(ক)
-3 -1 -3 1— C শূন্যের দিকে কাটে, ভাগশেষের চিহ্ন ভাজ্যের মতো (7 % −2 = 1, কারণ −2 × −3 + 1 = 7).(খ)
-4 1 -4 -1— Python floor নেয়, ভাগশেষের চিহ্ন ভাজকের মতো (7 % −2 = −1, কারণ −2 × −4 + (−1) = 7).(গ)
a2 = -1 (0xFFFFFFFF) # div by zero: all bits set a3 = -7 (0xFFFFFFF9) # rem by zero: the dividend a4 = 4294967295 (0xFFFFFFFF) # divu by zero: all bits set t1 = -2147483648 (0x80000000) # INT_MIN / -1 overflow: INT_MIN t2 = 0 # its remainder is 0কোনো instruction trap করে না, প্রোগ্রাম চলতেই থাকে। একই কাজ x86-এর
idiv-এ প্রথম ভাগেই #DE exception তুলত, আর C-তে এগুলো undefined behavior. -
Magic number বের করুন আর কোড লিখুন: (ক) ৩২-বিট unsigned x-এর জন্য x / 5-কে (x × M) >> 34 আকারে লিখতে M কত হবে? প্রমাণ করুন যে সব x < 232-এ ফল ঠিক। (খ)
uint32_tনিয়ে C-তেdiv5ফাংশন লিখুন যা কোনো ভাগ ছাড়াই কাজ করে। (গ) Newton–Raphson দিয়ে 1/0.8 বের করুন, x0 = 1.2 থেকে দুই ধাপ; প্রতিটি ধাপে ভুল e = 1 − d·x লিখুন।(ক) M = ⌈234/5⌉ = ⌈3435973836.8⌉ = 3435973837 =
0xCCCCCCCD, আর 5M = 234 + 1. তাহলে x·M/234 = x/5 + x/(5·234). ভুল < 232/(5·234) = 1/20. x/5-এর ভগ্নাংশ অংশ সর্বোচ্চ 4/5, আর 4/5 + 1/20 = 17/20 < 1 — তাই floor সবসময় ঠিক ভাগফল দেয়। (এটি x/10-এর একই M, শুধু shift এক কম: x/10 = (x/5)/2.) পুরো 232টি মানে script দিয়েও যাচাই করা হয়েছে — কোনো অমিল নেই।#include <stdint.h> uint32_t div5(uint32_t x) { return (uint32_t)(((uint64_t)x * 0xCCCCCCCDu) >> 34); /* 64-bit product: no overflow */ }(খ) গুরুত্বপূর্ণ অংশ
(uint64_t)x— cast না করলে গুণ ৩২ বিটে হয়ে ওপরের বিট হারিয়ে যেত।(গ) d = 0.8, x0 = 1.2: e0 = 1 − 0.96 = 0.04. x1 = 1.2 × (2 − 0.96) = 1.2 × 1.04 = 1.248, e1 = 1 − 0.9984 = 0.0016 = 0.042. x2 = 1.248 × (2 − 0.9984) = 1.248 × 1.0016 = 1.2499968, e2 = 0.00000256 = 0.00162. আসল মান 1.25 — দুই ধাপে প্রায় ছয় অঙ্ক ঠিক।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমRestoring division-এর একটি iteration আর তার পরের কাজগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- A:Q জোড়াকে এক বিট বাঁয়ে সরানো, যাতে Q-এর বাঁয়ের বিট A-তে ঢোকে
- A থেকে ভাজক M বিয়োগ করা
- A-এর চিহ্ন-বিট দেখা
- ঋণাত্মক হলে q0 = 0 আর A + M দিয়ে restore, নইলে q0 = 1
- n বার শেষ হলে Q থেকে ভাগফল আর A থেকে ভাগশেষ পড়া
n-বিট unsigned restoring division-এ সবচেয়ে খারাপ ক্ষেত্রে মোট কয়টি যোগ-বিয়োগ লাগে, আর non-restoring-এ সর্বোচ্চ কয়টি?
- দুটিতেই n
- দুটিতেই 2n
- Restoring 2n, non-restoring n + 1
- Restoring n2, non-restoring n
C99 বা তার পরের C-তে int হিসেবে -7 % 3-এর মান কত?
- 2
- −1
- 1
- Undefined behavior
RV32IM-এ a0 = 7 আর a1 = 0 থাকা অবস্থায় div a2, a0, a1 আর rem a3, a0, a1 চালালে কী হয়?
- একটি divide-by-zero exception হয়ে প্রোগ্রাম থামে
- a2 = 0, a3 = 0
- a2 = 7, a3 = 0
- a2 = −1 (0xFFFFFFFF), a3 = 7, কোনো trap নেই
1/d বের করতে Newton–Raphson xk+1 = xk(2 − d·xk) ব্যবহার হচ্ছে। কোনো ধাপে ভুল e = 1 − d·x = 10−3 হলে পরের ধাপে ভুল মোটামুটি কত?
- 10−6
- 5 × 10−4
- 10−4
- 10−9
৩২-বিট unsigned x-এর জন্য (uint32_t)(((uint64_t)x * 0xAAAAAAABu) >> 33) কী হিসাব করে?
- x >> 33, অর্থাৎ সবসময় 0
- x / 3 (সব x-এ নির্ভুল)
- x / 10
- x / 3, কিন্তু বড় x-এ মাঝে মাঝে এক বেশি
একটি hash table-এ ১০২৪টি bucket, আর hash h একটি ৩২-বিট unsigned সংখ্যা। কোন কথাটি সত্য?
- h % 1024 আর h & 1024 সমান
- ১০০০টি bucket হলে h % 1000 = h & 999
- h % 1024 = h & 1023, আর AND অনেক সস্তা
- h বড় হলে h % 1024 আর h & 1023 আলাদা হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Invariant: ভাজ্য = ভাজক × ভাগফল + ভাগশেষ, আর |ভাগশেষ| < |ভাজক|. Binary-তে প্রতিটি ভাগফল-বিট ০ বা ১, কারণ shift-এর পর আংশিক ভাগশেষ ভাজকের দ্বিগুণের কম।
- Restoring: A:Q বাঁয়ে সরানো, তারপর A − M; ঋণাত্মক হলে q0 = 0 আর A + M (restore), নইলে q0 = 1. A-তে n + 1 বিট; মোট n থেকে 2n যোগ-বিয়োগ। 0111 ÷ 0010 → Q = 0011, R = 0001.
- Non-restoring: আগের A ঋণাত্মক হলে পরের ধাপে যোগ, কারণ 2(A + M) − M = 2A + M; ঠিক n টি কাজ, শেষ A ঋণাত্মক হলে একটি সংশোধন।
- Signed: C, Java, RISC-V truncated (ভাগশেষ ভাজ্যের চিহ্নে), Python floored (ভাজকের চিহ্নে), Euclidean-এ ভাগশেষ ≥ 0. RISC-V-তে ÷0 → ভাগফল সব বিট ১, ভাগশেষ ভাজ্য; INT_MIN ÷ −1 → INT_MIN, ভাগশেষ 0; x86-এ দুটিতেই #DE; C-তে দুটিই undefined.
- দ্রুত ভাগ আর এড়ানো: SRT (redundant অঙ্ক, lookup table — Pentium FDIV), Newton–Raphson x(2 − dx) আর Goldschmidt (ভুল প্রতি ধাপে বর্গ)। ভাগ গুণের চেয়ে বহুগুণ ধীর, pipelined নয়; compiler ধ্রুবক ভাগকে magic গুণে বদলায় (x/3 = x·0xAAAAAAAB >> 33), আর ২-এর ঘাতে % = AND.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৩ · Floating point — IEEE 754, normalized, denormal আর বিশেষ মানপরবর্তী পর্বভগ্নাংশ আর বিশাল সংখ্যার বিট-রূপ।
- পর্ব ২১ · গুণ — shift-add, Booth-এর algorithm আর দ্রুত multiplierআগের পর্বShift-add আর Wallace tree — ভাগের সাথে তুলনার ভিত্তি।
- পর্ব ২৪ · Floating point-এর হিসাব — যোগ, গুণ, rounding আর ভুলের ফাঁদসামনেFloating-point যোগ, গুণ, ভাগ আর rounding-এর ধাপ।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বলজিক গেট থেকে pipeline আর cache — পুরো রোডম্যাপ।
- DSA · Hash tableপাশের ট্র্যাক২-এর ঘাতের আকার, h & (size − 1) আর collision সামলানো।
- C প্রোগ্রামিং · Undefined behavior আর নিরাপদ কোডপাশের ট্র্যাকশূন্য দিয়ে ভাগ আর INT_MIN / −1 কেন বিপজ্জনক।