এই পর্বে যা শিখবেন
- 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-64 | ARM (AArch64) | MIPS (ক্লাসিক) | RISC-V (RV32I/RV64I) |
|---|---|---|---|---|
| যোগের পর carry জানা | CF flag (সবসময় লেখা হয়) | C flag (শুধু adds-এ) | flag নেই; sltu | flag নেই; sltu |
| Signed overflow জানা | OF flag, jo | V flag, b.vs | add-এ trap, বা addu + যাচাই | trap নেই; slt/xor দিয়ে যাচাই |
| Carry-সহ যোগ | adc | adc, adcs | নেই | নেই (base ISA-তে) |
| তুলনা আর শাখা | cmp + jl/jb | cmp + b.lt/b.lo | slt + bne | blt/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. “পরে” চাপুন:
t1 = 0x7FFFFFFF (signed 2147483647 = INT_MAX)
t2 = 0x00000001 (signed 1)
add t0, t1, t2
t0 = 0x80000000 (signed -2147483648, unsigned 2147483648)
t0 <u t1 ? 2147483648 < 2147483647 ? no
t3 = 0 -> no carry
check: 2147483647 + 1 = 2147483648 < 2^32 (fits as unsigned)
slti t3, t2, 0 : 1 < 0 ? t3 = 0 slt t4, t0, t1 : -2147483648 < 2147483647 ? t4 = 1
t3 = 0, t4 = 1 -> not equal -> branch taken: signed overflow
check: 2147483647 + 1 = 2147483648 > INT_MAX
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
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)
- যোগ। t1 = 0x7FFFFFFF (৩২-বিট signed-এর সর্বোচ্চ মান), t2 = 1.
addকোনো প্রশ্ন না করে wrap করা ফল লেখে: 0x80000000 — signed চোখে −2147483648, unsigned চোখে 2147483648. - Carry আছে কি?
sltuদেখে ফল t1-এর চেয়ে unsigned অর্থে ছোট কি না। 2147483648 ছোট নয়, তাই t3 = 0 — carry নেই। ঠিকই: unsigned অর্থে 2147483648 সীমার (232 − 1) ভেতরে। - Signed-এর দুই তুলনা।
slti: t2 ঋণাত্মক কি? না, t3 = 0.slt: ফল কি t1-এর চেয়ে ছোট? signed অর্থে −2147483648 < 2147483647, তাই t4 = 1. ধনাত্মক সংখ্যা যোগ করে ফল ছোট হয়ে গেছে — সন্দেহজনক। - সিদ্ধান্ত। t3 ≠ t4, তাই
bneলাফ দেয়: signed overflow. আসল যোগফল 2147483648, INT_MAX-এর এক বেশি। একই বিট-প্যাটার্নে unsigned অর্থে কোনো সমস্যা নেই, signed অর্থে আছে। - দ্বিতীয় উদাহরণ। দুটি operand-ই 0xFFFFFFFF. যোগফল 0xFFFFFFFE.
sltu: 4294967294 < 4294967295, তাই t3 = 1 — carry বেরিয়েছে; unsigned আসল যোগফল 8589934590 সত্যিই ৩২ বিটে ধরে না। - 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-এর ক্রম একটু ভিন্ন হতে পারে। এবার একটি সংখ্যা দিয়ে ধাপে ধাপে:
A = 0x00000002_FFFFFFF0 (12884901872) a1 = 0x00000002 a0 = 0xFFFFFFF0 B = 0x00000001_00000020 ( 4294967328) a3 = 0x00000001 a2 = 0x00000020
0xFFFFFFF0 + 0x00000020 = 0x1_00000010 (33 bits!) a0 = 0x00000010 (only 32 bits are kept)
a0 <u a2 ? 0x00000010 < 0x00000020 ? yes
t0 = 1 (the lost 33rd bit, recovered)
a1 = 0x00000002 + 0x00000001 = 0x00000003
a1 = 0x00000003 + 1 = 0x00000004
result a1:a0 = 0x00000004_00000010 = 17179869200
check 12884901872 + 4294967328 = 17179869200 ok
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
- দুই অর্ধেক। RV32-এর register ৩২ বিটের, তাই প্রতিটি ৬৪-বিট সংখ্যা থাকে একটি register জোড়ায়: উপরের ৩২ বিট (hi) আর নিচের ৩২ বিট (lo). Calling convention অনুযায়ী প্রথম সংখ্যা a1:a0-এ, দ্বিতীয়টি a3:a2-এ।
- নিচের অর্ধেক যোগ। 0xFFFFFFF0 + 0x20 = 0x1_00000010 — ৩৩ বিট লাগে। Register রাখে শুধু নিচের ৩২ বিট, 0x00000010; ৩৩তম বিটটি — carry — হারিয়ে গেল, কারণ রাখার কোনো flag নেই।
- Carry উদ্ধার। নতুন a0 (0x10) কি a2 (0x20)-এর চেয়ে unsigned অর্থে ছোট? হ্যাঁ — যোগ করে ছোট হয়ে যাওয়া মানেই wrap হয়েছে। তাই t0 = 1: হারানো ৩৩তম বিট ফিরে পাওয়া গেল।
- উপরের অর্ধেক যোগ। 2 + 1 = 3. এই যোগ নিচের যোগের ওপর নির্ভর করে না, তাই superscalar CPU এটি
sltu-এর সাথে একই সময়ে চালাতে পারে। - Carry যোগ। 3 + 1 = 4. ফল a1:a0 = 0x00000004_00000010 = 17179869200, যা 12884901872 + 4294967328-এর সাথে মেলে।
- 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 ০ সবচেয়ে কম গুরুত্বের):
| Limb | A | B | carry-in | যোগফল (৩৩ বিট) | ফলের limb | carry-out |
|---|---|---|---|---|---|---|
| ০ | 76543210 | 90000000 | 0 | 1_06543210 | 06543210 | 1 |
| ১ | FEDCBA98 | 80000000 | 1 | 1_7EDCBA99 | 7EDCBA99 | 1 |
| ২ | 89ABCDEF | F0F0F0F0 | 1 | 1_7A9CBEE0 | 7A9CBEE0 | 1 |
| ৩ | 01234567 | 0F0F0F0F | 1 | 0_10325477 | 10325477 | 0 |
ফল (উপরের 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.
sllk = ×2k: signed আর unsigned দুটিতেই, mod 232-এ। বাঁয়ের বিট বেরিয়ে গেলে ফল wrap করে — গুণের overflow-এর মতোই। (C-তে ঋণাত্মক signed সংখ্যাকে<<করা আবার undefined behaviour; unsigned-এ করাই নিরাপদ।)srlk = unsigned ÷ 2k: ভাগফল নিচের দিকে গোল — unsigned-এ যা C-র/-এর সাথে হুবহু এক।srak = 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 >> 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-ই থাকে।
x = -7 = 0xFFFFFFF9 = 1111 ... 1111 1001 -7 / 4 = -1.75 C wants -1 (truncate toward zero) floor is -2
1111 ... 1111 1001 >> 2 (sign bits enter)
1111 ... 1111 1110 = 0xFFFFFFFE = -2
= floor(-1.75) -> WRONG for C
srai t0, a0, 31 : t0 = 0xFFFFFFFF (x < 0, all sign bits)
srli t0, t0, 30 : t0 = 0x00000003 = 3 = 2^2 - 1
a0 = -7 + 3 = -4 = 0xFFFFFFFC = 1111 ... 1111 1100
1111 ... 1111 1100 >> 2
1111 ... 1111 1111 = 0xFFFFFFFF = -1 = C's -7 / 4 ok
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)
- লক্ষ্য ঠিক করা। −7-এর ৩২-বিট রূপ 0xFFFFFFF9. গাণিতিক ভাগফল −1.75. C চায় শূন্যের দিকে গোল, অর্থাৎ −1; −∞-এর দিকে গোল করলে আসত −2.
- শুধু sra. দুই ঘর ডানে সরালে বাঁয়ে sign-বিট ১ ঢোকে, ফল 0xFFFFFFFE = −2 — এটি floor, C-র উত্তর নয়। শুধু
sraiবসালে compiler ভুল কোড বানাত। - Bias বানানো।
srai t0, a0, 31sign-বিটকে ৩২ ঘরে ছড়িয়ে দেয়: ঋণাত্মক x-এ 0xFFFFFFFF. তারপরsrli t0, t0, 30বাঁয়ে ৩০টি ০ এনে রাখে শুধু নিচের দুটি ১ — মান 3 = 22 − 1. এটি& 3-এর সমান, শুধু ভিন্ন instruction-এ। - Bias যোগ। −7 + 3 = −4 = 0xFFFFFFFC. এখন সংখ্যাটি 4-এর গুণিতক, তাই এর floor ভাগ আর truncation একই উত্তর দেবে।
- এবার shift. −4 sra 2 = −1 = 0xFFFFFFFF — C-র −7 / 4-এর সাথে মেলে। (x + 2k − 1)-এর floor আসলে x/2k-এর ceiling, আর ঋণাত্মক সংখ্যায় ceiling-ই truncation.
- ধনাত্মক 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 আর প্রতিটি মধ্যবর্তী মান চেয়ে নিন, আর নিজে চালানোর মতো পরীক্ষা চান:
AI-এর উত্তর হাতে পেলে — যাচাইয়ের চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- Carry আর overflow-এর পার্থক্য? — Carry: unsigned ফল সীমার বাইরে (MSB থেকে carry); overflow: signed ফল সীমার বাইরে (V = cn ⊕ cn−1).
- MIPS-এ add আর addu-এর পার্থক্য? — ফলের বিট একই;
addsigned 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-এ।
অনুশীলন
পরীক্ষার ধাঁচে লেখা — আগে খাতায় সমাধান করুন, তারপর উত্তর মিলিয়ে নিন।
-
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 এসেছে দ্বিতীয় ধাপ থেকে।
- (খ) Limb ০:
-
আউটপুট অনুমান করুন: 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 255a / 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 + qint-এ 260 (integer promotion);unsigned char-এ রাখলে 260 mod 256 = 4, saturate করলে 255.- Python:
-7 // 2= −4,-7 % 2= 1 (floor; −4 × 2 + 1 = −7).
-
কোড লিখুন: (ক) 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বিয়োগের আগে বসেছে, কারণsuba0-কে মুছে দেয়; বিয়োগের পরে তুলনা করতে চাইলে নতুন a0-কে পুরোনো মানের সাথে তুলনা করা যেত না। borrow = 1 মানে নিচের অর্ধেকে ধার লেগেছে, তাই উপরের অর্ধেক থেকে ১ কমে। - (খ)-তে মূল কৌশল: চওড়া type-এ (int) হিসাব করে তারপর সীমা যাচাই। 120 + 10 → 130 > 127 → 127; −120 + (−10) → −130 → −128.
- (ক)-তে
-
বাগ খুঁজুন: নিচের প্রতিটি টুকরোয় কী ভুল, কোন ইনপুটে দেখা দেয়, আর সংশোধন কী?
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 >= 0unsigned-এ সবসময় সত্য; i = 0-এর পর i হয় 4294967295 — অসীম লুপ আর array-র বাইরে পড়া। n = 0 হলে প্রথমেই i = 4294967295. সংশোধন:for (unsigned i = n; i-- > 0; ). - (খ) len = −1 হলে
len > 64মিথ্যা, পরীক্ষা পেরিয়ে যায়;memcpy-এর তৃতীয় argumentsize_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-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
srai t0, a0, 31— sign-বিট সব ঘরে ছড়ানোsrli t0, t0, 30— bias = ঋণাত্মক হলে 3, নাহলে 0add a0, a0, t0— x-এর সাথে bias যোগ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, ebxadd eax, ecxতারপরadd edx, ebxadc edx, ebxতারপরadd eax, ecxadd 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
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- এক adder, দুই প্রশ্ন: যোগ mod 2n; carry = unsigned সীমা ছাড়ানো, V = signed সীমা ছাড়ানো। ISA ঠিক করে এই খবর কোথায় যায় — flag (x86, ARM), trap (MIPS
add), নাকি কোথাও না (RISC-V). - RISC-V-র কৌশল: carry =
sltu t3, t0, t1(s <u a); signed overflow = (b < 0) ≠ (s < a), অর্থাৎslti+slt+bne; বা (s ⊕ a) ∧ (s ⊕ b)-এর MSB. - Multi-precision: limb ধরে যোগ, carry পরের limb-এ। x86/ARM-এ
add+adc; RV32-এ ৬৪-বিট যোগেadd,sltu,add,add. Python, GMP-এর bignum এই হিসাবেরই বড় রূপ। - Saturation: সীমা ছাড়ালে সর্বোচ্চ/সর্বনিম্নে আটকানো — ছবি, শব্দ, DSP, SIMD-এ; কিন্তু associative নয়। Signed-unsigned তুলনার বাগ: −1 unsigned-এ বিশাল, compiler বসায়
bltu. - Shift আর ভাগ: sra = floor, C-র
/= truncation; C-র জন্য (x + ((x >> 31) & (2k − 1))) >> k. C-তে ভাগশেষ ভাজ্যের চিহ্ন পায়, Python-এ ভাজকের।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · গুণ — shift-add, Booth-এর algorithm আর দ্রুত multiplierপরবর্তী পর্বগুণের circuit আর signed গুণের কৌশল।
- পর্ব ১৩ · ALU নকশা — যোগ, বিয়োগ, লজিক আর flagভিত্তিAdder-subtractor, Z N C V flag আর slt-এর hardware.
- পর্ব ০৫ · Integer-এর বিট-রূপ — unsigned, sign-magnitude, two's complement আর overflowভিত্তিTwo's complement, sign extension আর C-র integer ফাঁদ।
- কম্পিউটার আর্কিটেকচার ও ডিজিটাল লজিক · কোর্স হোম৬২টি পর্বলজিক গেট থেকে pipeline আর cache — পুরো রোডম্যাপ।
- C প্রোগ্রামিং · Undefined behavior ও নিরাপদ কোডপাশের ট্র্যাকSigned overflow, sanitizer আর নিরাপদ integer কোড।
- C প্রোগ্রামিং · Bitwise operationsপাশের ট্র্যাকShift, mask আর XOR — এই পর্বের কৌশলগুলো C-তে।