সরাসরি পর্বে যান
পর্ব ২০ · ৬২-এর মধ্যে · মডিউল ৪

Integer যোগ-বিয়োগ — adder-subtractor, overflow আর status flag

Integer Arithmetic at the ISA Level — Flags vs Traps vs RISC-V Idioms, Multi-precision, Saturation and Shift-based Division

Taught in Bangla, this lesson shows how ISAs report integer results: x86 and ARM flags, MIPS trapping add versus addu, and flag-free RISC-V, where sltu and slt sequences detect carry and signed overflow, with proofs. It covers multi-precision addition with adc, bignums, saturating arithmetic, blt versus bltu bugs, shift-based signed division, C and Python division rules, checked arithmetic and Rust, and x86 BCD instructions, using simulators and live C programs.

এই পর্বে যা শিখবেন

  • x86/ARM-এর flag, MIPS-এর trapping add আর RISC-V-এর flag-হীন নকশা — একই সমস্যার তিন সমাধান
  • RISC-V-তে unsigned carry আর signed overflow ধরার নির্দিষ্ট instruction sequence, আর কেন সেগুলো ঠিক — প্রমাণসহ
  • বহু-শব্দের (multi-precision) যোগ: ৩২-বিট যন্ত্রে ৬৪ ও ১২৮ বিট, adc, আর Python বা GMP-এর bignum কীভাবে কাজ করে
  • Saturating arithmetic, signed বনাম unsigned তুলনার বাগ (blt বনাম bltu), আর shift দিয়ে signed ভাগের bias কৌশল
  • C আর Python-এর ভাগের নিয়ম, checked arithmetic, Rust-এর পদ্ধতি, আর x86-এর পুরোনো দশমিক (BCD) instruction

১ · এক adder, দুই অর্থ — ISA-র সামনে প্রশ্নটা কী

মডিউল ২-এ hardware-এর দিকটা দেখা হয়ে গেছে। পর্ব ১১-এ (Adder) full adder জুড়ে ripple আর carry-lookahead adder, আর পর্ব ১৩-এ (ALU নকশা) সেই adder দিয়েই বিয়োগ: B-এর প্রতিটি বিটে একটি XOR আর প্রথম বিটের carry-in = 1, ফলে A − B = A + B′ + 1. এই যন্ত্রটির নাম adder-subtractor. এই পর্বে B′ মানে B-এর সব বিট উল্টানো (কোডে ~B), আর ⊕ মানে XOR. এক ছবিতে পুরো circuit:

          B (n bits)
            │
   sub ──►[ XOR each bit ]──► B or B'
   │                          │
   │      A ─────────────►[ n-bit adder ]──► S (n bits)
   └──────────► carry-in ──►      │
                              carry-out (C),  V = c(n) xor c(n-1)

এখানে একটি কথা মনে রাখলে বাকি পর্ব সহজ হয়ে যাবে: adder জানে না সংখ্যাগুলো signed না unsigned. পর্ব ০৫-এ (two's complement) দেখেছেন, two's complement-এর সবচেয়ে বড় সুবিধা এটাই — একই বিট-প্যাটার্নের যোগ দুই অর্থেই ঠিক, যদি ফল সীমার ভেতরে থাকে। সীমা ছাড়ালে দুই অর্থে দুই রকম বিপদ: unsigned-এ MSB থেকে carry বেরোয়, আর signed-এ overflow হয় (V = 1). কোন বিপদটি গুরুত্বপূর্ণ, তা জানে শুধু প্রোগ্রাম।

মুদি দোকানের ক্যালকুলেটরের পর্দায় আট ঘর। হিসাব আট ঘর ছাড়ালে সে কোণে একটি “E” জ্বালায় — কিন্তু থেমে যায় না, শেষের আট অঙ্ক দেখিয়েই যায়। দোকানদার ঠিক করেন “E” দেখে কী করবেন: হিসাব আবার ভাগ করে করবেন, নাকি খেয়াল না করে ভুল বিল দেবেন। CPU-র adder-ও এমন; প্রশ্ন শুধু “E” বাতিটি কীভাবে প্রোগ্রামের কাছে পৌঁছায়। ISA-ভেদে উত্তর তিন রকম।

তাই ISA নকশাকারের সামনে প্রশ্ন: carry আর overflow-এর খবর কোথায় রাখবেন? একটি বিশেষ register-এ (flag)? নাকি overflow হলে প্রোগ্রাম থামিয়ে দেবেন (trap)? নাকি কিছুই রাখবেন না, software-কে নিজে বের করে নিতে বলবেন? তিনটি পথই বাস্তবে আছে। আর এই সিদ্ধান্ত থেকেই ঠিক হয় বড় সংখ্যার যোগ, তুলনা আর শাখা কতগুলো instruction-এ হবে।

মূল ধারণা

Hardware যোগ করে mod 2n; অর্থ দেয় software. Carry মানে “unsigned ফল সীমার বাইরে”, V মানে “signed ফল সীমার বাইরে”। এই দুটি তথ্য প্রোগ্রামের কাছে কীভাবে পৌঁছাবে — এটুকুই ISA-র দায়িত্ব।

২ · ISA কীভাবে carry আর overflow জানায় — flag, trap, নাকি কিছুই না

পথ ১: flag — x86 আর ARM

x86-এর প্রায় প্রতিটি গাণিতিক instruction ফলের সাথে status flag লেখে: CF (unsigned carry), OF (signed overflow), ZF, SF. ARM-এর নাম C, V, Z, N; তবে ARM-এ সাধারণ add flag ছোঁয় না — শেষে S লাগানো adds/subs লেখে। Flag-গুলো কীভাবে তৈরি হয় আর বিয়োগের পর x86-এর CF আর ARM-এর C কেন উল্টো অর্থ বহন করে, তা পর্ব ১৩-এর অংশ ৬-এ বিস্তারিত আছে; এখানে আমরা শুধু ব্যবহারটা দেখব। Overflow হলে লাফ দিতে চাইলে একটি instruction-ই যথেষ্ট:

# উদাহরণ — a + b overflow করলে handler-এ লাফ (৩২-বিট int, হাতে লেখা; x86 অংশ Intel syntax-এ)
# x86-64                  # ARM64
add   edi, esi            adds  w0, w0, w1
jo    overflow   # OF=1   b.vs  overflow     # V=1 (signed)
# unsigned হলে:
add   edi, esi            adds  w0, w0, w1
jc    overflow   # CF=1   b.cs  overflow     # C=1 (carry)

পুরোনো x86-এ into (interrupt on overflow) নামে একটি instruction-ও ছিল, যা OF = 1 হলে সরাসরি exception তুলত; ৬৪-বিট মোডে এটি আর বৈধ নয় — কম ব্যবহৃত আরও কয়েকটি instruction-এর সাথে AMD64 নকশায় এটি বাদ পড়ে।

পথ ২: trap — MIPS-এর add বনাম addu

MIPS অন্য পথ নিয়েছিল। এর add, addi আর sub signed overflow হলে নিজে থেকেই একটি overflow trap (exception) তোলে — ফল register-এ লেখাই হয় না, CPU সরাসরি operating system-এর exception handler-এ চলে যায়। আর addu, addiu, subu কখনো trap করে না, শুধু wrap করে। নামের “u” দেখে অনেকে ভাবেন এগুলো unsigned যোগ — আসলে নয়। যোগফলের বিট দুটিতে হুবহু এক; পার্থক্য শুধু trap-এ। এমনকি addiu তার immediate-কে sign-extend করে, অর্থাৎ ঋণাত্মক ধ্রুবকও যোগ করা যায়।

বাস্তবে trap প্রায় কেউ ব্যবহার করেনি। C ভাষায় unsigned overflow নিয়মমতো wrap করে, আর signed overflow undefined behaviour — কোনোটিতেই trap চাওয়া হয় না। তাই MIPS-এর জন্য C compiler সাধারণত সব যোগে addu/addiu বসাত, যাতে pointer হিসাব বা hash-এর ইচ্ছাকৃত wrap-এ হঠাৎ exception না ওঠে। Trapping add কাজে লাগত মূলত সেই ভাষাগুলোর জন্য, যাদের নিয়মে overflow ধরা বাধ্যতামূলক (যেমন Ada).

পথ ৩: কিছুই না — RISC-V

RISC-V-এর base ISA-তে (RV32I, RV64I) কোনো flag register নেই, আর কোনো integer instruction overflow-এ trap-ও করে না। add শুধু mod 232 যোগফল লেখে — ব্যস। Overflow জানতে চাইলে প্রোগ্রাম নিজে এক থেকে তিনটি বাড়তি instruction দিয়ে বের করে নেয় (পরের অংশ)। এমনকি M extension-এর ভাগ instruction-ও শূন্য দিয়ে ভাগে trap করে না, একটি নির্দিষ্ট ফল দেয় (অংশ ৮)।

বিষয়x86-64ARM (AArch64)MIPS (ক্লাসিক)RISC-V (RV32I/RV64I)
যোগের পর carry জানাCF flag (সবসময় লেখা হয়)C flag (শুধু adds-এ)flag নেই; sltuflag নেই; sltu
Signed overflow জানাOF flag, joV flag, b.vsadd-এ trap, বা addu + যাচাইtrap নেই; slt/xor দিয়ে যাচাই
Carry-সহ যোগadcadc, adcsনেইনেই (base ISA-তে)
তুলনা আর শাখাcmp + jl/jbcmp + b.lt/b.loslt + bneblt/bltu এক instruction-এ

RISC-V কেন এমন করল? Flag একটি লুকোনো, সবার ভাগের register: প্রায় প্রতিটি instruction সেটি লেখে, তাই pipeline আর out-of-order CPU-কে flag-এর জন্যও নির্ভরতা খুঁজতে আর rename করতে হয় (পর্ব ৩৬, ৪০)। আর trap মানে প্রতিটি যোগকে “হয়তো exception তুলবে” ধরে নিয়ে নিখুঁত (precise) exception-এর ব্যবস্থা রাখা (পর্ব ৩৮)। RISC-V-র নকশাকারদের যুক্তি ছিল, বেশিরভাগ যোগে overflow যাচাই লাগেই না; যেখানে লাগে, সেখানে শাখা-instruction দিয়ে সস্তায় করা যায়। এর দামটা কোথায়, তা অংশ ৪-এ দেখবেন।

৩ · RISC-V-তে overflow ধরা — sltu আর slt-এর কৌশল

RISC-V-এর unprivileged spec-এর integer computational instruction অংশে একটি ব্যাখ্যামূলক টীকা আছে, যেখানে নকশাকাররা নিজেরাই overflow ধরার কয়েকটি sequence দেখিয়েছেন। নিচে সেগুলো প্রায় সেই রূপেই দেওয়া হলো (register-এর নাম ও মন্তব্য আমাদের; spec-এর সাম্প্রতিক সংস্করণে লেখার ধরন একটু ভিন্ন হতে পারে)। সবগুলোর ভিত্তি দুটি তুলনা-instruction: sltu আর slt — set-on-less-than-এর unsigned ও signed রূপ (hardware-এর দিক পর্ব ১৩-এর অংশ ৪-এ)।

# উদাহরণ — RV32I, t0 = t1 + t2

# (ক) unsigned যোগে carry: একটি বাড়তি শাখা
add   t0, t1, t2
bltu  t0, t1, overflow      # sum <u t1  <=>  carry out
#     (carry মান হিসেবে চাইলে: sltu t3, t0, t1)

# (খ) signed যোগ, একটি operand-এর চিহ্ন জানা (যেমন ধনাত্মক immediate)
addi  t0, t1, +imm
blt   t0, t1, overflow      # positive imm, yet sum got smaller

# (গ) সাধারণ signed যোগ: তিনটি বাড়তি instruction
add   t0, t1, t2
slti  t3, t2, 0             # t3 = (t2 < 0)
slt   t4, t0, t1            # t4 = (sum < t1)
bne   t3, t4, overflow      # these two must agree when there is no overflow

কেন sltu-তে carry ধরা পড়ে — প্রমাণ

ধরুন a আর b দুটি ৩২-বিট unsigned সংখ্যা, আর s = (a + b) mod 232. দুই ক্ষেত্র:

  • Carry নেই: s = a + b, আর b ≥ 0, তাই s ≥ a. অর্থাৎ s < a মিথ্যা।
  • Carry আছে: s = a + b − 232. যেহেতু b < 232, তাই b − 232 < 0, ফলে s < a সত্য।

সুতরাং carry ⇔ s <u a (একইভাবে s <u b). একটি sltu দিয়েই x86-এর CF পাওয়া গেল।

কেন (গ)-এর sequence signed overflow ধরে — প্রমাণ

এখন a, b, s-কে signed ধরুন। Overflow না হলে s = a + b হুবহু, তাই s < a ঠিক তখনই, যখন b < 0 — দুটি তুলনা একমত। Overflow হলে দুটি উপক্ষেত্র: b ≥ 0 হলে (ধনাত্মক overflow, a-ও তখন ধনাত্মক) s = a + b − 232 < a, অথচ b < 0 মিথ্যা; আর b < 0 হলে (ঋণাত্মক overflow) s = a + b + 232 > a, অথচ b < 0 সত্য। দুই উপক্ষেত্রেই তুলনা দুটি দ্বিমত। অতএব overflow ⇔ (b < 0) ≠ (s < a) — ঠিক যা slti, slt আর bne পরীক্ষা করে।

আরেকটি প্রচলিত কৌশল সরাসরি পর্ব ০৫-এর চিহ্নের নিয়ম থেকে আসে: “দুটি operand-এর চিহ্ন এক, অথচ ফলের চিহ্ন আলাদা”। বিটের ভাষায়, (s ⊕ a) আর (s ⊕ b) দুটিরই MSB ১ হলে overflow:

# উদাহরণ — XOR কৌশল (signed add overflow), RV32I
add   t0, t1, t2
xor   t3, t0, t1            # sign of sum differs from t1 ?
xor   t4, t0, t2            # sign of sum differs from t2 ?
and   t3, t3, t4
bltz  t3, overflow          # MSB = 1  ->  overflow   (bltz = blt t3, x0)

# বিয়োগ t0 = t1 - t2-এর জন্য spec-ধাঁচের যাচাই (একই যুক্তি, তুলনা উল্টো):
sub   t0, t1, t2
slti  t3, t2, 0
slt   t4, t1, t0            # no overflow: (t1 < t0) exactly when t2 < 0
bne   t3, t4, overflow

নিচের stepper-এ দুটি সীমানার উদাহরণে (ক) আর (গ) একসাথে চলছে — একটিতে শুধু signed overflow, অন্যটিতে শুধু unsigned carry. “পরে” চাপুন:

উদাহরণ ১add t0, t1, t2
t1 = 0x7FFFFFFF   (signed  2147483647 = INT_MAX)
t2 = 0x00000001   (signed  1)
add t0, t1, t2
t0 = 0x80000000   (signed -2147483648, unsigned 2147483648)
Unsigned carry?sltu t3, t0, t1
t0 <u t1 ?   2147483648 < 2147483647 ?  no
t3 = 0   -> no carry
check: 2147483647 + 1 = 2147483648 < 2^32   (fits as unsigned)
Signed যাচাইslti, slt
slti t3, t2, 0   : 1 < 0 ?            t3 = 0
slt  t4, t0, t1  : -2147483648 < 2147483647 ?  t4 = 1
সিদ্ধান্তbne t3, t4, overflow
t3 = 0, t4 = 1  ->  not equal  ->  branch taken: signed overflow
check: 2147483647 + 1 = 2147483648 > INT_MAX
উদাহরণ ২−1 + −1
t1 = t2 = 0xFFFFFFFF   (signed -1, unsigned 4294967295)
add  t0, t1, t2   : t0 = 0xFFFFFFFE
sltu t3, t0, t1   : 4294967294 < 4294967295 ?  t3 = 1  -> carry
check: 4294967295 + 4294967295 = 8589934590 >= 2^32
Signed যাচাইএকমত — overflow নেই
slti t3, t2, 0   : -1 < 0 ?    t3 = 1
slt  t4, t0, t1  : -2 < -1 ?    t4 = 1
bne  t3, t4      : equal  ->  not taken: no signed overflow   (-1 + -1 = -2)
  1. যোগ। t1 = 0x7FFFFFFF (৩২-বিট signed-এর সর্বোচ্চ মান), t2 = 1. add কোনো প্রশ্ন না করে wrap করা ফল লেখে: 0x80000000 — signed চোখে −2147483648, unsigned চোখে 2147483648.
  2. Carry আছে কি? sltu দেখে ফল t1-এর চেয়ে unsigned অর্থে ছোট কি না। 2147483648 ছোট নয়, তাই t3 = 0 — carry নেই। ঠিকই: unsigned অর্থে 2147483648 সীমার (232 − 1) ভেতরে।
  3. Signed-এর দুই তুলনা। slti: t2 ঋণাত্মক কি? না, t3 = 0. slt: ফল কি t1-এর চেয়ে ছোট? signed অর্থে −2147483648 < 2147483647, তাই t4 = 1. ধনাত্মক সংখ্যা যোগ করে ফল ছোট হয়ে গেছে — সন্দেহজনক।
  4. সিদ্ধান্ত। t3 ≠ t4, তাই bne লাফ দেয়: signed overflow. আসল যোগফল 2147483648, INT_MAX-এর এক বেশি। একই বিট-প্যাটার্নে unsigned অর্থে কোনো সমস্যা নেই, signed অর্থে আছে।
  5. দ্বিতীয় উদাহরণ। দুটি operand-ই 0xFFFFFFFF. যোগফল 0xFFFFFFFE. sltu: 4294967294 < 4294967295, তাই t3 = 1 — carry বেরিয়েছে; unsigned আসল যোগফল 8589934590 সত্যিই ৩২ বিটে ধরে না।
  6. Signed অর্থে ঠিক। t2 = −1 ঋণাত্মক, t3 = 1; ফল −2 < −1, t4 = 1. দুটি একমত, শাখা নেই — −1 + −1 = −2 সম্পূর্ণ সঠিক। এখানে carry হয়েছে কিন্তু overflow হয়নি; প্রথম উদাহরণে উল্টো। Carry আর overflow দুটি আলাদা প্রশ্ন।

দুটি উদাহরণ দিয়ে নিয়ম প্রমাণ হয় না। নিচের simulator RV32-এর add, sub, slt, sltu, xor, and-কে ৩২-বিট প্যাটার্নে নকল করে (>>> 0 দিয়ে unsigned, | 0 দিয়ে signed চোখ), চারটি কৌশল চালায়, আর ফলকে JavaScript-এর BigInt দিয়ে করা নির্ভুল গণিতের সাথে মেলায় — সীমানার সাতটি জোড়ায় এবং ২,০০,০০০টি এলোমেলো জোড়ায়। Run চাপুন:

// RV32 registers hold 32-bit patterns. u() = unsigned view, s() = signed view.
const u = x => x >>> 0, s = x => x | 0;
const add = (a, b) => u(a + b), sub = (a, b) => u(a - b);
const sltu = (a, b) => +(u(a) < u(b)), slt = (a, b) => +(s(a) < s(b));
const xor = (a, b) => u(a ^ b), and = (a, b) => u(a & b), srli = (a, k) => a >>> k;
function check(t1, t2) {
  const t0 = add(t1, t2);
  const carry = sltu(t0, t1);                          // add; sltu t3, t0, t1
  const ovfSpec = +(slt(t2, 0) !== slt(t0, t1));       // slti t3,t2,0; slt t4,t0,t1; bne
  const ovfXor = srli(and(xor(t0, t1), xor(t0, t2)), 31); // sign of (s^a)&(s^b)
  const d = sub(t1, t2);                               // sub t0, t1, t2
  const subOvf = +(slt(t2, 0) !== slt(t1, d));         // slti; slt t4, t1, t0; bne
  const A = BigInt(s(t1)), B = BigInt(s(t2)), lim = 2n ** 31n;
  const truth = { carry: +(BigInt(u(t1)) + BigInt(u(t2)) >= 2n ** 32n),
    ovf: +(A + B >= lim || A + B < -lim), subOvf: +(A - B >= lim || A - B < -lim) };
  return { t0, carry, ovfSpec, ovfXor, subOvf, truth };
}
const hx = v => "0x" + u(v).toString(16).toUpperCase().padStart(8, "0");
const cases = [[0x7FFFFFFF, 1], [0xFFFFFFFF, 0xFFFFFFFF], [0x80000000, 0x80000000],
  [0x7FFFFFFF, 0x7FFFFFFF], [0x80000000, 0x7FFFFFFF], [0xFFFFFFF0, 0x20], [0, 0x80000000]];
console.log("t1         t2         sum        carry ovf(spec) ovf(xor) subOvf  truth c/o/s");
for (const [a, b] of cases) {
  const r = check(a, b), t = r.truth;
  console.log(`${hx(a)} ${hx(b)} ${hx(r.t0)}   ${r.carry}      ${r.ovfSpec}        ${r.ovfXor}       ${r.subOvf}     ${t.carry}/${t.ovf}/${t.subOvf}`);
}
let seed = 12345, bad = 0, n = 200000;                 // deterministic LCG sweep
const rnd = () => (seed = u(Math.imul(seed, 1664525) + 1013904223));
for (let i = 0; i < n; i++) {
  const r = check(rnd(), rnd()), t = r.truth;
  if (r.carry !== t.carry || r.ovfSpec !== t.ovf || r.ovfXor !== t.ovf || r.subOvf !== t.subOvf) bad++;
}
console.log(`random pairs: ${n}, mismatches with exact BigInt math: ${bad}`);

আউটপুট (নির্ধারিত — প্রতিবার একই):

t1         t2         sum        carry ovf(spec) ovf(xor) subOvf  truth c/o/s
0x7FFFFFFF 0x00000001 0x80000000   0      1        1       0     0/1/0
0xFFFFFFFF 0xFFFFFFFF 0xFFFFFFFE   1      0        0       0     1/0/0
0x80000000 0x80000000 0x00000000   1      1        1       0     1/1/0
0x7FFFFFFF 0x7FFFFFFF 0xFFFFFFFE   0      1        1       0     0/1/0
0x80000000 0x7FFFFFFF 0xFFFFFFFF   0      0        0       1     0/0/1
0xFFFFFFF0 0x00000020 0x00000010   1      0        0       0     1/0/0
0x00000000 0x80000000 0x80000000   0      0        0       1     0/0/1
random pairs: 200000, mismatches with exact BigInt math: 0

শেষ কলাম (truth) BigInt-এর নির্ভুল উত্তর: carry / যোগে overflow / বিয়োগে overflow. প্রতিটি সারিতে বাঁয়ের চারটি কৌশল-কলাম তার সাথে হুবহু মেলে। তৃতীয় সারি (0x80000000 + 0x80000000) সবচেয়ে চরম: ফল ০, carry-ও আছে, signed overflow-ও আছে (−231 + −231 = −232). পঞ্চম আর সপ্তম সারিতে যোগ নিরাপদ, কিন্তু বিয়োগ নয়: −2147483648 − 2147483647 আর 0 − (−2147483648) দুটিই সীমার বাইরে — বিশেষ করে 0 - INT_MIN, অর্থাৎ INT_MIN-কে negate করা, একটি ক্লাসিক ফাঁদ। ২,০০,০০০টি এলোমেলো জোড়ায় অমিল শূন্য।

৪ · বহু-শব্দের যোগ — ৩২-বিট যন্ত্রে ৬৪ আর ১২৮ বিট

প্রাথমিক বিদ্যালয়ে যেভাবে যোগ শিখেছিলেন — এককের ঘর, তারপর হাতে থাকা ১ নিয়ে দশকের ঘর — বড় সংখ্যার যোগ CPU-তেও ঠিক তা-ই। পার্থক্য শুধু, এক-একটি “ঘর” এখানে একটি পুরো register: ৩২-বিট যন্ত্রে ৩২ বিট, অর্থাৎ ভিত্তি 232-এর একটি “অঙ্ক”। এই অঙ্কগুলোকে বলে limb, আর এভাবে register-এর চেয়ে চওড়া সংখ্যার হিসাবকে বলে multi-precision arithmetic. ৩২-বিট CPU-তে C-র uint64_t যোগ, বা ৬৪-বিট CPU-তে ১২৮-বিট যোগ — দুটিই এভাবে হয়।

রেলস্টেশনের টিকিট কাউন্টারে দিনের হিসাব: খুচরা পয়সার বাক্স ১০০ পয়সা ছাড়ালেই এক টাকার নোট হয়ে টাকার বাক্সে যায়। পয়সার বাক্সের হিসাব আগে, টাকার বাক্সের হিসাব পরে — আর মাঝখানের “এক টাকা উঠল কি না” তথ্যটুকুই carry. বাক্স যতগুলোই হোক, নিয়ম একটাই।

x86 আর ARM-এ এর জন্য বিশেষ instruction আছে: add with carry. প্রথম limb-এ সাধারণ যোগ, যা carry flag-এ carry রাখে; পরের limb-এ adc, যা দুটি operand-এর সাথে আগের carry-ও যোগ করে আর নতুন carry রেখে যায়। RISC-V-তে carry flag নেই, তাই sltu দিয়ে carry বের করে আলাদা add-এ যোগ করতে হয়:

# উদাহরণ — ৩২-বিট যন্ত্রে uint64_t যোগ (a = hi:lo, b = hi:lo), হাতে লেখা; x86 অংশ Intel syntax-এ
# x86 (32-bit)          # ARM (AArch32)        # RISC-V RV32I (a1:a0 += a3:a2)
add  eax, ecx           adds r0, r0, r2        add   a0, a0, a2    # low halves
adc  edx, ebx           adc  r1, r1, r3        sltu  t0, a0, a2    # carry = new low <u a2
                                               add   a1, a1, a3    # high halves
                                               add   a1, a1, t0    # + carry

লক্ষ করুন, RISC-V-তে sltu তুলনা করে a2-এর সাথে, a0-এর সাথে নয় — কারণ a0 ততক্ষণে নতুন যোগফল দিয়ে মুছে গেছে। অংশ ৩-এর প্রমাণ বলে, s <u a বা s <u b — যেকোনোটি দিয়েই carry ধরা যায়। Compiler (যেমন RV32-এর জন্য gcc -O2) uint64_t যোগে প্রায় এই চারটি instruction-ই বানায়, register-এর ক্রম একটু ভিন্ন হতে পারে। এবার একটি সংখ্যা দিয়ে ধাপে ধাপে:

ইনপুটregister জোড়া
A = 0x00000002_FFFFFFF0  (12884901872)   a1 = 0x00000002   a0 = 0xFFFFFFF0
B = 0x00000001_00000020  ( 4294967328)   a3 = 0x00000001   a2 = 0x00000020
নিচের অর্ধেকadd a0, a0, a2
  0xFFFFFFF0
+ 0x00000020
= 0x1_00000010   (33 bits!)
a0 = 0x00000010    (only 32 bits are kept)
Carrysltu t0, a0, a2
a0 <u a2 ?   0x00000010 < 0x00000020 ?  yes
t0 = 1     (the lost 33rd bit, recovered)
উপরের অর্ধেকadd a1, a1, a3
a1 = 0x00000002 + 0x00000001 = 0x00000003
Carry যোগadd a1, a1, t0
a1 = 0x00000003 + 1 = 0x00000004
result a1:a0 = 0x00000004_00000010 = 17179869200
check 12884901872 + 4294967328 = 17179869200   ok
তুলনাflag থাকলে (x86: Intel syntax)
x86 :  add eax, ecx    ; CF = 1
       adc edx, ebx    ; 2 + 1 + CF = 4
ARM :  adds r0, r0, r2 ; C = 1
       adc  r1, r1, r3
RISC-V: 4 instructions, x86/ARM: 2
  1. দুই অর্ধেক। RV32-এর register ৩২ বিটের, তাই প্রতিটি ৬৪-বিট সংখ্যা থাকে একটি register জোড়ায়: উপরের ৩২ বিট (hi) আর নিচের ৩২ বিট (lo). Calling convention অনুযায়ী প্রথম সংখ্যা a1:a0-এ, দ্বিতীয়টি a3:a2-এ।
  2. নিচের অর্ধেক যোগ। 0xFFFFFFF0 + 0x20 = 0x1_00000010 — ৩৩ বিট লাগে। Register রাখে শুধু নিচের ৩২ বিট, 0x00000010; ৩৩তম বিটটি — carry — হারিয়ে গেল, কারণ রাখার কোনো flag নেই।
  3. Carry উদ্ধার। নতুন a0 (0x10) কি a2 (0x20)-এর চেয়ে unsigned অর্থে ছোট? হ্যাঁ — যোগ করে ছোট হয়ে যাওয়া মানেই wrap হয়েছে। তাই t0 = 1: হারানো ৩৩তম বিট ফিরে পাওয়া গেল।
  4. উপরের অর্ধেক যোগ। 2 + 1 = 3. এই যোগ নিচের যোগের ওপর নির্ভর করে না, তাই superscalar CPU এটি sltu-এর সাথে একই সময়ে চালাতে পারে।
  5. Carry যোগ। 3 + 1 = 4. ফল a1:a0 = 0x00000004_00000010 = 17179869200, যা 12884901872 + 4294967328-এর সাথে মেলে।
  6. Flag-ওয়ালা ISA-র সাথে তুলনা। x86 আর ARM দুটি instruction-এ কাজটি সারে, কারণ carry flag-এ থেকে যায় আর adc সেটি পড়ে। RISC-V-তে চারটি লাগে। দুই limb-এ পার্থক্য ছোট, কিন্তু হাজার limb-এর সংখ্যায় (অংশের শেষে bignum) এই বাড়তি instruction প্রতিটি limb-এ জমে।

মাঝের limb-এ দুটি carry হতে পারে

তিন বা তার বেশি limb হলে মাঝের প্রতিটি limb-এ যোগ হয় তিনটি জিনিস: ai + bi + আগের carry. RISC-V-তে তখন দুই ধাপে যোগ আর দুবার sltu লাগে: প্রথমে s = a + b (carry c1 = s <u a), তারপর r = s + cin (carry c2 = r <u s), আর নতুন carry = c1 OR c2. দুটি একসাথে ১ হতে পারে না (s-এ wrap হলে s ≤ 232 − 2, তাই +1-এ আর wrap হয় না), তাই OR-এর বদলে যোগ করলেও চলে। প্রতি মাঝের limb-এ মোট পাঁচটি instruction, x86-এ একটি adc.

হাতে একটি ১২৮-বিট যোগ, চারটি ৩২-বিট limb-এ (limb ০ সবচেয়ে কম গুরুত্বের):

LimbABcarry-inযোগফল (৩৩ বিট)ফলের limbcarry-out
০765432109000000001_06543210065432101
১FEDCBA988000000011_7EDCBA997EDCBA991
২89ABCDEFF0F0F0F011_7A9CBEE07A9CBEE01
৩012345670F0F0F0F10_10325477103254770

ফল (উপরের limb আগে): 10325477 7A9CBEE0 7EDCBA99 06543210. নিচের C প্রোগ্রাম ঠিক এই যোগটি করে — limb-গুলো uint32_t-এ, carry RISC-V-এর মতো তুলনা দিয়ে — তারপর ফল gcc-র unsigned __int128 (একটি GCC/Clang extension, ৬৪-বিট লক্ষ্যে পাওয়া যায়) দিয়ে করা যোগের সাথে মেলায়। দ্বিতীয় অংশে signed overflow ধরা হয় দুইভাবে: gcc-র __builtin_add_overflow আর অংশ ৩-এর XOR নিয়ম — সব হিসাব unsigned-এ, তাই কোথাও undefined behaviour নেই:

#include <stdio.h>
#include <stdint.h>
#include <limits.h>
typedef unsigned __int128 u128;                     /* gcc extension */
/* r = a + b on 4 x 32-bit limbs (limb 0 = least significant); returns carry */
static uint32_t add128(uint32_t r[4], const uint32_t a[4], const uint32_t b[4]) {
    uint32_t carry = 0;
    for (int i = 0; i < 4; i++) {
        uint32_t s = a[i] + b[i], c1 = s < a[i];     /* wrap, then carry like sltu */
        r[i] = s + carry;
        carry = c1 | (r[i] < s);                     /* second possible carry */
    }
    return carry;
}
static u128 join(const uint32_t x[4]) {
    return ((u128)x[3] << 96) | ((u128)x[2] << 64) | ((u128)x[1] << 32) | x[0];
}
/* sign rule on raw bits: operands agree in sign, the sum does not */
static int xor_rule(uint32_t a, uint32_t b) { uint32_t s = a + b; return (int)(((a ^ s) & (b ^ s)) >> 31); }
int main(void) {
    uint32_t a[4] = {0x76543210, 0xFEDCBA98, 0x89ABCDEF, 0x01234567};
    uint32_t b[4] = {0x90000000, 0x80000000, 0xF0F0F0F0, 0x0F0F0F0F};
    uint32_t m[4] = {~0u, ~0u, ~0u, ~0u}, one[4] = {1, 0, 0, 0}, r[4];
    uint32_t c = add128(r, a, b);
    printf("limbs : %08X %08X %08X %08X  carry %u\n", r[3], r[2], r[1], r[0], c);
    printf("match __int128: %s\n", join(r) == join(a) + join(b) ? "yes" : "no");
    c = add128(r, m, one);
    printf("max+1 : %08X %08X %08X %08X  carry %u\n", r[3], r[2], r[1], r[0], c);
    int t[][2] = {{INT_MAX, 1}, {-1, -1}, {INT_MIN, -1}, {INT_MIN, INT_MIN}, {100, -200}};
    for (int i = 0; i < 5; i++) {
        int s, f = __builtin_add_overflow(t[i][0], t[i][1], &s);
        printf("%11d + %11d: builtin %d, xor rule %d, wrapped %d\n",
               t[i][0], t[i][1], f, xor_rule(t[i][0], t[i][1]), s);
    }
}

অনলাইন runner-এ (gcc, x86-64 Linux, warning চালু) কোনো warning ছাড়া compile হয়ে আউটপুট:

limbs : 10325477 7A9CBEE0 7EDCBA99 06543210  carry 0
match __int128: yes
max+1 : 00000000 00000000 00000000 00000000  carry 1
 2147483647 +           1: builtin 1, xor rule 1, wrapped -2147483648
         -1 +          -1: builtin 0, xor rule 0, wrapped -2
-2147483648 +          -1: builtin 1, xor rule 1, wrapped 2147483647
-2147483648 + -2147483648: builtin 1, xor rule 1, wrapped 0
        100 +        -200: builtin 0, xor rule 0, wrapped -100

প্রথম লাইন উপরের টেবিলের সাথে হুবহু মেলে, আর match __int128: yes বলছে compiler-এর নিজের ১২৮-বিট যোগও একই উত্তর দিয়েছে। max+1 লাইনে 2128 − 1 + 1: carry চারটি limb পেরিয়ে বাইরে চলে গেছে — ফল ০, শেষ carry ১, ঠিক ripple adder-এর মতো, শুধু বিটের বদলে ৩২-বিটের টুকরোয়। নিচের পাঁচ লাইনে builtin আর XOR নিয়ম সবখানে একমত; -1 + -1 আর 100 + -200-এ overflow নেই, বাকি তিনটিতে আছে। x86-64-এ gcc __builtin_add_overflow-কে সাধারণত একটি add আর OF flag পড়া (seto বা jo) দিয়ে বানায়; RISC-V-তে অংশ ৩-এর মতো কয়েকটি তুলনা দিয়ে।

Bignum — সীমাহীন integer কীভাবে হয়

Python-এ 2**1000 লিখলে ৩০২ অঙ্কের সঠিক সংখ্যা আসে; কোনো overflow নেই। এর রহস্য এই limb-এর হিসাবই। Python-এর int, Java-র BigInteger, আর C-র জন্য বিখ্যাত library GMP — সবাই একটি সংখ্যাকে limb-এর array হিসেবে রাখে, সাথে চিহ্ন আর দৈর্ঘ্য। এমন সংখ্যাকে বলে bignum বা arbitrary-precision integer. দুটি বৈশিষ্ট্য মনে রাখার মতো:

  • Limb-এর মাপ: GMP সাধারণত যন্ত্রের পুরো word ব্যবহার করে (৬৪-বিট CPU-তে ৬৪ বিটের limb), আর গতির জন্য যোগ-গুণের ভেতরের লুপ প্রতিটি CPU-র জন্য হাতে লেখা assembly-তে, যেখানে x86-এ adc-এর শিকল চলে। CPython (প্রচলিত Python) সাধারণত ৩০ বিটের “digit” রাখে, প্রতিটি একটি ৩২-বিট শব্দে — ওপরের বাড়তি বিট carry রাখার জায়গা দেয়, আর বহনযোগ্য C কোডে দুটি digit-এর গুণফল ৬৪ বিটে ধরে যায়।
  • খরচ বাড়ে দৈর্ঘ্যের সাথে: n-limb যোগে n ধাপ — O(n). তাই Python-এ বিশাল int-এর হিসাব ছোট int-এর চেয়ে অনেক ধীর, আর অসীম নয়: মেমরি শেষ হলে থামে। গুণের দ্রুত পদ্ধতিগুলো (Karatsuba ইত্যাদি) পর্ব ২১-এর আলোচনার সাথে মিলিয়ে পড়তে পারেন।

৫ · Saturating arithmetic — wrap-এর বদলে সীমায় আটকে রাখা

একটি সাদা-কালো ছবির প্রতিটি pixel ০ (কালো) থেকে ২৫৫ (সাদা) — একটি uint8_t. ছবি উজ্জ্বল করতে প্রতিটি pixel-এ ৬০ যোগ করলেন। ২৫০ মানের প্রায়-সাদা pixel-এ 250 + 60 = 310, mod 256-এ 54 — হঠাৎ গাঢ় ধূসর! ছবিতে উজ্জ্বল জায়গায় কালো ছোপ পড়বে। এখানে wrap সম্পূর্ণ ভুল আচরণ; ঠিক আচরণ হলো ২৫৫-এ আটকে যাওয়া। একে বলে saturating arithmetic: ফল সীমা ছাড়ালে সর্বোচ্চ মান, নিচে নামলে সর্বনিম্ন মান।

পানির ট্যাংক ভরে গেলে উপচে পড়ে — কিন্তু ট্যাংক খালি হয়ে যায় না! Wrap-এর আচরণ যেন “ট্যাংক উপচালেই খালি হয়ে আবার শুরু” — শব্দ, ছবি বা সেন্সরের মানের জন্য এটি অর্থহীন। Saturation হলো আসল ট্যাংকের মতো: সর্বোচ্চে আটকে থাকা।

নিয়ম দুই রকম: unsigned ৮ বিটে সীমা [0, 255], signed ৮ বিটে [−128, 127]. সীমা চেনার উপায় আগের অংশগুলো থেকেই আসে — unsigned যোগে carry হলে ২৫৫, unsigned বিয়োগে borrow হলে ০; signed-এ V = 1 হলে operand-এর চিহ্ন দেখে ১২৭ বা −১২৮. নিচের simulator ৮ বিটের যোগ-বিয়োগ wrap আর saturate দুইভাবে করে, তারপর একটি সারির pixel উজ্জ্বল করে:

// 8-bit wrapping vs saturating arithmetic (signed: -128..127, unsigned: 0..255)
const wrapU = x => ((x % 256) + 256) % 256;
const wrapS = x => { const w = wrapU(x); return w > 127 ? w - 256 : w; };
const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));
const rows = [                     // [kind, a, op, b]
  ["signed", 120, "+", 10], ["signed", -120, "-", 10], ["signed", -100, "+", -50],
  ["signed", 100, "+", 27], ["unsigned", 250, "+", 10], ["unsigned", 5, "-", 10],
  ["unsigned", 200, "-", 50],
];
console.log("kind      expr          exact   wrap    saturate");
for (const [kind, a, op, b] of rows) {
  const exact = op === "+" ? a + b : a - b;
  const w = kind === "signed" ? wrapS(exact) : wrapU(exact);
  const sat = kind === "signed" ? clamp(exact, -128, 127) : clamp(exact, 0, 255);
  console.log(`${kind.padEnd(9)} ${`${a} ${op} ${b}`.padEnd(13)} ${String(exact).padStart(5)}` +
              ` ${String(w).padStart(6)} ${String(sat).padStart(8)}`);
}
// brightness +60 on a row of 8-bit grey pixels (0 = black, 255 = white)
const px = [10, 90, 180, 200, 230, 250];
const fmt = a => a.map(v => String(v).padStart(4)).join("");
console.log("\npixels       :" + fmt(px));
console.log("+60 wrapping :" + fmt(px.map(v => wrapU(v + 60))));
console.log("+60 saturate :" + fmt(px.map(v => clamp(v + 60, 0, 255))));
// associativity is lost: (a + b) - b is no longer a
const a = 120, b = 10;
console.log(`\nsigned (${a} + ${b}) - ${b}: wrap = ${wrapS(wrapS(a + b) - b)}, ` +
            `saturate = ${clamp(clamp(a + b, -128, 127) - b, -128, 127)}`);

আউটপুট:

kind      expr          exact   wrap    saturate
signed    120 + 10        130   -126      127
signed    -120 - 10      -130    126     -128
signed    -100 + -50     -150    106     -128
signed    100 + 27        127    127      127
unsigned  250 + 10        260      4      255
unsigned  5 - 10           -5    251        0
unsigned  200 - 50        150    150      150

pixels       :  10  90 180 200 230 250
+60 wrapping :  70 150 240   4  34  54
+60 saturate :  70 150 240 255 255 255

signed (120 + 10) - 10: wrap = 120, saturate = 117

প্রথম চার সারি signed: 120 + 10-এ wrap দেয় −126 (ধনাত্মক যোগ করে বড় ঋণাত্মক!), saturate দেয় 127. 100 + 27 = 127 ঠিক সীমায়, তাই দুটিই একই। Unsigned-এ 250 + 10 → wrap 4, saturate 255; 5 − 10 → wrap 251, saturate 0. Pixel সারিতে মূল কথা: wrap-এ ২০০, ২৩০, ২৫০-এর উজ্জ্বল pixel হয়ে গেছে 4, 34, 54 — প্রায় কালো; saturate-এ সব 255. শেষ লাইনটি সাবধানবাণী: saturation-এ (a + b) − b আর a সমান নয় — 120 + 10 আটকে গেল 127-এ, তারপর −10 দিয়ে 117. Wrap-এর mod 28 গণিতে উল্টো দিকে ফিরে আসা যায়, saturation-এ তথ্য হারায়। তাই সাধারণ integer-এর জন্য নয়, শুধু সেইসব মানের জন্য saturation, যেখানে “সর্বোচ্চে আটকে থাকা” বাস্তব অর্থ বহন করে।

কোথায় ব্যবহার হয়? শব্দ (audio) প্রক্রিয়ায় — wrap হলে বিকট “ক্লিক” শোনা যায়, saturation-এ শুধু হালকা clipping; ছবি আর ভিডিওতে; আর DSP chip-এ, যেখানে saturating যোগ বহু দশক ধরে একটি মৌলিক instruction. এর সবচেয়ে বড় ব্যবহার SIMD-এ: x86-এর SSE2-তে paddusb (unsigned saturate) আর paddsb (signed saturate) একসাথে ১৬টি byte যোগ করে; ARM-এর NEON-এ uqadd/sqadd; RISC-V Vector extension-এ vsaddu/vsadd. ARM-এর ৩২-বিট DSP extension-এ scalar qadd-ও আছে। RISC-V-এর base ISA-তে scalar saturating instruction নেই — software-এ তুলনা আর শাখা দিয়ে করতে হয়। SIMD-এর পুরো গল্প পর্ব ৪২-এ (SIMD আর vector).

৬ · Signed বনাম unsigned তুলনা — blt বনাম bltu আর −1-এর ফাঁদ

পর্ব ১৩-এর অংশ ৭-এ দেখেছেন, একই বিয়োগ থেকে signed তুলনা পড়ে N ⊕ V, আর unsigned তুলনা পড়ে borrow; RISC-V-তে এরা আলাদা instruction: blt/bge (signed) আর bltu/bgeu (unsigned). কোনটি বসবে, তা ঠিক করে compiler — C-র type দেখে। আর এখানেই একটি পুরোনো, বিপজ্জনক বাগের জন্ম।

/* উদাহরণ — একটি পরিচিত বাগ */
int i = -1;
size_t n = 10;                  /* sizeof, strlen() ইত্যাদির type */
if (i < n) puts("in range");    /* ছাপে না! */
else       puts("out of range");

C-র usual arithmetic conversions (পর্ব ০৫-এর অংশ ৮) অনুযায়ী signed আর unsigned (সমান বা বড় মাপের) তুলনায় signed-টি unsigned-এ রূপান্তরিত হয়। −1 হয়ে যায় 0xFFFFFFFF… — অর্থাৎ সবচেয়ে বড় সংখ্যা। Compiler তাই বিয়োগ বা signed তুলনা নয়, unsigned তুলনা বসায়:

# উদাহরণ — RV32 (size_t = 32 বিট unsigned), i in a0, n in a1
bltu  a0, a1, in_range       # 0xFFFFFFFF <u 10 ?  false
# signed হলে হতো:  blt a0, a1, in_range   # -1 < 10 ?  true

Hardware কোনো ভুল করেনি; ভুল হয়েছে type-এ। gcc -Wall -Wextra এমন তুলনায় -Wsign-compare warning দেয় — এজন্যই এই কোর্সের প্রতিটি C কোড warning চালু রেখে চালানো হয়। বিপজ্জনক দিকটি উল্টো ক্ষেত্রে: একটি signed দৈর্ঘ্য len যদি “len > MAX হলে বাতিল” পরীক্ষা পেরিয়ে যায় (−1 তো MAX-এর চেয়ে ছোট!), তারপর memcpy(buf, src, len)-এ গিয়ে size_t হয়ে প্রায় ৪৩০ কোটি byte-এর (৩২-বিট size_t-এ; ৬৪-বিটে আরও অনেক বেশি) কপি চায় — buffer overflow. নিরাপদ নিয়ম: দৈর্ঘ্য আর index-এর জন্য শুরু থেকেই size_t, আর বাইরে থেকে আসা signed মানে আগে len < 0 যাচাই।

কৌশল হিসেবেও কাজে লাগে। Array-র সীমা যাচাই 0 <= i && i < n-এ দুটি তুলনা লাগে। কিন্তু i-কে unsigned ধরে (unsigned)i < (unsigned)n একটি bltu-তেই দুটি কাজ করে — ঋণাত্মক i unsigned-এ বিশাল হয়ে যায়, তাই আপনা থেকেই বাদ পড়ে (n অঋণাত্মক হলে)। Java-র মতো ভাষার JIT compiler আর Linux kernel-এর মতো কোড এই কৌশল ব্যবহার করে বলে জানা যায়। একই বিট, একই তুলনা — জেনে করলে কৌশল, না জেনে হলে বাগ।

৭ · Shift দিয়ে গুণ-ভাগ — sra-র floor আর C-র truncation

পর্ব ১৩-এর অংশ ৮-এ barrel shifter-এর circuit দেখেছেন। এবার shift-কে গণিত হিসেবে দেখা যাক, কারণ compiler গুণ-ভাগের বদলে প্রায়ই shift বসায় — গুণ কয়েক cycle নেয়, ভাগ আরও অনেক বেশি, আর shift সাধারণত এক cycle. ব্যয়বহুল কাজকে সস্তা কাজে বদলানোর এই কৌশলকে compiler-এর ভাষায় বলে strength reduction.

  • sll k = ×2k: signed আর unsigned দুটিতেই, mod 232-এ। বাঁয়ের বিট বেরিয়ে গেলে ফল wrap করে — গুণের overflow-এর মতোই। (C-তে ঋণাত্মক signed সংখ্যাকে << করা আবার undefined behaviour; unsigned-এ করাই নিরাপদ।)
  • srl k = unsigned ÷ 2k: ভাগফল নিচের দিকে গোল — unsigned-এ যা C-র /-এর সাথে হুবহু এক।
  • sra k = floor(x ÷ 2k): arithmetic shift সবসময় −∞-এর দিকে গোল করে। ধনাত্মক x-এ এটি C-র ভাগের সাথে এক, কিন্তু ঋণাত্মক x-এ নয়: −7 sra 2 = −2, অথচ C-তে −7 / 4 = −1.

কারণ C (C99 থেকে নির্দিষ্টভাবে) ভাগফলকে শূন্যের দিকে গোল করে — একে বলে truncating division. −1.75 শূন্যের দিকে গোল করলে −1, −∞-এর দিকে করলে −2. ঋণাত্মক সংখ্যায় শূন্যের দিকে গোল করা মানে ওপরের দিকে গোল করা (ceiling). তাই compiler-কে sra-র আগে একটি সংশোধন বসাতে হয়:

x / 2k (C) = (x + ((x >> 31) & (2k − 1))) >> k

x >> 31 (arithmetic) ঋণাত্মক x-এ সব বিট ১ (−1), অঋণাত্মকে ০. তার সাথে & (2k − 1) দিলে bias = ঋণাত্মক x-এ 2k − 1, নাহলে ০. প্রমাণ: m = 2k আর x = qm + r (0 ≤ r < m) লিখুন। r = 0 হলে (x + m − 1) ÷ m-এর floor = q + floor((m − 1)/m) = q. আর r > 0 হলে = q + 1 + floor((r − 1)/m) = q + 1. দুই ক্ষেত্রেই এটি ceiling(x/m) — ঋণাত্মক x-এর জন্য ঠিক যা truncation চায়। ধনাত্মক x-এ bias ০, তাই সাধারণ floor-ই থাকে।

লক্ষ্যC-তে −7 / 4
x = -7 = 0xFFFFFFF9 = 1111 ... 1111 1001
-7 / 4 = -1.75
C wants  -1   (truncate toward zero)
floor is -2
Bias ছাড়াsrai a0, a0, 2
1111 ... 1111 1001   >> 2 (sign bits enter)
1111 ... 1111 1110 = 0xFFFFFFFE = -2
= floor(-1.75)   -> WRONG for C
Bias বানানোsrai t0, a0, 31 · srli t0, t0, 30
srai t0, a0, 31 : t0 = 0xFFFFFFFF   (x < 0, all sign bits)
srli t0, t0, 30 : t0 = 0x00000003 = 3 = 2^2 - 1
Bias যোগadd a0, a0, t0
a0 = -7 + 3 = -4 = 0xFFFFFFFC = 1111 ... 1111 1100
Shiftsrai a0, a0, 2
1111 ... 1111 1100 >> 2
1111 ... 1111 1111 = 0xFFFFFFFF = -1   = C's -7 / 4   ok
ধনাত্মক xx = 7
srai t0, a0, 31 : t0 = 0
srli t0, t0, 30 : bias = 0
7 + 0 = 7,  7 >> 2 = 1 = 7 / 4   ok  (fix-up changes nothing)
  1. লক্ষ্য ঠিক করা। −7-এর ৩২-বিট রূপ 0xFFFFFFF9. গাণিতিক ভাগফল −1.75. C চায় শূন্যের দিকে গোল, অর্থাৎ −1; −∞-এর দিকে গোল করলে আসত −2.
  2. শুধু sra. দুই ঘর ডানে সরালে বাঁয়ে sign-বিট ১ ঢোকে, ফল 0xFFFFFFFE = −2 — এটি floor, C-র উত্তর নয়। শুধু srai বসালে compiler ভুল কোড বানাত।
  3. Bias বানানো। srai t0, a0, 31 sign-বিটকে ৩২ ঘরে ছড়িয়ে দেয়: ঋণাত্মক x-এ 0xFFFFFFFF. তারপর srli t0, t0, 30 বাঁয়ে ৩০টি ০ এনে রাখে শুধু নিচের দুটি ১ — মান 3 = 22 − 1. এটি & 3-এর সমান, শুধু ভিন্ন instruction-এ।
  4. Bias যোগ। −7 + 3 = −4 = 0xFFFFFFFC. এখন সংখ্যাটি 4-এর গুণিতক, তাই এর floor ভাগ আর truncation একই উত্তর দেবে।
  5. এবার shift. −4 sra 2 = −1 = 0xFFFFFFFF — C-র −7 / 4-এর সাথে মেলে। (x + 2k − 1)-এর floor আসলে x/2k-এর ceiling, আর ঋণাত্মক সংখ্যায় ceiling-ই truncation.
  6. ধনাত্মক x-এ কোনো ক্ষতি নেই। x = 7 হলে sign-বিট ০, bias ০, 7 sra 2 = 1 = 7 / 4. একই চারটি instruction সব x-এর জন্য — কোনো শাখা ছাড়াই।

বাস্তব compiler কী বানায়? int f(int x) { return x / 4; }-এর জন্য RV32-এ gcc -O2 সাধারণত ঠিক stepper-এর চারটি instruction দেয়, আর x86-64-এ gcc সাধারণত একটি lea দিয়ে x + 3 বানিয়ে cmovns দিয়ে বেছে নেয় (version ও flag-ভেদে সামান্য ভিন্ন হতে পারে; Compiler Explorer-এ নিজে দেখে নিতে পারেন):

# উদাহরণ — int x / 4, gcc -O2 (প্রচলিত রূপ; x86-64 অংশ Intel syntax-এ)
# RISC-V RV32               # x86-64
srai  a5, a0, 31            lea    eax, [rdi+3]     # x + 3
srli  a5, a5, 30            test   edi, edi
add   a0, a5, a0            cmovns eax, edi         # x >= 0 ? x : x + 3
srai  a0, a0, 2             sar    eax, 2
ret                         ret
# unsigned x / 4 হলে দুটিতেই শুধু একটি srli / shr.

নিচের C প্রোগ্রাম কয়েকটি মানে /, >> আর bias কৌশল পাশাপাশি দেখায়, তারপর −১,০০,০০০ থেকে +১,০০,০০০ পর্যন্ত প্রতিটি x-এ k = 1…4-এর জন্য মিলিয়ে গোনে কতবার অমিল হয়। (C-তে ঋণাত্মক signed সংখ্যার >> implementation-defined; gcc নথিভুক্তভাবে একে arithmetic shift হিসেবে করে।)

#include <stdio.h>
#include <stdint.h>
/* gcc defines >> on a negative int as an arithmetic shift (sra/sar) */
static int32_t sra(int32_t x, int k)  { return x >> k; }
static int32_t biased(int32_t x, int k) {           /* what compilers emit */
    int32_t bias = (x >> 31) & ((1 << k) - 1);       /* 2^k - 1 if x < 0, else 0 */
    return (x + bias) >> k;
}
int main(void) {
    int32_t xs[] = {7, -7, -8, -9, -1, -2147483647 - 1};
    printf("          x        x/4       x>>2     biased  x%%4\n");
    for (int i = 0; i < 6; i++) {
        int32_t x = xs[i];
        printf("%11d %10d %10d %10d %5d\n", x, x / 4, sra(x, 2), biased(x, 2), x % 4);
    }
    for (int k = 1; k <= 4; k++) {
        long bad_sra = 0, bad_bias = 0, d = 1L << k;
        for (int32_t x = -100000; x <= 100000; x++) {
            if (sra(x, k) != x / d) bad_sra++;
            if (biased(x, k) != x / d) bad_bias++;
        }
        printf("k=%d (divide by %2ld): x>>k wrong %6ld times, biased wrong %ld\n",
               k, d, bad_sra, bad_bias);
    }
    return 0;
}

আউটপুট:

          x        x/4       x>>2     biased  x%4
          7          1          1          1     3
         -7         -1         -2         -1    -3
         -8         -2         -2         -2     0
         -9         -2         -3         -2    -1
         -1          0         -1          0    -1
-2147483648 -536870912 -536870912 -536870912     0
k=1 (divide by  2): x>>k wrong  50000 times, biased wrong 0
k=2 (divide by  4): x>>k wrong  75000 times, biased wrong 0
k=3 (divide by  8): x>>k wrong  87500 times, biased wrong 0
k=4 (divide by 16): x>>k wrong  93750 times, biased wrong 0

−8 (4-এর গুণিতক) ছাড়া প্রতিটি ঋণাত্মক সারিতে x>>2 এক কম: −7 → −2, −9 → −3, আর সবচেয়ে মজার −1 → −1 (C-তে −1 / 4 = 0). Bias কলাম সবখানে x/4-এর সাথে মেলে, INT_MIN-সহ। অমিলের সংখ্যাও হিসাবমতো: ১,০০,০০০টি ঋণাত্মক সংখ্যার মধ্যে যেগুলো 2k-এর গুণিতক নয়, শুধু সেগুলোতে ভুল — k = 2-এ তিন-চতুর্থাংশ, অর্থাৎ ৭৫,০০০. Bias কৌশলে একবারও ভুল নেই। শেষ কলামে x%4-এর চিহ্ন লক্ষ করুন — C-তে ভাগশেষ ভাজ্যের চিহ্ন পায় (−7 % 4 = −3); পরের অংশে এটিই বিষয়।

৮ · ভাগের নিয়ম এক নজরে — C, Python আর hardware

ভাগের circuit (restoring, non-restoring) পর্ব ২২-এ (ভাগ)। এখানে শুধু “ফল কী হওয়া উচিত” — কারণ ভাষাভেদে উত্তর ভিন্ন, আর এক ভাষা থেকে আরেক ভাষায় কোড আনার সময় এখানেই বাগ ঢোকে। সব নিয়মে একটি অভেদ সত্য থাকে: a = (a / b) × b + (a % b). ভাগফল কোন দিকে গোল হয়, তা থেকেই ভাগশেষের চিহ্ন ঠিক হয়।

হিসাবC, C++, Java, Rust, RISC-V div/rem, x86 idiv (truncation)Python // আর % (floor)
−7 ÷ 2ভাগফল −3, ভাগশেষ −1ভাগফল −4, ভাগশেষ 1
7 ÷ −2ভাগফল −3, ভাগশেষ 1ভাগফল −4, ভাগশেষ −1
−7 ÷ 4ভাগফল −1, ভাগশেষ −3ভাগফল −2, ভাগশেষ 1
ভাগশেষের চিহ্নভাজ্যের (a) মতোভাজকের (b) মতো

Python-এর পদ্ধতিকে বলে floor division, আর এতে একটি সুবিধা আছে: x % n সবসময় 0 থেকে n − 1 (n ধনাত্মক হলে) — ঘড়ি, সপ্তাহের দিন বা circular buffer-এর index-এর জন্য ঠিক যা দরকার। C-তে (-1) % 7 হয় −1, তাই “গতকাল কোন বার” হিসাবে ((d - 1) % 7 + 7) % 7 লিখতে হয়। Python-এ (d - 1) % 7-ই যথেষ্ট। লক্ষ করুন, C-তে signed ভাগের shift কৌশলের মতো %-এর জন্যও compiler bias বসায়; Python-এর // সরাসরি sra-র সাথে মেলে।

দুটি বিশেষ ক্ষেত্র, যেখানে ISA-গুলোও আলাদা আচরণ করে:

  • শূন্য দিয়ে ভাগ: x86-এর div/idiv একটি divide-error exception তোলে (Linux-এ প্রোগ্রাম সাধারণত SIGFPE পেয়ে বন্ধ হয়)। RISC-V কোনো trap করে না: ভাগফলের সব বিট ১ আর ভাগশেষ = ভাজ্য — শাখা দিয়ে যাচাই software-এর কাজ। ARM64-এ ফল ০, trap নেই। C-তে এটি undefined behaviour.
  • INT_MIN / −1: আসল উত্তর +231, যা ৩২ বিটে ধরে না — ভাগেও overflow হয়! x86-এ আবার divide-error; RISC-V-তে ভাগফল INT_MIN (ভাজ্য নিজেই) আর ভাগশেষ ০, কোনো trap নেই। C-তে undefined behaviour.

৯ · বাস্তবে overflow ঠেকানো — checked arithmetic, ভাষার সুবিধা আর লুপের সীমা

Overflow-এর বিখ্যাত দুর্ঘটনাগুলো — Ariane 5 রকেট, YouTube-এর view counter, Year 2038 — পর্ব ০৫-এর অংশ ৯-এ আছে। এখানে প্রশ্ন: আজকের প্রোগ্রামার কী করেন? উত্তর তিন স্তরে।

১. Checked arithmetic — হিসাবের সাথে overflow-এর খবর

যে যোগে overflow হলে ক্ষতি (টাকার অঙ্ক, buffer-এর মাপ, malloc-এর আকার), সেখানে checked arithmetic ব্যবহার করুন — হিসাব আর “overflow হলো কি না” একসাথে।

__builtin_add_overflow(a, b, &r), __builtin_sub_overflow, __builtin_mul_overflow — overflow হলে true ফেরত দেয়, আর r-এ wrap করা ফল রাখে; operand-এর type যা-ই হোক, গাণিতিকভাবে সঠিক তুলনা করে। C23 মানদণ্ডে একই কাজ <stdckdint.h>-এর ckd_add, ckd_sub, ckd_mul দিয়ে। x86-এ এগুলো সাধারণত একটি add + jo/jc-এ পরিণত হয় — প্রায় বিনা খরচে।

প্রতিটি integer type-এ চার রকম method: checked_add (overflow হলে None), wrapping_add (mod 2n), saturating_add (সীমায় আটকায়), overflowing_add (ফল আর একটি bool). সাধারণ +: debug build-এ overflow হলে প্রোগ্রাম panic করে থামে; release build-এ ডিফল্ট হিসেবে wrap করে (overflow-checks সেটিং দিয়ে release-এও যাচাই চালু করা যায়)। অর্থাৎ ভাষাটি প্রোগ্রামারকে স্পষ্ট করে বলতে বাধ্য করে — কোন আচরণ চান।

Java-তে int নিয়মমতো wrap করে, আর Math.addExact, Math.multiplyExact overflow হলে ArithmeticException ছোড়ে। Python-এর int-এ overflow নেই (bignum, অংশ ৪), তবে NumPy-র fixed-width array (যেমন int32) আবার wrap করে। Swift-এ সাধারণ + overflow হলে প্রোগ্রাম থামায়, আর wrap চাইলে আলাদা operator &+.

২. Compiler আর sanitizer দিয়ে পাহারা

C-তে signed overflow undefined behaviour, আর optimizer সেই ধারণায় overflow-যাচাই মুছে দিতে পারে (পর্ব ০৫-এর অংশ ৮-এ উদাহরণ)। পরীক্ষার সময় কাজে লাগে UBSan: -fsanitize=signed-integer-overflow (বা পুরো -fsanitize=undefined) দিয়ে compile করলে চলার সময় overflow ঘটলেই কোন লাইনে, তা জানায়। GCC-র পুরোনো -ftrapv signed যোগ-বিয়োগ-গুণে overflow হলে প্রোগ্রাম থামানোর কথা, কিন্তু বিভিন্ন version-এ এটি অসম্পূর্ণ বা অনির্ভরযোগ্য বলে পরিচিত; আজকাল সাধারণত sanitizer-কেই (দরকারে -fsanitize-trap-সহ) বেশি ভরসাযোগ্য ধরা হয়। আর -fwrapv উল্টো কাজ করে — signed overflow-কে নিয়মমতো wrap বানিয়ে দেয়, ফলে UB আর থাকে না (কিন্তু ভুল ফল ঠিকও হয় না)।

৩. লুপ আর সীমার পরিচিত ফাঁদ

  • Unsigned উল্টো গণনা: for (unsigned i = n - 1; i >= 0; i--) কখনো থামে না — unsigned i কখনো ঋণাত্মক হয় না; 0-এর পর 4294967295. নিরাপদ রূপ: for (size_t i = n; i-- > 0; ).
  • n − 1 যখন n = 0: for (size_t i = 0; i < n - 1; i++)-এ n = 0 হলে n − 1 হয়ে যায় সবচেয়ে বড় size_t — লুপ কার্যত অসীম, array-র বাইরে পড়া। লিখুন i + 1 < n.
  • মাঝের index: binary search-এ mid = (lo + hi) / 2-এ lo + hi signed সীমা ছাড়াতে পারে। ২০০৬ সালে Joshua Bloch একটি বহুল-পঠিত লেখায় দেখান, Java-র standard library-র binary search-সহ বহু বাস্তবায়নে ঠিক এই বাগ বহু বছর লুকিয়ে ছিল — দেখা দেয় শুধু প্রায় ১০৭ কোটির (230) বেশি উপাদানের array-তে। সমাধান: lo + (hi - lo) / 2.
  • গুণে আকার: malloc(count * size)-এ গুণফল wrap করলে ছোট buffer বরাদ্দ হয়, পরে বড় লেখা — নিরাপত্তা-ত্রুটির পরিচিত উৎস। calloc(count, size) বা __builtin_mul_overflow ব্যবহার করুন।

দশমিক হিসাব — BCD আর x86-এর DAA

টাকা-পয়সার হিসাবে binary-র বদলে দশমিক অঙ্কেই হিসাব রাখার একটি পুরোনো ধারা আছে: BCD, যেখানে প্রতিটি দশমিক অঙ্ক ৪ বিটে (পর্ব ০৪-এর অংশ ৯), আর যোগফল ৯ ছাড়ালে +6 সংশোধন (পর্ব ১১-এর BCD adder). 8080 আর 8086 যুগের CPU এই সংশোধন একটি instruction-এ করত: x86-এর DAA (Decimal Adjust after Addition) সাধারণ binary add-এর পর AL register-এর দুটি packed BCD অঙ্ক ঠিক করে দেয়, auxiliary carry flag (AF — নিচের nibble থেকে carry) দেখে। যেমন BCD 0x38 + 0x45: binary যোগফল 0x7D, নিচের nibble D > 9, তাই +6 → 0x83 — অর্থাৎ 38 + 45 = 83. এই পরিবারের (DAA, DAS, AAA, AAS, AAM, AAD) instruction ৬৪-বিট মোডে আর বৈধ নয়। দশমিক hardware অবশ্য হারিয়ে যায়নি: IBM-এর mainframe-এ packed decimal instruction আজও ব্যাংক আর বিমার হিসাবে চলে, আর কিছু IBM POWER প্রসেসরে decimal floating-point unit আছে।

আরও গভীরে: RISC-V-তে multi-precision যোগে প্রতি limb-এ বাড়তি instruction লাগে — এটি নিয়ে cryptography আর bignum library-র লেখকদের মধ্যে আলোচনা আছে, আর বড় out-of-order core হয়তো add + sltu-এর মতো জোড়াকে ভেতরে এক micro-op-এ মিলিয়ে (macro-op fusion) খরচ কমাতে পারে, তবে এটি নকশাভেদে ভিন্ন। উল্টো দিকে x86 আরও এগিয়েছে: Intel-এর ADX extension (মোটামুটি ২০১৪-র Broadwell প্রজন্ম থেকে) দুটি আলাদা carry শিকল চালানোর জন্য adcx (CF ব্যবহার করে) আর adox (OF-কে carry হিসেবে ব্যবহার করে) এনেছে, যাতে বড় গুণের দুটি যোগ-শিকল একসাথে চলে। আর flag-ওয়ালা core-কে flag-ও rename করতে হয় — যা পর্ব ৪০-এ register renaming-এর আলোচনায় আসবে।

১০ · AI-এর যুগে integer হিসাব

AI টুল overflow-এর সংজ্ঞা ভালো বলে, কিন্তু ISA-র খুঁটিনাটিতে আর সীমানার মানে প্রায়ই হোঁচট খায়। প্রচলিত ভুলগুলো:

AI-এর প্রচলিত ভুলঠিক কথাকীভাবে যাচাই
“RISC-V-তে adc বা carry flag আছে”, বা MIPS-এর add-এর trap RISC-V-তেও আছেRV32I/RV64I-এ flag নেই, overflow trap নেই; carry sltu দিয়েRISC-V unprivileged spec-এর instruction তালিকা
“addu মানে unsigned যোগ”বিট একই; পার্থক্য শুধু trap না হওয়ায়MIPS manual-এ ADD আর ADDU-এর বিবরণ পাশাপাশি
৬৪-বিট যোগে sltu t0, a0, a0 বা ভুল register-এর সাথে তুলনানতুন যোগফলকে একটি অপরিবর্তিত operand-এর সাথে তুলনা0xFFFFFFF0 + 0x20 হাতে বা simulator-এ
“x / 4 = x >> 2” সব int-এর জন্যঋণাত্মক x-এ শুধু floor; C-র জন্য bias লাগেx = −7, −1 দিয়ে চালিয়ে দেখা
Python আর C-র % একই ধরে নেওয়াC: ভাজ্যের চিহ্ন; Python: ভাজকের চিহ্নদুই ভাষায় -7 % 4
Overflow যাচাই if (a + b < a) signed int-এSigned-এ UB, optimizer মুছে দিতে পারে; unsigned-এ বা builtin দিয়ে-O2-এ assembly দেখা, UBSan চালানো

AI-কে এই বিষয়ে প্রশ্ন করলে ISA, width আর প্রতিটি মধ্যবর্তী মান চেয়ে নিন, আর নিজে চালানোর মতো পরীক্ষা চান:

আমি RV32I (RISC-V, ৩২ বিট) শিখছি। মনে রেখো RISC-V-তে কোনো flag register বা overflow trap নেই। ১. t0 = t1 + t2-এর পর unsigned carry আর signed overflow ধরার instruction sequence দাও, প্রতিটি instruction-এর পর register-এর মান hex-এ দেখিয়ে — t1 = 0x7FFFFFFF, t2 = 0x00000001 আর t1 = t2 = 0xFFFFFFFF দুই ক্ষেত্রে। ২. a1:a0 + a3:a2 (৬৪-বিট যোগ) RV32I-এ লেখো, আর বলো sltu কোন register-এর সাথে তুলনা করছে ও কেন। ৩. int x / 8 (C, truncation)-এর জন্য shift-ভিত্তিক কোড দাও, আর x = -9 দিয়ে প্রতিটি ধাপ দেখাও। ৪. শেষে সব নিয়ম BigInt বা Python দিয়ে কয়েক লাখ এলোমেলো মানে মেলানোর একটি ছোট স্ক্রিপ্ট দাও — আমি নিজে চালিয়ে দেখব।

AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Carry আর overflow-এর পার্থক্য? — Carry: unsigned ফল সীমার বাইরে (MSB থেকে carry); overflow: signed ফল সীমার বাইরে (V = cn ⊕ cn−1).
  • MIPS-এ add আর addu-এর পার্থক্য? — ফলের বিট একই; add signed overflow-এ trap করে, addu করে না।
  • Flag ছাড়া RISC-V-তে ৬৪-বিট যোগ কীভাবে? — নিচের অর্ধেক যোগ, sltu দিয়ে carry, উপরের অর্ধেক যোগ, তারপর carry যোগ — চারটি instruction.
  • −7 >> 1 আর −7 / 2 (C) কত? — −4 আর −3; sra floor করে, C truncate করে।
  • Saturating arithmetic কোথায় লাগে? — ছবি, শব্দ, DSP আর SIMD-এ; ৮-বিট unsigned-এ 250 + 10 = 255.

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১MIPS overflow-এ trap করত, RISC-V কিছুই করে না। নতুন ISA কেন “নিরাপত্তা” কমিয়ে দিল — এটি কি ভুল সিদ্ধান্ত?

১৯৮০-র দশকে MIPS যখন নকশা হয়, তখন ধারণা ছিল ভাষাগুলো overflow ধরতে চাইবে, আর hardware-এ বিনা খরচে ধরা গেলে software-এর বাড়তি instruction বাঁচবে। তাই add trap করত, আর যারা wrap চায় তাদের জন্য addu. বাস্তবে যা ঘটল তা উল্টো: সবচেয়ে বেশি ব্যবহৃত ভাষা C আর C++ overflow-এ trap চায় না, তাই compiler প্রায় সবখানে addu বসাত। Trapping add হয়ে রইল একটি বিরল instruction, যার জন্য CPU-কে তবু প্রতিটি যোগে exception-এর পথ প্রস্তুত রাখতে হতো।

Trap-এর একটি লুকোনো খরচ আছে। Pipeline-এ (পর্ব ৩৫–৩৮) একটি instruction exception তুললে তার পরের কোনো instruction-এর ফল যেন দৃশ্যমান না হয় — এই “precise exception”-এর নিশ্চয়তা দিতে hardware-কে সতর্ক থাকতে হয়। যত বেশি instruction exception তুলতে পারে, তত বেশি জায়গায় এই সতর্কতা। RISC-V-র নকশাকাররা (Berkeley-তে, ২০১০ সালের দিকে শুরু) বলেছিলেন, base ISA যত ছোট আর নিয়মিত হবে, ছোট microcontroller থেকে বড় server chip পর্যন্ত সবার জন্য তত ভালো; overflow যাচাই যেখানে লাগে, সেখানে একটি শাখা প্রায়ই যথেষ্ট — অংশ ৩-এ যেমন দেখেছেন, unsigned-এ এক instruction, signed-এ সাধারণত তিনটি।

সমালোচনাও আছে। Rust-এর মতো ভাষা debug build-এ প্রতিটি যোগে overflow যাচাই করে; Swift সবসময় করে। এমন কোডে প্রতিটি যোগের পর বাড়তি instruction কোডকে বড় করে, আর x86-এ যেখানে একটি jo লাগে, RISC-V-তে সেখানে বেশি লাগে। একইভাবে bignum আর cryptography-র কোডে প্রতি limb-এ বাড়তি sltu আর add. RISC-V-র সমর্থকদের উত্তর: বড় core এমন জোড়া instruction ভেতরে মিলিয়ে (macro-op fusion) চালাতে পারে, আর দরকার হলে ঐচ্ছিক extension যোগ করার পথ খোলা — ISA-টি মডিউলার।

তাহলে “নিরাপত্তা কমল” বলা ঠিক নয়; দায়িত্ব সরল hardware থেকে compiler আর ভাষার দিকে। Java, Rust, Swift-এর মতো ভাষা নিজেই ঠিক করে কোথায় যাচাই, আর C-তে __builtin_add_overflow সেই একই sequence বানায়। যে ভাষা যাচাই চায় না, সে কোনো খরচও দেয় না।

মূল উপলব্ধি: ISA-র একটি বৈশিষ্ট্য তখনই মূল্যবান, যখন compiler আর ভাষা সেটি সত্যিই ব্যবহার করে। MIPS-এর trap ব্যবহার না হওয়াই RISC-V-র সিদ্ধান্তের বড় যুক্তি; আর তার দাম — overflow-যাচাই-ভারী কোডে বাড়তি instruction — জেনেশুনে দেওয়া।

প্র ০২RSA-র ২০৪৮-বিট সংখ্যা ৬৪-বিট CPU কীভাবে সামলায়? বড় সংখ্যার হিসাবে hardware কী সাহায্য দেয়?

RSA-র মতো public-key cryptography-তে ২০৪৮ বা তার বেশি বিটের সংখ্যার গুণ আর ভাগশেষ লাগে। ৬৪-বিট CPU-তে ২০৪৮ বিট মানে ৩২টি limb. দুটি এমন সংখ্যার “স্কুলের পদ্ধতিতে” গুণে 32 × 32 = ১,০২৪টি ৬৪×৬৪-বিট গুণ, আর প্রতিটির ১২৮-বিট ফলকে সঠিক জায়গায় যোগ করতে carry-র লম্বা শিকল। তাই bignum library-র গতি নির্ভর করে দুটি জিনিসের ওপর: পূর্ণ ১২৮-বিট গুণফল দেওয়া instruction (x86-64-এ mul RDX:RAX-এ দেয়, ARM64-এ mul + umulh, RISC-V-তে M extension-এর mul + mulhu) আর carry-সহ যোগ।

GMP-এর মতো library এজন্য প্রতিটি জনপ্রিয় CPU-র জন্য আলাদা হাতে-লেখা assembly রাখে। x86-এ ভেতরের লুপ মূলত mul আর adc-এর ঘন বিন্যাস। কিন্তু একটি সমস্যা ছিল: x86-এ একটিই carry flag, অথচ গুণের লুপে স্বাভাবিকভাবে দুটি স্বাধীন যোগের শিকল চলে। Intel তাই ADX extension আনে (মোটামুটি ২০১৪-র Broadwell প্রজন্ম থেকে): adcx শুধু CF পড়ে-লেখে, adox শুধু OF-কে carry হিসেবে ব্যবহার করে, আর সাথের mulx (BMI2) গুণের সময় কোনো flag ছোঁয় না। ফলে দুটি শিকল পাশাপাশি চলতে পারে। এটি দেখায়, flag-ওয়ালা ISA-ও শেষ পর্যন্ত flag-এর “একটিই ভাগের জিনিস” সমস্যায় ভুগেছে।

Cryptography-তে আরেকটি শর্ত আছে: হিসাবের সময় গোপন key-এর মানের ওপর নির্ভর করে বদলানো চলবে না (constant-time), নাহলে সময় মেপে key-এর তথ্য ফাঁস হতে পারে — একে timing side channel বলে, আর এটি প্রতিরক্ষামূলক নকশার একটি বড় বিষয়। তাই crypto library-তে carry-র জন্য শাখা নয়, সবসময় adc বা sltu-র মতো শাখাহীন instruction ব্যবহার হয়, আর ডেটার ওপর নির্ভর করে লুপ আগে থামানো হয় না। RISC-V-তে sltu-ভিত্তিক carry স্বভাবতই শাখাহীন — এটি একটি সুবিধা।

আরও বড় সংখ্যায় (হাজার হাজার limb) স্কুলের O(n2) গুণের বদলে Karatsuba বা FFT-ভিত্তিক গুণ ব্যবহার হয়; GMP আকার দেখে নিজে থেকে পদ্ধতি বদলায়। এর বিস্তারিত পর্ব ২১-এর গুণের আলোচনার সাথে মিলিয়ে পড়ার মতো।

মূল উপলব্ধি: Multi-precision হিসাব হলো স্কুলের যোগ-গুণ, শুধু ভিত্তি 264. Hardware সাহায্য করে দুইভাবে — পূর্ণ-প্রস্থের গুণফল আর সস্তা carry — আর নিরাপত্তার জন্য শাখাহীন, সমান-সময়ের কোড দরকার।

প্র ০৩Python-এর int কেন কখনো overflow করে না, আর তবু ২০২২ সালে Python-এ বিশাল int নিয়ে একটি নিরাপত্তা-সীমা কেন বসাতে হলো?

Python-এর (CPython) int একটি bignum: একটি ছোট header (মাপ আর চিহ্ন) আর digit-এর array. বেশিরভাগ platform-এ প্রতিটি digit ৩০ বিটের, রাখা হয় একটি ৩২-বিট শব্দে। কেন ৩২ নয়, ৩০? দুটি digit যোগ করলে ৩১ বিট লাগে — ৩২-বিট শব্দে carry রাখার জায়গা থাকে; আর দুটি ৩০-বিট digit-এর গুণফল ৬০ বিট, যা ৬৪-বিট শব্দে বাড়তি carry-সহ ধরে যায়। এতে C-তে বহনযোগ্যভাবে, কোনো assembly বা compiler extension ছাড়াই, সব হিসাব লেখা যায়। খরচ: প্রতিটি digit-এ ২ বিট “নষ্ট”, আর GMP-এর মতো হাতে-টিউন করা library-র চেয়ে ধীর।

এর ফলে Python-এ 2**10000 নির্ভুল আসে, আর প্রোগ্রামারকে overflow নিয়ে ভাবতে হয় না — ঠিক এই পর্বের limb-এর হিসাব, language runtime-এর ভেতরে লুকিয়ে। কিন্তু “overflow নেই” মানে “খরচ নেই” নয়। সংখ্যা যত বড়, হিসাব তত ধীর; আর কিছু কাজের খরচ দৈর্ঘ্যের বর্গের মতো বাড়ে। এর একটি উদাহরণ হলো বিশাল int-কে দশমিক string-এ বা string থেকে int-এ রূপান্তর, কারণ ভিত্তি 230 থেকে ভিত্তি ১০-এ যেতে বারবার ভাগ লাগে।

২০২২ সালে এই কারণেই একটি নিরাপত্তা-সমস্যা (CVE-2020-10735 নামে নথিভুক্ত) সমাধান করা হয়: কেউ একটি web server-এ লক্ষ লক্ষ অঙ্কের একটি সংখ্যা পাঠালে (যেমন JSON-এ) server-কে int() রূপান্তরে সেকেন্ডের পর সেকেন্ড CPU খরচ করতে হতো — একটি denial-of-service ঝুঁকি। Python তখন ডিফল্ট হিসেবে int আর দশমিক string-এর রূপান্তরে প্রায় ৪,৩০০ অঙ্কের সীমা বসায় (প্রয়োজনে বদলানো যায়)। Hex বা binary-তে রূপান্তরে এই সীমা নেই, কারণ সেগুলো সরাসরি বিটের দল থেকে হয়, দ্রুত।

এখান থেকে একটি সাধারণ শিক্ষা: fixed-width integer-এ বিপদ হলো নীরব wrap; bignum-এ বিপদ হলো সীমাহীন সময় আর মেমরি। দুই ক্ষেত্রেই বাইরে থেকে আসা ইনপুটের আকার যাচাই করা প্রোগ্রামারের দায়িত্ব। NumPy-তে আবার fixed-width (int32, int64) — একই Python প্রোগ্রামে দুই জগৎ পাশাপাশি থাকে, আর NumPy array-র যোগে wrap হয় চুপচাপ।

মূল উপলব্ধি: Overflow-হীন integer আসলে runtime-এ লুকোনো multi-precision arithmetic. এটি সঠিকতা দেয়, কিন্তু সময় আর মেমরির খরচ সীমাহীন হতে পারে — তাই সেখানেও সীমা লাগে, শুধু অন্য জায়গায়।

প্র ০৪Saturation আর wrap — কোনটি “সঠিক” আচরণ? DSP, SIMD আর AI-এর জগৎ কেন saturation পছন্দ করে, আর সাধারণ CPU কেন wrap?

Wrap-এর বড় গুণ গাণিতিক: n-বিট হিসাব mod 2n-এর একটি ring, যেখানে যোগ associative — (a + b) − b সবসময় a, মাঝখানে যত overflow-ই হোক। তাই অনেকগুলো সংখ্যা যোগের মাঝপথে overflow হলেও শেষ ফল সীমার ভেতরে থাকলে সঠিক আসে (checksum, hash, pointer হিসাব এর ওপর নির্ভর করে)। Compiler-ও যোগের ক্রম বদলে optimize করতে পারে। এজন্য সাধারণ CPU-র integer হিসাব wrap করে।

কিন্তু শব্দ, ছবি বা সেন্সরের মান কোনো ring নয় — এগুলো একটি ভৌত পরিমাণ, যার সীমা আছে। Microphone-এর সংকেত সর্বোচ্চ মান ছাড়ালে বাস্তবে যা হয় তা clipping — সর্বোচ্চে আটকে থাকা। Wrap হলে সর্বোচ্চ ধনাত্মক থেকে হঠাৎ সর্বনিম্ন ঋণাত্মক — speaker-এ বিকট শব্দ। তাই ১৯৮০-র দশক থেকে Texas Instruments, Analog Devices, Motorola-র মতো কোম্পানির DSP chip-এ saturating যোগ মৌলিক ছিল, প্রায়ই একটি mode বিট দিয়ে চালু করা যেত। পরে ARM তার ARMv5TE-র DSP extension-এ qadd/qsub আনে, আর x86-এর MMX (১৯৯৭) থেকে SIMD instruction-এ saturating byte আর word যোগ — ভিডিও আর ছবির কাজ ঠিক এগুলোই চায়।

দাম: অংশ ৫-এর শেষ লাইন — (120 + 10) − 10 = 117. Saturation-এ যোগ আর associative নয়, তাই ক্রম বদলালে ফল বদলায়, আর compiler নিজে থেকে এমন হিসাব পুনর্বিন্যাস করতে পারে না। এজন্য saturation সবসময় আলাদা, স্পষ্ট instruction বা function — কখনো ডিফল্ট নয়। Rust-এ saturating_add, C-তে নিজে লেখা বা intrinsic.

AI-এর জগতে saturation আবার ফিরে এসেছে। Neural network-এর মডেলকে ছোট আর দ্রুত করতে weight আর activation ৮-বিট integer-এ (int8 quantization) রাখা হয়। ৩২-বিট accumulator থেকে আবার ৮ বিটে নামানোর সময় সীমা ছাড়ালে saturate করা হয় — wrap হলে বড় ধনাত্মক activation হঠাৎ বড় ঋণাত্মক হয়ে মডেলের উত্তর নষ্ট করত। GPU আর AI accelerator-এর (পর্ব ৫৫) integer unit-এ তাই saturating রূপান্তর সাধারণ ব্যাপার।

মূল উপলব্ধি: “সঠিক” আচরণ নির্ভর করে সংখ্যাটি কী বোঝায় তার ওপর। গণনার কাঠামো (index, hash, বড় সংখ্যার limb) চায় wrap-এর গাণিতিক সৌন্দর্য; ভৌত পরিমাণ (শব্দ, আলো, AI-এর activation) চায় saturation-এর বাস্তবতা।

প্র ০৫C ভাগফলকে শূন্যের দিকে গোল করে, Python নিচের দিকে। কোনটি ভালো, আর এই সিদ্ধান্ত কীভাবে hardware আর compiler-কে প্রভাবিত করেছে?

পুরোনো C (C89) ঋণাত্মক সংখ্যার ভাগের দিক নির্দিষ্ট করেনি — implementation-defined ছিল, যাতে প্রতিটি যন্ত্র তার hardware-এর স্বাভাবিক নিয়ম মানতে পারে। কিন্তু একই কোড দুই যন্ত্রে দুই উত্তর দেওয়া বহনযোগ্যতার জন্য খারাপ, আর ততদিনে প্রায় সব CPU-র ভাগ instruction (x86-এর idiv সহ) শূন্যের দিকে গোল করত, Fortran-ও তাই। তাই C99 truncation বাধ্যতামূলক করে। পরে RISC-V-ও div-কে শূন্যের দিকে গোল করার নিয়মে বানায় — ভাষা আর hardware একে অপরকে শক্তিশালী করেছে।

Python-এর স্রষ্টা Guido van Rossum একটি লেখায় (২০১০ সালের দিকে) ব্যাখ্যা করেন কেন Python floor বেছে নিয়েছে: ভাগশেষ সবসময় ভাজকের চিহ্ন পায়, তাই x % n সবসময় 0…n − 1 (ধনাত্মক n-এ)। Timestamp থেকে দিনের ঘণ্টা বা সপ্তাহের দিন বের করতে, circular buffer-এ index ঘোরাতে এটি সরাসরি কাজ করে; C-তে ঋণাত্মক মানে বাড়তি সংশোধন লাগে। গণিতবিদ আর Donald Knuth-এর মতো কম্পিউটার বিজ্ঞানীরাও floor-ভিত্তিক সংজ্ঞাকে গাণিতিকভাবে বেশি স্বাভাবিক মনে করেন। আরেকটি বিকল্প Euclidean division — ভাগশেষ সবসময় অঋণাত্মক, ভাজকের চিহ্ন যা-ই হোক (Rust-এ rem_euclid).

Hardware আর compiler-এ প্রভাব এই পর্বেই দেখলেন: arithmetic shift স্বভাবত floor করে, তাই C-র truncation-এর জন্য ২k দিয়ে signed ভাগে compiler-কে bias বসাতে হয় — তিনটি বাড়তি instruction. Python-এর //-এ ২k দিয়ে ভাগ সরাসরি একটি sra. অন্য ধ্রুবক দিয়ে ভাগে (যেমন x / 10) compiler ভাগ instruction এড়াতে একটি “magic number” দিয়ে গুণ করে উপরের অর্ধেক নেয় — এই পদ্ধতি Torbjörn Granlund আর Peter Montgomery-র ১৯৯৪ সালের একটি গবেষণাপত্রে সুশৃঙ্খলভাবে বর্ণিত — আর সেখানেও signed truncation-এর জন্য ছোট একটি সংশোধন লাগে। এর বিস্তারিত পর্ব ২১–২২-এর গুণ-ভাগের আলোচনার সাথে মিলিয়ে দেখতে পারেন।

ব্যবহারিক শিক্ষা: এক ভাষা থেকে আরেক ভাষায় কোড আনার সময় (যেমন Python-এর algorithm C-তে) প্রতিটি ঋণাত্মক ভাগ আর % আলাদা করে যাচাই করুন। এই বাগ সাধারণত শুধু ঋণাত্মক ইনপুটে দেখা দেয় — তাই সাধারণ পরীক্ষায় ধরা পড়ে না।

মূল উপলব্ধি: কোনোটি সার্বজনীনভাবে “ভালো” নয়; truncation hardware-এর ঐতিহ্য আর প্রতিসাম্য (−a/b = −(a/b)) রক্ষা করে, floor ভাগশেষকে সুন্দর রাখে। সিদ্ধান্তটি ভাষার, কিন্তু তার দাম দেয় compiler — প্রতিটি signed ভাগের সংশোধনী instruction-এ।

অনুশীলন

পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।

  1. Multi-precision যোগ (হাতে হিসাব): দুটি ৯৬-বিট সংখ্যা তিনটি ৩২-বিট limb-এ রাখা আছে: A = 00000005 FFFFFFFF 80000001, B = 00000002 00000000 7FFFFFFF (উপরের limb আগে)। (ক) প্রতিটি limb-এর যোগফল, carry-in আর carry-out লিখে ফল বের করুন। (খ) অংশ ৪-এর পদ্ধতিতে (মাঝের limb-এ দুই ধাপের যোগ আর দুটি sltu) RV32I-এ loads/stores বাদে কয়টি instruction লাগবে? x86-এ কয়টি? (গ) একই ধারা মেনে n-limb যোগে RV32I-এ মোট কয়টি instruction-এর সূত্র লিখুন (n ≥ 2, শেষ carry-out লাগবে না)।
    limb 0: 80000001 + 7FFFFFFF + 0 = 1_00000000  -> 00000000, carry 1
    limb 1: FFFFFFFF + 00000000 + 1 = 1_00000000  -> 00000000, carry 1
    limb 2: 00000005 + 00000002 + 1 = 0_00000008  -> 00000008, carry 0
    A + B = 00000008 00000000 00000000
    • (খ) Limb ০: add + sltu = ২; মাঝের limb ১: add, sltu, add, sltu, or = ৫; উপরের limb ২ (carry-out লাগে না): add, add = ২. মোট ৯টি. x86-এ add, adc, adc = ৩টি.
    • (গ) 2 + 5(n − 2) + 2 = 5n − 6; n = 3-এ ৯, n = 2-এ ৪ (stepper-এর সাথে মেলে)। x86-এ n.
    • লক্ষ করুন limb ১-এ: FFFFFFFF + 0 = FFFFFFFF (প্রথম sltu = 0), তারপর +1-এ wrap করে 0 (দ্বিতীয় sltu = 1) — carry এসেছে দ্বিতীয় ধাপ থেকে।
  2. আউটপুট অনুমান করুন: x86-64 Linux-এ gcc দিয়ে নিচের C কোড চালালে কী ছাপবে? তারপর Python-এ -7 // 2 আর -7 % 2 কত?
    int a = -7;
    printf("%d %d %d %d\n", a / 2, a % 2, a >> 1, (a + 1) >> 1);
    unsigned int n = 0;
    printf("%u\n", n - 1);
    unsigned char p = 250, q = 10;
    unsigned char w = (unsigned char)(p + q);
    unsigned char s = (p + q > 255) ? 255 : (unsigned char)(p + q);
    printf("%u %u\n", (unsigned)w, (unsigned)s);
    -3 -1 -4 -3
    4294967295
    4 255
    • a / 2 = −3 (truncation), a % 2 = −1 (ভাজ্যের চিহ্ন; −3 × 2 + (−1) = −7).
    • a >> 1 = −4 (gcc-তে arithmetic shift, floor(−3.5)). (a + 1) >> 1 = −6 >> 1 = −3 — এটিই k = 1-এর bias কৌশল (bias = 21 − 1 = 1), তাই C-র /-এর সাথে মেলে।
    • n - 1: unsigned 0 − 1 wrap করে 232 − 1 = 4294967295.
    • p + q int-এ 260 (integer promotion); unsigned char-এ রাখলে 260 mod 256 = 4, saturate করলে 255.
    • Python: -7 // 2 = −4, -7 % 2 = 1 (floor; −4 × 2 + 1 = −7).
  3. কোড লিখুন: (ক) RV32I-এ ৬৪-বিট বিয়োগ a1:a0 = a1:a0 − a3:a2 লিখুন (borrow sltu দিয়ে), আর x86 (৩২-বিট) ও ARM (AArch32)-এ এর সমতুল্য দুটি instruction কী? A = 0x00000001_00000000, B = 0x00000000_00000001 দিয়ে প্রতিটি register-এর মান দেখান। (খ) C-তে int8_t sat_add_s8(int8_t a, int8_t b) লিখুন, যাতে কোনো overflow বা undefined behaviour না ঘটে।
    # (ক) RV32I: a1:a0 -= a3:a2
    sltu  t0, a0, a2      # borrow = (a_lo <u b_lo)   -> 0 <u 1 : t0 = 1
    sub   a0, a0, a2      # a0 = 0x00000000 - 0x00000001 = 0xFFFFFFFF
    sub   a1, a1, a3      # a1 = 0x00000001 - 0x00000000 = 0x00000001
    sub   a1, a1, t0      # a1 = 0x00000001 - 1 = 0x00000000
    # result 0x00000000_FFFFFFFF = 4294967296 - 1   ok
    
    # x86 (32-bit, Intel syntax): sub eax, ecx  /  sbb edx, ebx
    # ARM (AArch32): subs r0, r0, r2  /  sbc r1, r1, r3
    /* (খ) */
    #include <stdint.h>
    int8_t sat_add_s8(int8_t a, int8_t b) {
        int s = a + b;              /* promoted to int: -256..254, cannot overflow */
        if (s > INT8_MAX) return INT8_MAX;
        if (s < INT8_MIN) return INT8_MIN;
        return (int8_t)s;
    }
    • (ক)-তে sltu বিয়োগের আগে বসেছে, কারণ sub a0-কে মুছে দেয়; বিয়োগের পরে তুলনা করতে চাইলে নতুন a0-কে পুরোনো মানের সাথে তুলনা করা যেত না। borrow = 1 মানে নিচের অর্ধেকে ধার লেগেছে, তাই উপরের অর্ধেক থেকে ১ কমে।
    • (খ)-তে মূল কৌশল: চওড়া type-এ (int) হিসাব করে তারপর সীমা যাচাই। 120 + 10 → 130 > 127 → 127; −120 + (−10) → −130 → −128.
  4. বাগ খুঁজুন: নিচের প্রতিটি টুকরোয় কী ভুল, কোন ইনপুটে দেখা দেয়, আর সংশোধন কী? n আর len-এর type পাশে লেখা।
    /* (ক) */ for (unsigned i = n - 1; i >= 0; i--) use(a[i]);   /* unsigned n */
    /* (খ) */ if (len > 64) return -1;                            /* int len    */
              memcpy(buf, src, len);                              /* char buf[64] */
    /* (গ) */ int mid = (lo + hi) / 2;                            /* int lo, hi */
    /* (ঘ) */ int avg = (a + b) >> 1;   /* "fast average", a = -3, b = 0 */
    • (ক) i >= 0 unsigned-এ সবসময় সত্য; i = 0-এর পর i হয় 4294967295 — অসীম লুপ আর array-র বাইরে পড়া। n = 0 হলে প্রথমেই i = 4294967295. সংশোধন: for (unsigned i = n; i-- > 0; ).
    • (খ) len = −1 হলে len > 64 মিথ্যা, পরীক্ষা পেরিয়ে যায়; memcpy-এর তৃতীয় argument size_t, তাই −1 হয়ে যায় বিশাল সংখ্যা — buffer overflow. সংশোধন: if (len < 0 || len > 64) return -1; বা len-কে শুরু থেকেই size_t রাখা।
    • (গ) lo + hi INT_MAX ছাড়ালে signed overflow (UB) — খুব বড় array-তে। সংশোধন: lo + (hi - lo) / 2 (যখন 0 ≤ lo ≤ hi).
    • (ঘ) −3 >> 1 = −2 (floor), অথচ (−3 + 0) / 2 = −1 (C-র truncation) — ঋণাত্মকে উত্তর এক কম। তার ওপর a + b-তেও overflow হতে পারে। floor-গড় চাইলে overflow এড়াতে (a & b) + ((a ^ b) >> 1); C-র মতো truncation চাইলে int64_t-এ যোগ করে / 2.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমRV32I-এ C-র int x / 4 (truncation) shift দিয়ে করার instruction-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. srai t0, a0, 31 — sign-বিট সব ঘরে ছড়ানো
  2. srli t0, t0, 30 — bias = ঋণাত্মক হলে 3, নাহলে 0
  3. add a0, a0, t0 — x-এর সাথে bias যোগ
  4. srai a0, a0, 2 — arithmetic shift দিয়ে ভাগ

RV32I-এ add t0, t1, t2-এর পর কোন instruction t3-এ unsigned carry (১ বা ০) রাখে?

  • slt t3, t0, t1
  • sltu t3, t0, t1
  • sltu t3, t1, t0
  • carry flag থেকে পড়ার একটি বিশেষ instruction

MIPS-এর add আর addu instruction-এর পার্থক্য কী?

  • add signed যোগ করে, addu unsigned যোগ করে — ফলের বিট আলাদা
  • addu overflow-এ trap করে, add করে না
  • ফলের বিট একই; add signed overflow-এ trap করে, addu করে না
  • add flag লেখে, addu লেখে না

৩২-বিট x86-এ দুটি ৬৪-বিট সংখ্যা (EDX:EAX আর EBX:ECX) যোগ করতে কোন instruction-জোড়া লাগে (Intel syntax, অর্থাৎ গন্তব্য register আগে)?

  • add eax, ecx তারপর adc edx, ebx
  • add eax, ecx তারপর add edx, ebx
  • adc edx, ebx তারপর add eax, ecx
  • add eax, ecx তারপর sbb edx, ebx

৮-বিট signed saturating যোগে 120 + 10 আর −120 + (−10)-এর ফল কত?

  • −126 আর 126
  • 130 আর −130
  • 255 আর 0
  • 127 আর −128

gcc-তে (যেখানে ঋণাত্মক int-এর >> arithmetic shift) int x = -7; হলে x >> 2 আর x / 4-এর মান কত?

  • দুটিই −2
  • −2 আর −1
  • −1 আর −2
  • দুটিই −1

৩২-বিট C-তে int i = -1; unsigned int n = 5; হলে if (i < n)-এর ফল কী, আর RISC-V-তে compiler সাধারণত কোন শাখা-instruction বসায়?

  • সত্য, blt
  • মিথ্যা, blt
  • মিথ্যা, bltu
  • সত্য, bltu

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. এক adder, দুই প্রশ্ন: যোগ mod 2n; carry = unsigned সীমা ছাড়ানো, V = signed সীমা ছাড়ানো। ISA ঠিক করে এই খবর কোথায় যায় — flag (x86, ARM), trap (MIPS add), নাকি কোথাও না (RISC-V).
  2. RISC-V-র কৌশল: carry = sltu t3, t0, t1 (s <u a); signed overflow = (b < 0) ≠ (s < a), অর্থাৎ slti + slt + bne; বা (s ⊕ a) ∧ (s ⊕ b)-এর MSB.
  3. Multi-precision: limb ধরে যোগ, carry পরের limb-এ। x86/ARM-এ add + adc; RV32-এ ৬৪-বিট যোগে add, sltu, add, add. Python, GMP-এর bignum এই হিসাবেরই বড় রূপ।
  4. Saturation: সীমা ছাড়ালে সর্বোচ্চ/সর্বনিম্নে আটকানো — ছবি, শব্দ, DSP, SIMD-এ; কিন্তু associative নয়। Signed-unsigned তুলনার বাগ: −1 unsigned-এ বিশাল, compiler বসায় bltu.
  5. Shift আর ভাগ: sra = floor, C-র / = truncation; C-র জন্য (x + ((x >> 31) & (2k − 1))) >> k. C-তে ভাগশেষ ভাজ্যের চিহ্ন পায়, Python-এ ভাজকের।

পরের পর্বে: যোগ থেকে গুণে — কাগজে-কলমে গুণের মতো shift আর add, Booth-এর algorithm দিয়ে signed গুণ, আর আধুনিক CPU-র দ্রুত multiplier কীভাবে এক cycle-এ অনেকগুলো আংশিক গুণফল যোগ করে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান