ARM64 (AArch64) Basics — এক syntax-এ, বিশুদ্ধ RISC-এ
ARM64 (AArch64) Basics
ARM64 (AArch64) — গত module-এর risc-vs-cisc আলোচনার একদম বিশুদ্ধ বাস্তবায়ন। একটাই syntax (কোনো AT&T/Intel ফোর্ক নেই), x0-x30/w0-w30-এর সরল রেজিস্টার-এলিয়াসিং, x30/lr link register দিয়ে return-address সামলানোর ভিন্ন কৌশল, আর load-store architecture-এর কঠোর নিয়ম -- arithmetic instruction কখনো memory ছোঁয় না। x86-64-র সাথে পাশাপাশি একই ফাংশন কম্পাইল করে এই সব পার্থক্য concrete করে দেখানো হবে।
আগে এটা বুঝি
গত লেসনের শেষে একটা পর্যবেক্ষণ রেখে এসেছিলাম — x86-64-এ add [rbp-4], eax-এর মতো instruction সরাসরি memory-তে read-modify-write করতে পারে, একটাই instruction-এ। এই লেসনে আমরা একটা সম্পূর্ণ ভিন্ন দর্শনের ISA দেখব, যেখানে এমন instruction আদৌ অস্তিত্বহীন — ARM64, আনুষ্ঠানিকভাবে AArch64 (৬৪-বিট ARM architecture)।
গত module-এর risc-vs-cisc লেসনে RISC আর CISC-এর তাত্ত্বিক পার্থক্য দেখেছিলেন — uniform instruction length, load-store architecture, বড় register file, hardwired control। ARM64 এই দর্শনের একটা প্রায়-বিশুদ্ধ বাস্তবায়ন — RISC-V-র মতোই RISC-ঘরানার, কিন্তু বাস্তব দুনিয়ায় RISC-V-র চেয়েও অনেক বেশি ব্যাপকভাবে ব্যবহৃত (প্রতিটা আধুনিক smartphone, Apple-এর M-সিরিজ ল্যাপটপ/ডেস্কটপ, AWS-এর Graviton সার্ভার — এই সবকিছুর মূলে ARM64)।
আর এই লেসনে একটা স্বস্তির খবরও আছে — গত লেসনের AT&T-বনাম-Intel বিভ্রান্তি এখানে নেই। ARM64-এর একটাই canonical syntax, সবখানে, সব টুলে। কেন এই পার্থক্য? আর load-store architecture ঠিক কী শর্ত আরোপ করে, তা কী পরিমাণ instruction-সংখ্যায় খরচ হয়? এই লেসন সেই প্রশ্নগুলোর concrete, তুলনামূলক উত্তর দেবে — বারবার x86-64-র সাথে পাশাপাশি রেখে।
মূল ধারণা
কেন কোনো AT&T/Intel-সদৃশ বিভাজন নেই
x86-64-এর সেই ঐতিহাসিক syntax-ফোর্কের একটা নির্দিষ্ট উৎস ছিল — GNU-এর as Unix-এর পুরনো assembler-ঐতিহ্য (AT&T) অনুসরণ করেছিল, যখন Intel তাদের নিজস্ব official manual-এ একটা ভিন্ন, স্বাধীনভাবে-বিকশিত কনভেনশন ব্যবহার করত — দুটোই x86-এর জন্মের অনেক আগে থেকে (AT&T) বা তার নিজস্ব প্রতিষ্ঠানিক-ইতিহাস থেকে (Intel) আলাদাভাবে বিকশিত হয়ে এসেছিল, তাই যখন GNU টুলচেইন x86 সমর্থন করল, দুইটা প্রতিদ্বন্দ্বী কনভেনশন একসাথে বাস করা শুরু করল।
ARM-এর গল্প ভিন্ন। Arm Ltd (মূলত Acorn Computers, ১৯৮৫-তে প্রথম ARM ডিজাইন) শুরু থেকেই তাদের নিজস্ব official Architecture Reference Manual-এ একটা একক, সুনির্দিষ্ট assembly syntax প্রকাশ করেছে — কোনো প্রতিদ্বন্দ্বী “Unix-ঐতিহ্য” এই ISA-র জন্য আগে থেকেই বিদ্যমান ছিল না যেটা GNU টুলচেইনকে একটা ভিন্ন কনভেনশন উত্তরাধিকারসূত্রে নিতে বাধ্য করত। GNU-এর ARM assembler (as, ARM target-এ) তাই সরাসরি Arm-এর নিজস্ব official syntax গ্রহণ করল — কোনো প্রতিযোগী কনভেনশন তৈরি হওয়ার সুযোগই হয়নি।
Register set — x0-x30 / w0-w30, একটা সরল দুই-স্তরের aliasing
গত লেসনে x86-64-র sub-register স্কিম দেখেছিলেন — rax/eax/ax/al/ah, চার স্তরের ওভারল্যাপিং, কিছু register-এ upper-byte অ্যাক্সেসও আছে (ah), কিছুতে নেই (r8b)। ARM64-র স্কিম উল্লেখযোগ্যভাবে সরল — প্রতিটা ৬৪-বিট register-এর ঠিক দুইটা view, না বেশি, না কম:
| ৬৪-বিট নাম | ৩২-বিট নাম | সম্পর্ক |
|---|---|---|
x0 | w0 | w0 হলো x0-এর নিচের ৩২ বিট |
x1 | w1 | w1 হলো x1-এর নিচের ৩২ বিট |
| … | … | … |
x30 | w30 | w30 হলো x30-এর নিচের ৩২ বিট (link register, নিচে দেখুন) |
মোট ৩১টা general-purpose register (x0-x30), প্রতিটার একটা ৩২-বিট view (w0-w30) — কোনো ah-এর মতো “উপরের-byte-শুধু” special-case নেই, কোনো register-ভেদে ভিন্ন সুবিধা নেই। এটাই এই লেসনের অবজেক্টিভে উল্লেখিত “একটা পরিষ্কার sub-register aliasing উদাহরণ” — একটা সহজ, uniform নিয়ম, প্রতিটা register-এর জন্য সমান।
x86-64-র মতোই ৩২-বিট (w) register-এ লেখা উপরের ৩২ বিট zero-extend করে (গত লেসনের সেই zero-extension আচরণের একটা সমান্তরাল নিয়ম ARM64-তেও বিদ্যমান — w0-এ লিখলে x0-এর উপরের ৩২ বিট স্বয়ংক্রিয়ভাবে শূন্য হয়ে যায়)।
sp — আলাদা, বিশেষ-নিয়ন্ত্রিত একটা register
Stack pointer (sp) x0-x30-এর অংশ না — এটা একটা সম্পূর্ণ আলাদা, ৩২তম physical register, বিশেষ বিধিনিষেধসহ। বেশিরভাগ সাধারণ arithmetic instruction সরাসরি sp-কে operand হিসেবে নিতে পারে না — sp-এর মান বদলাতে হলে প্রথমে একটা সাধারণ register-এ কপি করতে হয় (mov x0, sp), সেখানে গণনা করে, তারপর ফেরত sp-তে বসাতে হয় (কিছু নির্দিষ্ট instruction, যেমন add sp, sp, #16, সরাসরি sp নিয়ে কাজ করার অনুমতি পায়, কিন্তু সাধারণ arithmetic instruction-এর মতো পূর্ণ স্বাধীনতা নেই)।
xzr/wzr — একটা hardwired-শূন্য register
ARM64-তে x31-এর জায়গায় (encoding-এ যেখানে x31 বসত) প্রসঙ্গ-নির্ভরভাবে হয় sp নয়তো xzr (zero register, ৩২-বিট view wzr) — একটা register যেটা সবসময় শূন্য পড়ে, আর তাতে লেখা মানে সেই লেখা নীরবে বাতিল হয়ে যায়। এটা RISC-V-এর x0 (গত module-এর registers-pc-flags লেসনে উল্লেখিত hardwired-zero) থেকে সরাসরি ধার করা একটা নকশা-প্যাটার্ন — উভয় ISA-ই বুঝেছে “শূন্য” এত ঘন ঘন দরকার হয় (তুলনা, initialization, dummy-write) যে এটাকে একটা বিনামূল্যের, hardware-supported ধ্রুবক বানিয়ে দেওয়া উপযোগী।
x30/lr — Link Register, function-return সামলানোর ভিন্ন কৌশল
এবার এই লেসনের সবচেয়ে গুরুত্বপূর্ণ ডিজাইন-পার্থক্যে আসি। গত লেসনে দেখেছিলেন x86-64-এ call/ret সবসময় stack-এ push/pop করে return address সামলায় — কোনো ব্যতিক্রম নেই, এমনকি সবচেয়ে ছোট leaf function-এও।
ARM64 এই একই সমস্যার একটা সম্পূর্ণ ভিন্ন সমাধান বেছে নিয়েছে — x30 (alias lr, link register) নামের একটা নির্দিষ্ট, dedicated register। ফাংশন-কল instruction bl (Branch with Link) দুইটা কাজ করে: (১) পরের instruction-এর ঠিকানা সরাসরি x30/lr-এ বসায় (কোনো memory access নেই, শুধু একটা register-write), (২) target-এ jump করে। ফেরার instruction ret সরাসরি x30/lr-এর মান পড়ে PC-তে jump করে — আবারও, কোনো memory access নেই।
bl square ; x30 ← পরের instruction-এর ঠিকানা (register-write, memory না), তারপর jump square-এ
... ; ফেরত আসবে এখানে- x86-64: call targetreturn address stack-এ push (memory write, rsp কমে)
- x86-64: ... target-এর কোড চলে ...return address memory-তে বসে আছে
- x86-64: retstack থেকে pop (memory read, rsp বাড়ে), PC-তে বসে
- ARM64: bl targetreturn address সরাসরি x30/lr register-এ (memory access নেই)
- ARM64: ... target-এর কোড চলে ...return address শুধু x30 register-এ, memory-তে না (যদি target আরেকটা bl না করে)
- ARM64: retসরাসরি x30-এর মান PC-তে (আবারও, memory access নেই)
Leaf function-এর জন্য বিশাল তাৎপর্য। একটা leaf function — এমন একটা function যেটা নিজে অন্য কোনো function call করে না — ARM64-তে সম্পূর্ণ zero memory access নিয়ে সম্পন্ন হতে পারে তার নিজের entry/exit-এ: bl করলে x30-এ ঠিকানা বসে, function তার কাজ করে (শুধু register-এ, যদি memory-access দরকার না হয়), ret সরাসরি x30 থেকে ফেরত যায় — stack-এ কোনো push/pop লাগেই না। x86-64-তে একই leaf function হলেও call বাধ্যতামূলকভাবে stack-এ push করবে, ret pop করবে — এই memory traffic ISA-স্তরে বেক-ইন করা, এড়ানোর উপায় নেই।
Non-leaf function-এ কী হয়? যদি একটা function নিজে আরেকটা function call করে (bl আবার চালায়), তাহলে সমস্যা — দ্বিতীয় bl x30-কে ওভাররাইট করে দেবে, প্রথম function-এর নিজের return address হারিয়ে যাবে। তাই non-leaf function-কে নিজে থেকেই সচেতনভাবে x30-কে stack-এ সংরক্ষণ (save) করতে হয়, প্রথম নেস্টেড bl-এর আগে — সাধারণত stp (store pair) instruction দিয়ে, যা এক instruction-এ দুইটা register একসাথে push করে:
stp x29, x30, [sp, #-16]! ; frame pointer আর link register একসাথে সংরক্ষণ
bl helper_function ; এখন নিরাপদে nested call -- x30 বদলাবে, কিন্তু আমাদের কপি stack-এ নিরাপদ
...
ldp x29, x30, [sp], #16 ; ফেরত আনা
ret ; এখন x30-এ আমাদের নিজের return address, সঠিকভাবে ফেরত যাবেএটা পরের লেসনগুলোর (stack frame, prologue/epilogue) একটা পূর্বরূপ — সেখানে এই প্যাটার্ন বিস্তারিতভাবে আসবে।
ভেতরে কী ঘটছে
Load-store architecture — কঠোর নিয়ম, কংক্রিট উদাহরণ
গত module-এর risc-vs-cisc লেসনে load-store architecture-এর সংজ্ঞা এসেছিল তাত্ত্বিকভাবে — “শুধু LOAD/STORE memory স্পর্শ করে, arithmetic শুধু register-এ।” ARM64 এই নিয়ম কঠোরভাবে মেনে চলে, কোনো ব্যতিক্রম নেই — এবার একটা সম্পূর্ণ concrete উদাহরণ দিয়ে দেখি ঠিক কী পার্থক্য তৈরি করে।
একটা সহজ C statement — একটা pointer-এর মাধ্যমে একটা int বাড়ানো:
void increment(int *p) {
*p += 1;
}x86-64 (একটাই instruction, memory-তে সরাসরি read-modify-write):
add dword ptr [rdi], 1 ; একটা instruction: memory থেকে পড়ো, ১ যোগ করো, memory-তে ফেরত লেখো
retARM64 (তিনটা instruction, প্রতিটা ধাপ আলাদা, স্পষ্ট):
ldr w8, [x0] ; ধাপ ১ -- memory থেকে register-এ পড়ো (LOAD)
add w8, w8, #1 ; ধাপ ২ -- register-এই arithmetic (memory স্পর্শ নেই)
str w8, [x0] ; ধাপ ৩ -- register থেকে memory-তে লেখো (STORE)
retx86-64 (register-memory, CISC heritage):
memory ──read+modify+write──▶ memory (১টা instruction, hardware ভেতরে জটিল)
ARM64 (load-store, RISC principle):
memory ──read──▶ register ──arithmetic──▶ register ──write──▶ memory
(ldr) (add) (str)
৩টা instruction, প্রতিটা hardware সরল, uniformএটাই এই module আর গত module জুড়ে বারবার ফিরে আসা থিমের একটা নিখুঁত, হাতে-ধরা উদাহরণ — জটিলতা কোথাও না কোথাও থাকতেই হয়, প্রশ্ন শুধু কোথায়। x86-64-এ hardware-এর ভেতরে (একটা add [mem], reg-কে internally মাইক্রো-অপারেশনে ভাঙতে হয় — memory-read + add + memory-write, নাম যাই দেওয়া হোক, hardware ভেতরে ঠিক ARM64-র মতোই তিনটা ধাপ চালায়, কিন্তু প্রোগ্রামারের/compiler-এর কাছে একটাই instruction হিসেবে দেখায়)। ARM64-এ এই তিনটা ধাপ explicitly, compiler/assembler-স্তরে দৃশ্যমান — hardware সরল থাকে (প্রতিটা instruction একটাই সরল কাজ করে), কিন্তু compiler-কে বেশি instruction generate করতে হয়।
Condition flags — NZCV, x86-64-র সাথে সমান্তরাল
গত লেসনে x86-64-র flags register (ZF/CF/OF/SF) দেখেছিলেন। ARM64-র সমতুল্য — NZCV (Negative, Zero, Carry, oVerflow) — একটা PSTATE register-এর অংশ, একই ধারণা, ভিন্ন নাম আর সামান্য ভিন্ন bit-ক্রম:
| ARM64 flag | সমতুল্য x86-64 flag | অর্থ |
|---|---|---|
N | SF | ফলাফল ঋণাত্মক (sign bit) |
Z | ZF | ফলাফল শূন্য |
C | CF | Unsigned overflow/borrow |
V | OF | Signed overflow |
গুরুত্বপূর্ণ পার্থক্য — ARM64-তে সাধারণ arithmetic instruction ডিফল্টে flags বদলায় না। গত module-এর registers-pc-flags লেসনের সেই ARM64-বিশেষ নিয়ম মনে করুন — add w0, w1, w2 flags স্পর্শ করে না; শুধু S-suffix ভ্যারিয়েন্ট (adds, subs) flags সেট করে। তুলনা করার জন্য নির্দিষ্ট cmp instruction আছে (ভেতরে আসলে subs with result discarded, ঠিক x86-64-র cmp-এর মতোই ধারণাগতভাবে):
cmp w0, w1 ; w0 - w1 গণনা করে, শুধু NZCV আপডেট করে, ফলাফল ফেলে দেয়
b.lt .target ; N ≠ V হলে jump (signed "কম", ঠিক x86-64-র SF≠OF-এর সমান্তরাল)
| ARM64 conditional branch | শর্ত | সমতুল্য x86-64 |
|---|---|---|
b.eq | Z=1 | je |
b.ne | Z=0 | jne |
b.lt | N≠V | jl |
b.ge | N=V | jge |
b.gt | Z=0 এবং N=V | jg |
b.lo | C=0 (unsigned কম) | jb |
b.hi | C=1 এবং Z=0 (unsigned বেশি) | ja |
এই টেবিল গত লেসনের x86-64 flag-টেবিলের প্রায় হুবহু একটা “অনুবাদ” — signed condition (N≠V) আর x86-64-র (SF≠OF) গাণিতিকভাবে ঠিক একই যুক্তি প্রকাশ করে, শুধু flag-এর নাম ভিন্ন।
Addressing mode — load-store-এর ভেতরেই সমৃদ্ধি
Load-store architecture মানে এই না যে ARM64-র addressing সীমিত — বরং সব addressing-জটিলতা ldr/str-এর ভেতরেই কেন্দ্রীভূত। গত লেসনের sum_array উদাহরণে দেখা ldr w4, [x0, x3, lsl #2] একটা register-offset-with-shift addressing mode-এর উদাহরণ — x0 (base) + x3 (index) shifted left by ২ (lsl #2, অর্থাৎ ×৪, ঠিক x86-64-র scale-৪-এর সমতুল্য, int-এর আকারের জন্য)।
| Addressing mode | সিনট্যাক্স | ব্যবহার |
|---|---|---|
| Base register | [x0] | সরাসরি ঠিকানা |
| Base + immediate offset | [x0, #16] | struct field access |
| Base + register (scaled) | [x0, x1, lsl #2] | array indexing |
| Pre-indexed | [x0, #16]! | ঠিকানা প্রথমে বাড়িয়ে তারপর access, base আপডেট হয়ে যায় |
| Post-indexed | [x0], #16 | আগে access, তারপর base বাড়ে — loop-এ pointer-walk-এর জন্য উপযোগী |
Pre/post-indexed addressing একটা বাড়তি সুবিধা — একটা instruction-ই memory access আর pointer-update দুটোই করে ফেলে (ldr w1, [x0], #4 — পড়ো, তারপর x0 স্বয়ংক্রিয়ভাবে ৪ বাড়াও, পরের array element-এর জন্য প্রস্তুত) — একটা loop-এ আলাদা add x0, x0, #4 লেখার প্রয়োজনই থাকে না। এটা “load-store মানেই কম সুবিধাজনক” ধারণার একটা প্রতিষেধক — RISC hardware সরল রেখেও ergonomic addressing সম্ভব।
উদাহরণ
add ফাংশন — দুই ISA-তে, পাশাপাশি
সবচেয়ে ছোট সম্ভব উদাহরণ দিয়ে শুরু — দুইটা int যোগ করা:
int add(int a, int b) {
return a + b;
}x86-64 (-O2, System V ABI-তে a আসে edi, b আসে esi):
add:
lea eax, [rdi+rsi]
retকম্পাইলার এখানে গত লেসনের lea-ট্রিক ব্যবহার করেছে — সরাসরি একটা add-এর বদলে lea, কারণ lea flags স্পর্শ করে না (একটা ছোট optimization, পরের instruction-এ flags-এর দরকার না থাকলে) আর একই cycle-এ addition করে ফেলতে পারে ঠিকানা-গণনা-hardware ব্যবহার করে।
ARM64 (-O2, AAPCS64-তে a আসে w0, b আসে w1, ফলাফল w0-এ):
add:
add w0, w0, w1
retলক্ষ করুন — ARM64-র সংস্করণ এখানে এক লাইনেই সোজাসুজি, x86-64-র lea-ট্রিকের মতো কোনো পরোক্ষ কৌশল লাগেনি (কারণ ARM64-এ সাধারণ add ডিফল্টে flags-ও বদলায় না, তাই কোনো optimization-প্রয়োজন ছিল না একটা সরাসরি add-এর বদলে অন্য কিছু বেছে নেওয়ার)। আর ret-এ কোনো stack access নেই — add একটা leaf function (নিজে কাউকে call করে না), তাই x30/lr-এই return address আছে, bl-এর সময় থেকেই, কোনো push/pop লাগেনি।
sum_array — গত লেসনের ঠিক সেই ফাংশন, এবার ARM64-তে
গত লেসনে এই একই ফাংশন x86-64-এ AT&T আর Intel দুই syntax-এই দেখেছিলাম। এখন ARM64-তে — তিনটা ISA/syntax-এর পূর্ণ তুলনার সুযোগ:
int sum_array(int *arr, int n) {
int total = 0;
for (int i = 0; i \< n; i++) {
total += arr[i];
}
return total;
}AAPCS64-তে arr আসে x0-এ (pointer, তাই ৬৪-বিট), n আসে w1-এ। -O2-ঘরানার output:
sum_array:
cmp w1, #0
ble .L_end
mov w2, #0 ; total = 0
mov x3, #0 ; i = 0 (৬৪-বিটে, ঠিকানা-গণনার জন্য)
.L_loop:
ldr w4, [x0, x3, lsl #2] ; w4 = arr[i] -- base+index shifted-left-2 addressing
add w2, w2, w4 ; total += arr[i] -- সম্পূর্ণ register-এ
add x3, x3, #1 ; i++
cmp w3, w1
blt .L_loop
mov w0, w2
ret
.L_end:
mov w0, #0
retx86-64 (AT&T, গত লেসন):
movl (%rdx,%rax,4), %eax ; একটা instruction: memory থেকে পড়া
addl %eax, -4(%rbp) ; একটা instruction: সরাসরি memory-তে যোগ (register-memory!)
ARM64 (এই লেসন):
ldr w4, [x0, x3, lsl #2] ; আলাদা LOAD
add w2, w2, w4 ; আলাদা arithmetic, সম্পূর্ণ register-এএই একটা তুলনাই এই module-এর তিনটা লেসনের মূল থিম একসাথে দেখায় — একই উচ্চ-স্তরের কোড, তিনটা ভিন্ন পথে machine code হয়ে ওঠে, প্রতিটা তার নিজস্ব ISA-দর্শনের সাথে সামঞ্জস্যপূর্ণ।
-O0 বনাম -O2 — এমনকি leaf function-ও frame পায়, optimization ছাড়া
উপরের add আর sum_array উদাহরণ দুটোই -O2 (optimized) output ছিল। কৌতূহলবশত -O0 (কোনো optimization ছাড়া, ডিবাগিং-বান্ধব) দিয়ে কম্পাইল করলে add-এর মতো একটা ছোট্ট leaf function-ও একটা সম্পূর্ণ frame-setup পায়:
add: ; -O0, optimization ছাড়া
sub sp, sp, #16 ; local variable-এর জন্য stack জায়গা বরাদ্দ
str w0, [sp, #12] ; a-কে stack-এ সংরক্ষণ (unused optimization skip না করে)
str w1, [sp, #8] ; b-কে stack-এ সংরক্ষণ
ldr w0, [sp, #12] ; a আবার পড়া
ldr w1, [sp, #8] ; b আবার পড়া
add w0, w0, w1 ; আসল কাজ -- বাকি সবই "নিরাপদ কিন্তু অপ্রয়োজনীয়"
add sp, sp, #16 ; stack জায়গা ফেরত
retএই -O0 সংস্করণে add (নিজে leaf function হওয়া সত্ত্বেও) x30-কেই স্পর্শ করেনি (কারণ কোনো nested call নেই) — কিন্তু তবুও a, b-কে অপ্রয়োজনীয়ভাবে stack-এ store করে আবার load করছে, যেটা -O2-এ সম্পূর্ণ বাদ পড়ে গিয়েছিল (গত লেসনের example section-এ দেখা এক-লাইনের add w0, w0, w1 সংস্করণ)।
নিজে চালিয়ে দেখুন
Compiler Explorer দিয়ে x86-64 বনাম ARM64 পাশাপাশি দেখুন
পদ্ধতি ১ — Compiler Explorer (সবচেয়ে সহজ, কোনো ইনস্টল লাগে না):
১. godbolt.org খুলুন
২. এই লেসনের increment() ফাংশন পেস্ট করুন:
void increment(int *p) { *p += 1; }
৩. বাম প্যানেলে compiler x86-64 gcc (trunk) নির্বাচন করুন, -O2 flag দিন
৪. একটা দ্বিতীয় compiler panel যোগ করুন ("+ Add compiler"), সেখানে
ARM64 gcc (trunk) নির্বাচন করুন, একই -O2 flag
৫. দুই panel পাশাপাশি তুলনা করুন -- x86-64-এ একটা add [mem], imm,
ARM64-এ ldr/add/str তিনটা আলাদা লাইন দেখবেনপদ্ধতি ২ — নিজের মেশিনে cross-compiler (যদি ইনস্টল থাকে, Linux-এ):
# ARM64 cross-compiler ইনস্টল থাকলে (Debian/Ubuntu-তে):
# sudo apt install gcc-aarch64-linux-gnu
aarch64-linux-gnu-gcc -S -O2 increment.c -o increment_arm64.s
cat increment_arm64.s
gcc -S -O2 increment.c -o increment_x86.s
cat increment_x86.s
# তুলনা করুন -- ARM64-তে অন্তত দুইটা instruction (ldr+add+str-এর কোনো
# সমন্বয়) দেখবেন, x86-64-তে একটাই addApple Silicon Mac ব্যবহারকারীদের জন্য (নিজেই native ARM64):
gcc -S -O2 increment.c -o increment.s # সরাসরি ARM64 output, cross-compiler লাগবে না
cat increment.sload-store architecture-এর দাবি সত্যি -- একই C কোডের ARM64 output-এ arithmetic instruction কখনো memory operand ব্যবহার করে না, শুধু ldr/str করে -- এটা একটা তাত্ত্বিক নিয়ম না, প্রতিটা কম্পাইল করা ফাংশনে পর্যবেক্ষণযোগ্য প্যাটার্ন।
নিজে বানান
Load-store নিয়ম নিজে ভাঙার চেষ্টা করুন -- আর ব্যর্থ হওয়াটাই শিক্ষা
- একটা ছোট ARM64 assembly ফাইল লিখুন যেখানে ইচ্ছাকৃতভাবে একটা arithmetic instruction-এ সরাসরি memory operand দেওয়ার চেষ্টা করুন, যেমন add w0, [x1], w2 -- এটা বৈধ ARM64 syntax না
- as দিয়ে (aarch64 target) অ্যাসেম্বল করার চেষ্টা করুন, assembler error বার্তা পড়ুন
- এবার সঠিকভাবে তিন-ধাপে লিখুন -- ldr দিয়ে memory থেকে পড়া, add দিয়ে register arithmetic, str দিয়ে ফেরত লেখা
- এই সংস্করণ সফলভাবে অ্যাসেম্বল করে objdump -d দিয়ে machine code দেখুন
- গণনা করুন -- এই তিন-instruction সংস্করণ কতগুলো memory access করে (২টা -- একটা ldr, একটা str), x86-64-র এক-instruction সংস্করণ কতগুলো করত (also ২টা, ভেতরে -- কিন্তু programmer/compiler-এর কাছে একটাই instruction হিসেবে দেখা যায়)
মূল লক্ষ্য — শুধু পড়ে “load-store মানে memory-arithmetic নিষিদ্ধ” মেনে না নিয়ে, নিজে ভাঙার চেষ্টা করে, assembler-এর প্রত্যাখ্যান নিজের চোখে দেখা।
// ভুল -- ARM64-এ এমন কোনো syntax নেই (arithmetic instruction memory operand নেয় না)
add w0, [x1], w2
// সঠিক -- তিন ধাপ
ldr w3, [x1]
add w0, w3, w2
str w0, [x1]aarch64-linux-gnu-as broken.s -o broken.o
# error: expected register, got memory reference -- বা অনুরূপ সিনট্যাক্স-এরর
aarch64-linux-gnu-as fixed.s -o fixed.o # সফল
objdump -d fixed.oএকটা আকর্ষণীয় পর্যবেক্ষণ: যদিও x86-64-র add [rdi], eax “একটা instruction” দেখতে, ভেতরে CPU-কে এখনও একটা memory-read আর একটা memory-write করতেই হয় (ARM64-র মতোই মোট memory-access সংখ্যা একই) — পার্থক্যটা programmer-এর/compiler-এর কাছে দৃশ্যমান জটিলতার পরিমাণ, hardware-এর প্রকৃত কাজের পরিমাণ না। এটাই RISC/CISC-এর “জটিলতা কোথায় রাখা হবে” থিমের একটা নিখুঁত, ছোট প্রমাণ।
বাস্তব সিস্টেমে
যেখানে ARM64 প্রতিদিন চলছে
Apple Silicon (M1/M2/M3/M4)। ২০২০ সাল থেকে প্রতিটা নতুন Mac ARM64-চালিত — Apple-এর নিজস্ব ডিজাইন করা core, একই ARM64 ISA যা এই লেসনে শেখানো হয়েছে, যদিও microarchitecture সম্পূর্ণ Apple-এর নিজস্ব।
প্রায় প্রতিটা Android ফোন। Android ecosystem-এর প্রায় সম্পূর্ণটাই ARM64-চালিত (কিছু পুরনো ডিভাইস ৩২-বিট ARM/AArch32 এখনো ব্যবহার করে, কিন্তু আধুনিক সব ফ্ল্যাগশিপ AArch64)।
AWS Graviton — ক্লাউড সার্ভারে ARM64। Amazon-এর নিজস্ব ডিজাইন করা Graviton প্রসেসর সিরিজ ARM64-ভিত্তিক, বিশেষভাবে ভালো performance-per-watt-এর জন্য পরিচিত — অনেক production workload এখন x86-64 থেকে ARM64-তে স্থানান্তরিত হচ্ছে ঠিক এই কারণে, cloud খরচ কমানোর একটা বাস্তব প্রকৌশল-সিদ্ধান্ত।
Raspberry Pi 4/5 (64-bit mode)। শিক্ষামূলক এবং hobbyist কমিউনিটিতে সবচেয়ে জনপ্রিয় single-board computer, এখন ডিফল্টে ৬৪-বিট ARM64 OS চালায় — এই লেসনের সব উদাহরণ সরাসরি একটা Raspberry Pi-তে নিজে চালিয়ে দেখা সম্ভব।
iPhone-এর A-সিরিজ চিপ। Apple-এর মোবাইল প্রসেসর সিরিজ, ARM64-ভিত্তিক — একটা iPhone-এর প্রতিটা অ্যাপ শেষমেশ এই লেসনের ldr/str/add instruction-এই কম্পাইল হয়ে চলছে।
Nintendo Switch। এর Tegra প্রসেসর (Cortex-A57 cores) ARMv8-A architecture-এর, অর্থাৎ AArch64 সমর্থন করে — একটা গেম কনসোল থেকে শুরু করে সবচেয়ে শক্তিশালী cloud সার্ভার পর্যন্ত, একই মৌলিক ISA পরিবারের বিস্তৃতি দেখায়।
Rosetta 2 — গত লেসনের forward-reference-এর পূর্ণাঙ্গ প্রেক্ষাপট। Apple-এর Rosetta 2 x86-64 binary-কে (Intel Mac-এর জন্য কম্পাইল করা পুরনো software) চলার সময় ARM64 machine code-এ অনুবাদ করে (dynamic binary translation) — এই একই লেসনের সব পার্থক্য (register-memory বনাম load-store, syntax differences না হলেও ISA-স্তরের semantic গ্যাপ) তাকে সামলাতে হয় প্রতিটা translated instruction-এ, বিশেষভাবে জটিল x86-64-র সেই memory-directly-touching instruction-গুলোর জন্য, যেগুলোকে ARM64-এর একাধিক ldr/arithmetic/str-এ ভাঙতে হয় — এই লেসনের increment() উদাহরণের ঠিক এই রূপান্তরটাই Rosetta 2-কে প্রতিনিয়ত করতে হয়, রানটাইমে।
big.LITTLE / heterogeneous computing। আধুনিক ARM64 SoC (System-on-Chip)-এ প্রায়ই দুই ধরনের core থাকে — “big” (দ্রুত, বেশি power-খরচ) আর “LITTLE” (ধীর, কম power) — দুটোই একই ARM64 ISA চালায়, তাই একই compiled binary যেকোনো core-এ চলতে পারে, OS scheduler কাজের প্রকৃতি অনুযায়ী বেছে নেয় কোন core-এ পাঠাবে — একটা ISA-স্তরের uniformity যা এই ধরনের heterogeneous hardware ডিজাইন সম্ভব করে।
যে ভুলগুলো সবাই করে
“ARM64-তেও নিশ্চয়ই AT&T আর Intel-এর মতো কোনো syntax বিভাজন আছে -- যেকোনো CPU architecture-এই এমন হওয়া স্বাভাবিক।”
এই লেসনের শুরুতেই যা প্রতিষ্ঠিত হয়েছিল — এই বিভাজন x86-64-র একটা নির্দিষ্ট ঐতিহাসিক দুর্ঘটনা (Unix AT&T-ঐতিহ্য বনাম Intel-এর independently-বিকশিত official manual), সব ISA-র সহজাত বৈশিষ্ট্য না।
ARM64-এর একটাই canonical syntax — Arm Ltd-র নিজস্ব official Architecture Reference Manual-এ প্রকাশিত, আর GNU টুলচেইনসহ সব প্রধান assembler/disassembler এটাই অনুসরণ করে, কোনো প্রতিদ্বন্দ্বী কনভেনশন ছাড়াই। RISC-V-রও (গত module-এ পরিচিত) একটাই canonical syntax — এটাই বরং বেশিরভাগ আধুনিক ISA-র স্বাভাবিক অবস্থা; x86-64-র দ্বৈততাই ব্যতিক্রম, নিয়ম না।
“Load-store architecture মানে ARM64 arithmetic-এ 'দুর্বল' -- x86-64 এক instruction-এ যা করে, ARM64-তে তিনটা লাগে, তাই তিনগুণ ধীর।”
এই লেসনের hood সেকশনের warn Callout-এই এই ভুল ধারণার প্রতিষেধক দেওয়া হয়েছিল — instruction সংখ্যা সরাসরি speed-এর মাপকাঠি না (গত module-এর risc-vs-cisc লেসনেও এই একই ভুল ধারণা চিহ্নিত হয়েছিল)।
x86-64-র “এক instruction” add [mem], reg-ও ভেতরে হার্ডওয়্যার-স্তরে memory-read + arithmetic + memory-write আলাদাভাবেই করে — শুধু programmer/compiler-এর কাছে একটা instruction হিসেবে “প্যাকেজড” দেখায়। ARM64-র তিন instruction-এর প্রতিটাই সরল, uniform, দ্রুত-decode — যেখানে x86-64-র জটিল instruction-এর decode/execution hardware নিজেই বাড়তি জটিল (length-decode সমস্যা, গত module-এর risc-vs-cisc লেসনের বিষয়)। প্রকৃত পারফরম্যান্স নির্ভর করে বাস্তব pipeline depth, cache behavior, আর নির্দিষ্ট microarchitecture-এর উপর — কোনো সরল instruction-গণনা-ভিত্তিক সিদ্ধান্তে পৌঁছানো ভুল।
“Link register (x30/lr) মানে ARM64-তে function call-এ কখনো stack লাগে না -- পুরো stack ধারণাটাই এখানে অপ্রয়োজনীয়।”
এই লেসনের concept সেকশনেই স্পষ্ট করা হয়েছিল — এই সুবিধা শুধু leaf function-এর জন্য প্রযোজ্য।
যেকোনো non-leaf function (যেটা নিজে অন্তত একটা function call করে) অবশ্যই তার নিজের x30 stack-এ সংরক্ষণ করতে হবে, প্রথম nested bl-এর আগে — নাহলে সেই nested call নিজের return address x30-এ লিখে আগেরটা মুছে ফেলবে। বাস্তব প্রোগ্রামে বেশিরভাগ function-ই non-leaf (একে অপরকে call করে), তাই ARM64-তে stack ব্যবহার এখনো সর্বব্যাপী — শুধু leaf function-এর একটা নির্দিষ্ট, সাধারণ কিন্তু সার্বজনীন-না উপশ্রেণির জন্য এই “stack-মুক্ত call” সুবিধাটা প্রযোজ্য।
“w0-w30 আর x0-x30 আসলে ভিন্ন, স্বাধীন রেজিস্টার -- ঠিক যেমন x86-64-এ eax আর rax কিছুটা ভিন্ন সত্তা মনে হতে পারে।”
এটা একটা ভুল সমান্তরাল টানা — ARM64-তে w0 আর x0 একই একটা physical register-এর দুইটা ভিন্ন-প্রস্থ view, x86-64-র মতোই আসলে (eax/raxও একই physical register-এর view), কিন্তু ARM64-র নিয়মটা অনেক বেশি uniform আর predictable — প্রতিটা xN-এর ঠিক একটা wN counterpart (নিচের ৩২ বিট), কোনো exception নেই, কোনো ah-এর মতো বিশেষ upper-byte-only view নেই, কোনো r8-r15-এর মতো “নতুন register, পুরনো ঐতিহাসিক নাম নেই” অসামঞ্জস্যও নেই।
এই সরলতাই এই লেসনের objective-এ উল্লেখিত মূল শিক্ষণীয় পয়েন্ট — sub-register aliasing ধারণাগতভাবে দুই ISA-তেই এক (একই physical storage-এর ভিন্ন-প্রস্থ access), কিন্তু ARM64-র বাস্তবায়ন x86-64-র চেয়ে অনেক বেশি clean, ঐতিহাসিক জঞ্জাল-মুক্ত।
বুঝেছেন কি না দেখুন
1ARM64-তে w5-এ 0xFFFFFFFF লেখা হলো। এর আগে x5 ছিল 0x123456789ABCDEF0। এখন x5-এর মান কত?
স্মরণ
w5-এ 0xFFFFFFFF লেখা হলো। এর আগে x5 ছিল 0x123456789ABCDEF0। এখন x5-এর মান কত?x5 = 0x00000000FFFFFFFF
গত লেসনের x86-64-র zero-extension নিয়মের ঠিক সমান্তরাল একটা নিয়ম ARM64-তেও প্রযোজ্য — একটা ৩২-বিট sub-register (w5)-এ লেখা স্বয়ংক্রিয়ভাবে ৬৪-বিট register (x5)-এর উপরের ৩২ বিট শূন্য করে দেয়। তাই আগের 0x12345678 (উপরের অর্ধেক) হারিয়ে যায়, নতুন মান হয় 0x00000000FFFFFFFF — শুধু নিচের ৩২ বিট (0xFFFFFFFF) থেকে যায়, উপরের বিট শূন্যে রিসেট হয়।
2নিচের ARM64 assembly-তে একটা সিনট্যাক্স ভুল আছে — এটা assemble করার চেষ্টা করলে error দেবে। কোন লাইনে সমস্যা, আর কেন?
myFunc:
ldr w0, [x1]
add w0, [x1], w2
str w0, [x1]
ret
যুক্তি
myFunc:
ldr w0, [x1]
add w0, [x1], w2
str w0, [x1]
retদ্বিতীয় লাইন (add w0, [x1], w2) ভুল — এটা load-store architecture-এর একটা মৌলিক নিয়ম ভাঙছে।
ARM64-এ add একটা arithmetic instruction — এই ধরনের instruction কখনোই সরাসরি memory operand ([x1]-এর মতো bracket-syntax) নিতে পারে না। শুধু ldr/str (আর তাদের কিছু নির্দিষ্ট আত্মীয়, যেমন ldp/stp) memory স্পর্শ করার অনুমতি পায়। এটা এই লেসনের হুড সেকশনের কেন্দ্রীয় নিয়মের সরাসরি লঙ্ঘন — x86-64-এ add [mem], reg-এর মতো একটা instruction সম্পূর্ণ বৈধ, কিন্তু ARM64-এ এমন কিছুর অস্তিত্বই নেই।
সঠিক সংস্করণ (মনে রাখুন — মূল উদ্দেশ্যটা সম্ভবত *x1 += w2 করা ছিল):
myFunc:
ldr w0, [x1] ; load
add w0, w0, w2 ; register-only arithmetic
str w0, [x1] ; store
ret(লক্ষ করুন প্রথম আর তৃতীয় লাইন আগেই সঠিক ছিল — শুধু মাঝের arithmetic লাইনটাই ভুলভাবে একটা memory operand অন্তর্ভুক্ত করার চেষ্টা করেছিল।)
3একটা ARM64 function f() নিজে কোনো অন্য function call করে না (leaf function)। এটা কল হওয়ার সময় (bl f) আর ফেরত আসার সময় (ret) মোট কতটা memory access হয় — শুধু return-address সামলানোর জন্য (function-এর নিজস্ব কাজের memory access বাদ দিয়ে)? x86-64-তে একই leaf function-এর জন্য এই সংখ্যাটা কত হতো?
প্রয়োগ
f() নিজে কোনো অন্য function call করে না (leaf function)। এটা কল হওয়ার সময় (bl f) আর ফেরত আসার সময় (ret) মোট কতটা memory access হয় — শুধু return-address সামলানোর জন্য (function-এর নিজস্ব কাজের memory access বাদ দিয়ে)? x86-64-তে একই leaf function-এর জন্য এই সংখ্যাটা কত হতো?ARM64: শূন্য (0) memory access।
bl f return address সরাসরি x30/lr register-এ বসায় — এটা একটা register-write, memory-access না। ret সরাসরি x30-এর মান পড়ে PC-তে বসায় — এটাও একটা register-read, memory-access না। যেহেতু f() leaf function (নিজে bl চালায় না), তার x30 কখনো ওভাররাইট হওয়ার ঝুঁকি নেই, তাই তাকে stack-এ সংরক্ষণ করারও দরকার নেই — সম্পূর্ণ call/return cycle-এ return-address-সংক্রান্ত কোনো memory access ঘটে না।
x86-64: দুইটা (2) memory access।
call f return address অবশ্যই stack-এ push করে — একটা memory write (rsp কমিয়ে, সেই ঠিকানায় লেখা)। ret সেই ঠিকানা stack থেকে pop করে — একটা memory read। x86-64-র ISA-design-এ এই push/pop বাধ্যতামূলক, leaf function হোক বা না হোক — কোনো ব্যতিক্রম নেই।
এই পার্থক্যটাই (২ বনাম ০) এই লেসনের link register আলোচনার কেন্দ্রীয় পরিমাণগত পয়েন্ট — বাস্তব প্রোগ্রামে অসংখ্য ছোট leaf function (getter, helper, recursive base case) থাকে, আর প্রতিটাতে ARM64 এই memory-access সম্পূর্ণ এড়িয়ে যায়, যেখানে x86-64-কে (আধুনিক Return-Stack-Buffer prediction থাকা সত্ত্বেও) প্রতিটাতেই আসল memory traffic বহন করতে হয়।
4আপনি একটা recursive function ডিজাইন করছেন যা নিজেকেই বারবার call করে (যেমন factorial বা Fibonacci)। ARM64-তে, এই function-এর প্রতিটা recursive call-এ কি x30 stack-এ সংরক্ষণ করা লাগবে? যুক্তি দিন।
ডিজাইন
x30 stack-এ সংরক্ষণ করা লাগবে? যুক্তি দিন।হ্যাঁ, প্রতিটা recursive call-এই x30 stack-এ সংরক্ষণ করা লাগবে — কারণ একটা recursive function সংজ্ঞা অনুযায়ীই non-leaf: এটা নিজেকে (বা অন্তত একটা function-কে, এক্ষেত্রে নিজেকেই) call করে, তাই এটা leaf function-এর সেই বিশেষ “stack-মুক্ত” সুবিধা পায় না।
কেন প্রয়োজন, ধাপে ধাপে:
factorial(n) যখন নিজেকে factorial(n-1) হিসেবে call করে (একটা bl factorial instruction দিয়ে), সেই bl-এর কারণে x30-এ নতুন একটা return address বসে যাবে (এই recursive call-এর জন্য) — কিন্তু factorial(n)-এর নিজের return address (যেখানে তাকে ফিরে যেতে হবে, তার caller-এর কাছে) তখন সেই একই x30-এই ছিল, ওভাররাইট হয়ে যাবে যদি আগে সংরক্ষণ না করা হয়।
তাই factorial-এর prologue-এ (function শুরুতে) x30-কে stack-এ push করতে হবে (সাধারণত stp x29, x30, [sp, #-16]! দিয়ে, frame pointer-এর সাথে একসাথে), recursive bl factorial call করার আগেই। প্রতিটা recursion-স্তর তার নিজস্ব stack frame-এ নিজস্ব x30-এর কপি রাখে — n স্তরের recursion মানে nটা আলাদা সংরক্ষিত return address, স্বাভাবিকভাবেই stack-এর মতোই (LIFO) সংগঠিত।
সংক্ষেপে: link register-এর “memory-মুক্ত” সুবিধা শুধুই leaf function-এ প্রযোজ্য একটা optimization — recursive function-এর মতো inherently non-leaf ক্ষেত্রে ARM64-ও x86-64-র মতোই stack-নির্ভর, কোনো “জাদু” এড়ানোর উপায় নেই।
5নিচের ARM64 assembly-তে b.lt-এর ঠিক আগে কোন instruction চলেছিল বলে ধরে নেওয়া যায়, আর N/V flag-এর কোন combination-এ jump নেওয়া হবে?
cmp w0, w1
b.lt .target
যুক্তি
b.lt-এর ঠিক আগে কোন instruction চলেছিল বলে ধরে নেওয়া যায়, আর N/V flag-এর কোন combination-এ jump নেওয়া হবে? cmp w0, w1
b.lt .targetcmp w0, w1 অবশ্যই b.lt-এর ঠিক আগে চলেছিল — কারণ b.lt NZCV flags পড়ে সিদ্ধান্ত নেয়, আর flags শুধু cmp (বা S-suffix arithmetic, যেমন adds, subs)-এর মতো instruction দিয়েই আপডেট হয় (এই লেসনের হুড সেকশনে দেখানো হয়েছিল — ARM64-এ সাধারণ add/sub ডিফল্টে flags বদলায় না)।
cmp w0, w1 ভেতরে w0 - w1 গণনা করে (ফলাফল ফেলে দেয়, শুধু flags বসায়)। b.lt জাম্প নেবে যখন N ≠ V (এই লেসনের condition-টেবিল অনুযায়ী, signed “কম” নির্ধারণের নিয়ম) — অর্থাৎ w0 \< w1 (signed তুলনায়) হলে।
এই নিয়মটা গত লেসনের x86-64-র jl (SF ≠ OF হলে jump) নিয়মের গাণিতিকভাবে হুবহু সমতুল্য — শুধু flag-নাম ভিন্ন (N/V বনাম SF/OF), অন্তর্নিহিত hardware-যুক্তি অভিন্ন: signed subtraction-এর sign bit যদি “প্রত্যাশিত” overflow bit-এর সাথে না মেলে, প্রকৃত গাণিতিক ফলাফল ঋণাত্মক।
এরপর কী
পরের বিষয় — Register ব্যবহার একটা কনভেনশনে পরিণত হওয়া
এই তিনটা লেসন জুড়ে আমরা দেখেছি pipeline (assembler-linker-loader), তারপর দুইটা প্রতিদ্বন্দ্বী কিন্তু সমমূল্যের syntax (AT&T/Intel), আর এখন একটা সম্পূর্ণ ভিন্ন ISA-দর্শন (ARM64-র load-store, single-syntax approach) — তিনটাই এই module-এর driving question-এর একেকটা টুকরো উত্তর দিয়েছে: আমার C function compile হয়ে ঠিক কোন instruction-গুলো হলো, আর কেন?
কিন্তু একটা প্রশ্ন এখনো অনুত্তরিত রয়ে গেছে, যা এই লেসনের link register আলোচনায় বারবার ছুঁয়ে গেছে — যখন একটা function call হয় (call/bl), argument (যেমন a, b) ঠিক কোন register-এ বসে? কে ঠিক করে a edi/w0-এ যাবে, b esi/w1-এ? কোন register caller-কে (কল-কারীকে) সংরক্ষণ করতে হয়, কোনটা callee-কে (কল-হওয়া ফাংশনকে)? এই প্রশ্নগুলোর উত্তর কোনো hardware-নিয়ম না — এটা একটা সফটওয়্যার কনভেনশন, যাকে বলে calling convention (x86-64-এ System V ABI, ARM64-এ AAPCS64) — module-এর পরবর্তী বিষয়গুলোর একটা, যেখানে এই লেসনগুলোতে যা “ধরে নেওয়া হয়েছিল” (যেমন a edi-তে আসে) তার প্রকৃত নিয়ম-বই উন্মোচিত হবে, সাথে stack frame, prologue/epilogue-এর সম্পূর্ণ প্রক্রিয়া — এই লেসনের stp x29, x30-এর মতো প্যাটার্নগুলো তখন তাদের পূর্ণাঙ্গ প্রেক্ষাপট পাবে।
আরও পড়ুন
- Arm Architecture Reference Manual for A-profile Architecture — Arm Limited · AArch64 instruction set-এর প্রামাণ্য, সম্পূর্ণ বর্ণনা — এই লেসনের সব instruction encoding এখান থেকে যাচাইযোগ্য
- Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64) — Arm Limited · Register ব্যবহারের কনভেনশন (x0-x7 argument, x30 link register, callee-saved সেট) — পরের লেসনগুলোর ভিত্তি
- Computer Organization and Design (RISC-V Edition) — RISC ISA design principle-এর সাধারণ আলোচনা — David A. Patterson, John L. Hennessy · Load-store architecture-এর সাধারণ RISC-দর্শন, যা ARM64-তেও প্রযোজ্য
- Compiler Explorer (godbolt.org) · এই লেসনের experiment-এ ARM64 target সিলেক্ট করে x86-64-র সাথে পাশাপাশি তুলনার জন্য ব্যবহৃত