SIMD — একটা Instruction, বহু Data
SIMD — Single Instruction, Multiple Data
একটা instruction একসাথে বহু data element-এ কাজ করে — parallelism-এর সম্পূর্ণ ভিন্ন একটা অক্ষ, যা multimedia থেকে আজকের machine learning পর্যন্ত সবখানে লুকিয়ে আছে।
আগে এটা বুঝি
আগের লেসনে আমরা দেখলাম CPU কীভাবে ভিন্ন ভিন্ন instruction একসাথে চালানোর চেষ্টা করে — superscalar issue, out-of-order execution, register renaming — সবকিছুই একটা লক্ষ্যে: প্রতি cycle-এ যতটা সম্ভব বেশি আলাদা কাজ সম্পন্ন করা।
এই লেসনে সম্পূর্ণ ভিন্ন একটা প্রশ্ন। ধরুন আপনার কাছে দুটো array আছে, প্রতিটায় আটটা float, আর আপনি চান element-by-element যোগ:
a = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]
b = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
result = a + b # প্রতিটা জোড়া আলাদাভাবে যোগScalar পদ্ধতিতে (যা এই module-এর এখন পর্যন্ত সব লেসনে ধরে
নেওয়া হয়েছে) এটা আটটা আলাদা ADD instruction লাগবে — একটার
পর একটা (বা out-of-order হলে কাছাকাছি সময়ে, কিন্তু তবু আটটা
আলাদা instruction, আটটা আলাদা register-জোড়া নিয়ে)।
কিন্তু লক্ষ্য করুন — এই আটটা addition-এর মধ্যে কোনো dependency নেই। প্রতিটাই সম্পূর্ণ independent, আর প্রতিটাই ঠিক একই অপারেশন (addition) করছে, শুধু ভিন্ন ভিন্ন data-তে। এটা এমন একটা প্যাটার্ন যা কম্পিউটিং-এ সর্বত্র — audio sample, pixel color, matrix entry, neural network-এর weight — একই গাণিতিক অপারেশন হাজার হাজার বার, ভিন্ন সংখ্যার উপর।
এই প্যাটার্নের জন্য CPU ডিজাইনাররা একটা সম্পূর্ণ ভিন্ন ধরনের
instruction তৈরি করেছেন: একটাই instruction, যেটা একসাথে বহু
data element-এ কাজ করে। একে বলে SIMD — Single Instruction,
Multiple Data। উপরের উদাহরণে, একটা মাত্র vaddps instruction
আটটা float একসাথে যোগ করে দেবে — আটটা আলাদা instruction না,
একটা।
এটা parallelism-এর একটা নতুন অক্ষ। আগের লেসনগুলো ছিল “কীভাবে বিভিন্ন কাজ একসাথে করা যায়” (instruction-level parallelism)। এই লেসন হলো “কীভাবে একটা কাজ বহুগুণ করা যায়” (data-level parallelism)। দুটো সম্পূর্ণ স্বাধীন কৌশল, আর আধুনিক CPU দুটোই একসাথে ব্যবহার করে।
মূল ধারণা
দুইটা ভিন্ন অক্ষ — গুলিয়ে ফেলবেন না
এটা এই লেসনের সবচেয়ে গুরুত্বপূর্ণ পার্থক্য, তাই শুরুতেই স্পষ্ট করি।
| Superscalar / Out-of-order (আগের লেসন) | SIMD (এই লেসন) | |
|---|---|---|
| কী parallelize হয় | ভিন্ন instruction | একই instruction, ভিন্ন data-তে |
| Instruction সংখ্যা | একাধিক instruction fetch/issue হয় | একটা instruction, কিন্তু বহু data element ধারণ করে |
| Hardware | একাধিক execution unit, scheduler, ROB | একটাই wide execution unit, wide register |
| উদাহরণ | ADD R1,R2,R3 আর MUL R4,R5,R6 একসাথে | vaddps আটটা float একসাথে যোগ করে |
| কার্যকর হয় কখন | Independent, ভিন্ন ধরনের কাজ থাকলে | একই operation বহু element-এ প্রয়োগ করতে হলে |
দুটো একে অপরের বিকল্প না, পরিপূরক। একটা modern CPU একই সময়ে একাধিক SIMD instruction out-of-order চালাতে পারে — দুটো কৌশলই একসাথে কাজ করে, একটা আরেকটার উপরে স্তরিত।
Packed register — মূল hardware ধারণা
SIMD-এর ভিত্তি হলো packed register — একটা register যা একটা একক বড় সংখ্যা না রেখে, ভেতরে ভাগ হয়ে একাধিক ছোট সংখ্যা একসাথে রাখে।
সাধারণ ৬৪-bit register (একটা মান):
┌────────────────────────────────────────────────────────────┐
│ একটা 64-bit integer │
└────────────────────────────────────────────────────────────┘
২৫৬-bit AVX register, আটটা 32-bit float হিসেবে "packed":
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ float0 │ float1 │ float2 │ float3 │ float4 │ float5 │ float6 │ float7 │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
৩২bit ৩২bit ৩২bit ৩২bit ৩২bit ৩২bit ৩২bit ৩২bitএকটা vaddps ymm0, ymm1, ymm2 instruction পুরো ২৫৬ bit-কে
আটটা 32-bit “লেন” হিসেবে দেখে, আর প্রতিটা লেনে একই সাথে,
একই cycle-এ addition চালায়। এক instruction, আট addition —
scalar কোডে যেটা আটটা আলাদা instruction (এবং আটটা আলাদা issue
slot) লাগত।
x86-এর বিবর্তন — একটা বাস্তব ইতিহাস
x86-এ SIMD একদিনে আসেনি — প্রতিটা প্রজন্ম একটা নির্দিষ্ট বাস্তব চাহিদার জবাবে এসেছে।
| প্রজন্ম | বছর | Register প্রস্থ | নতুন কী | মূল প্রেরণা |
|---|---|---|---|---|
| MMX | ১৯৯৭ | ৬৪-bit (MM0-MM7) | Integer-only SIMD | Multimedia — video/audio decode |
| SSE | ১৯৯৯ | ১২৮-bit (XMM0-XMM7) | আলাদা FP register, single-precision float | 3D graphics, geometry |
| SSE2 | ২০০১ | ১২৮-bit | Double-precision, integer একসাথে | সাধারণীকরণ, Pentium 4-তে বেসলাইন |
| SSE3/SSSE3/SSE4 | ২০০৪-০৬ | ১২৮-bit | নতুন shuffle/horizontal ops | Video codec অপ্টিমাইজেশন |
| AVX | ২০১১ | ২৫৬-bit (YMM0-YMM15) | তিন-operand form, শুধু float | HPC, scientific computing |
| AVX2 | ২০১৩ | ২৫৬-bit | Integer ops-ও ২৫৬-bit-এ সম্প্রসারিত | সাধারণীকরণ |
| AVX-512 | ২০১৬+ | ৫১২-bit (ZMM0-ZMM31) | মাস্ক register, আরও বেশি lane | ML inference, HPC |
ARM-এর পথ — NEON
ARM আর্কিটেকচারেও সমান্তরাল একটা বিবর্তন হয়েছে — NEON,
ARMv7-এ চালু হয় আর ARMv8 (AArch64)-এ standard অংশ হয়ে যায়
(x86-এর মতো ঐচ্ছিক না)। NEON-এর ১২৮-bit register (Q0-Q15,
প্রতিটা দুইটা ৬৪-bit D register হিসেবেও দেখা যায়) অনেকটা
SSE-এর সমতুল্য। পরবর্তীতে SVE/SVE2 (Scalable Vector Extension)
এসেছে — এর একটা আকর্ষণীয় ধারণাগত পার্থক্য: SVE instruction
register-এর প্রকৃত প্রস্থ hardware-নির্দিষ্ট রাখে (১২৮ থেকে
২০৪৮ bit পর্যন্ত বিভিন্ন implementation), আর একই compiled binary
বিভিন্ন প্রস্থের hardware-এ পুনরায় compile ছাড়াই চলতে পারে —
x86-এর প্রতিটা প্রজন্মে নতুন instruction set (SSE→AVX→AVX-512)
যোগ করার পদ্ধতির থেকে আলাদা দর্শন।
Apple-এর M-সিরিজ চিপ NEON ব্যবহার করে, আর সেখানেও AI/ML workload-এর জন্য বিশেষ matrix-multiply extension (AMX) যোগ হয়েছে — এটাই পরের ভাগের বিষয়ের দিকে ইঙ্গিত করে।
ভেতরে কী ঘটছে
কোথায় SIMD সত্যিই কাজে লাগে — আর কোথায় লাগে না
SIMD-এর পুরো লাভ একটা শর্তের উপর দাঁড়িয়ে: একই operation সত্যিই সব element-এ সমানভাবে প্রযোজ্য হতে হবে, কোনো element-নির্ভর branching ছাড়া। একে বলে data parallelism।
যেখানে চমৎকার কাজ করে
// প্রতিটা element-এ ঠিক একই কাজ — uniform, branch-free
for (int i = 0; i \< n; i++) {
c[i] = a[i] + b[i];
}প্রতিটা iteration সম্পূর্ণ একই কাজ করছে, কোনো condition নেই,
আর একটা iteration আরেকটার উপর নির্ভর করে না। এটা আদর্শ SIMD
candidate — compiler বা programmer সহজেই এই লুপকে একটা vaddps-এর
মতো instruction-এ রূপান্তর করতে পারে।
যেখানে ব্যর্থ হয় (বা কষ্টকর হয়ে যায়)
// প্রতিটা element ভিন্ন পথে যেতে পারে — data-dependent branch
for (int i = 0; i \< n; i++) {
if (a[i] > 0) {
c[i] = a[i] * 2;
} else {
c[i] = -a[i];
}
}এখানে সমস্যা: SIMD একটা instruction-এ সব লেন একই কাজ করে —
কোনো লেন if-এর branch নেয়, আরেকটা লেন else-এর branch নেয়,
এটা সরাসরি সম্ভব না (একটা মাত্র instruction pointer, সব লেনের
জন্য একটাই)। সমাধান হলো both পথ চালানো, আর mask দিয়ে
বেছে নেওয়া (predicated execution) — অর্থাৎ প্রতিটা লেনে দুটোই
হিসাব হয় (a[i]*2 আর -a[i]), তারপর একটা mask register
(কোন লেনে a[i] > 0 সত্য) ব্যবহার করে সঠিক ফলাফল বেছে নেওয়া
হয়। এটা কাজ করে, কিন্তু কার্যত উভয় path-এর কাজ করতে হয় —
scalar branch-এর তুলনায় সুবিধা অনেক কমে যায়, বিশেষ করে যদি
branch-এর দুই দিকের কাজ ভারী হয়।
সব লেনে '*2' হিসাব করা হয় (নিরর্থক লেনেও)
↓
সব লেনে '-a[i]' হিসাব করা হয় (নিরর্থক লেনেও)
↓
mask = (a[i] > 0) দিয়ে সঠিক ফলাফল বেছে নেওয়াআর একটা এলোমেলো memory access প্যাটার্ন (যেমন c[index[i]] = a[i],
যেখানে index[i] অনির্দিষ্ট) SIMD-এর জন্য মৌলিকভাবে কঠিন — কারণ
SIMD load/store সাধারণত contiguous, সংলগ্ন memory আশা করে
(যদিও “gather/scatter” নামের বিশেষ instruction পরবর্তী AVX2/AVX-512-এ
এসেছে, সেগুলো নিয়মিত load-এর চেয়ে অনেক ধীর)।
Auto-vectorization — কম্পাইলার কীভাবে চেষ্টা করে, আর কেন ব্যর্থ হয়
আধুনিক কম্পাইলার (gcc -O3, clang -O3) স্বয়ংক্রিয়ভাবে
scalar loop-কে SIMD instruction-এ রূপান্তরের চেষ্টা করে —
একে বলে auto-vectorization। কিন্তু এটা প্রায়ই নীরবে
ব্যর্থ হয় — প্রোগ্রাম compile হয়ে যায়, কোনো error দেখায় না,
কিন্তু আসলে vectorize হয়নি।
সাধারণ কারণ:
১. Pointer aliasing-এর অনিশ্চয়তা। C-তে দুটো পয়েন্টার একে অন্যের সাথে overlap করতে পারে কি না কম্পাইলার সবসময় নিশ্চিতভাবে জানে না:
void add(float *c, float *a, float *b, int n) {
for (int i = 0; i \< n; i++) c[i] = a[i] + b[i];
}যদি c আর a একই memory-র অংশ হয় (overlap করে), তাহলে
vectorize করা (একসাথে ৮টা element প্রসেস করা, যা memory access-এর
ক্রম বদলে দেয়) ভুল ফলাফল দিতে পারে। কম্পাইলার নিশ্চিত না হয়ে
ঝুঁকি নেবে না — তাই vectorize করবে না, যদি না প্রোগ্রামার
restrict keyword দিয়ে নিশ্চয়তা দেয়:
void add(float *restrict c, float *restrict a, float *restrict b, int n) {
for (int i = 0; i \< n; i++) c[i] = a[i] + b[i]; // এখন vectorize করা নিরাপদ
}২. Loop-carried dependency। একটা iteration যদি আগের iteration-এর ফলাফলের উপর নির্ভর করে (যেমন একটা running sum), vectorization জটিল হয়ে যায় (যদিও reduction pattern-এর জন্য বিশেষ কৌশল আছে, সহজ ক্ষেত্রে কম্পাইলার তা প্রয়োগ করতে পারে)।
৩. Alignment আর tail handling। যদি array-র দৈর্ঘ্য SIMD
প্রস্থের গুণিতক না হয় (যেমন n=17 কিন্তু vector প্রস্থ ৮),
শেষের বাকি element-গুলো আলাদাভাবে scalar কোডে সামলাতে হয় —
কম্পাইলার এটা করতে পারে, কিন্তু জটিলতা বাড়ায়।
৪. জটিল control flow, function call ভেতরে। একটা loop-এর ভেতরে যদি কোনো function call থাকে (যার side effect অনিশ্চিত) বা জটিল branching থাকে, কম্পাইলার সাধারণত হাল ছেড়ে দেয়।
এই কারণেই performance-critical কোডে দুটো বিকল্প থাকে: কম্পাইলার
hint (restrict, #pragma omp simd, বা -ffast-math-এর
মতো flag যা কিছু নিরাপত্তা শিথিল করে) দিয়ে auto-vectorization-কে
সাহায্য করা, অথবা সরাসরি intrinsics — C function-এর মতো
দেখতে কিন্তু সরাসরি একটা নির্দিষ্ট SIMD instruction-এ compile
হওয়া বিশেষ ফাংশন — হাতে লেখা।
#include \<immintrin.h>
void add_avx(float *restrict c, float *restrict a, float *restrict b, int n) {
int i = 0;
for (; i + 8 \<= n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_add_ps(va, vb); // ঠিক এখানেই vaddps instruction
_mm256_storeu_ps(&c[i], vc);
}
for (; i \< n; i++) c[i] = a[i] + b[i]; // বাকি element scalar-এ
}এখানে প্রোগ্রামার নিজেই নিশ্চিত করছেন correctness (aliasing, alignment), আর সরাসরি hardware instruction নির্দিষ্ট করছেন — কম্পাইলারের সিদ্ধান্তের উপর নির্ভর করছেন না।
উদাহরণ
একটা সম্পূর্ণ ট্রেস — dot product
একটা বাস্তব, প্রায়ই ব্যবহৃত অপারেশন দিয়ে scalar বনাম SIMD তুলনা করি — dot product (দুটো vector-এর element-wise গুণ তারপর যোগফল), যা linear algebra আর machine learning-এর ভিত্তি।
Scalar সংস্করণ:
float dot_scalar(float *a, float *b, int n) {
float sum = 0.0f;
for (int i = 0; i \< n; i++) {
sum += a[i] * b[i]; // একটা multiply, একটা add, প্রতি iteration
}
return sum;
}n = 1024-এ, এটা ১০২৪টা multiply, ১০২৪টা add — মোট ২০৪৮টা
scalar floating-point instruction (compiler ILP-এর জন্য কিছু
পুনর্বিন্যাস করলেও, মৌলিক কাজের পরিমাণ একই)।
AVX সংস্করণ (ধারণাগত):
#include \<immintrin.h>
float dot_avx(float *a, float *b, int n) {
__m256 acc = _mm256_setzero_ps(); // ৮টা 0.0f
int i = 0;
for (; i + 8 \<= n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
acc = _mm256_fmadd_ps(va, vb, acc); // ৮টা multiply+add, এক instruction
}
// acc-এর ৮টা lane এখন একসাথে যোগ করতে হবে (horizontal sum)
float tmp[8];
_mm256_storeu_ps(tmp, acc);
float sum = tmp[0]+tmp[1]+tmp[2]+tmp[3]+tmp[4]+tmp[5]+tmp[6]+tmp[7];
for (; i \< n; i++) sum += a[i] * b[i]; // বাকি element
return sum;
}n = 1024-এ এখন মূল loop চলে মাত্র 1024/8 = 128 বার, প্রতিবার
একটা vfmadd231ps instruction (fused multiply-add — একটা
instruction-এই multiply আর add দুটোই, আরেকটা optimization যা
এই লেসনের পরিধির বাইরে কিন্তু বাস্তবে সবসময় একসাথে ব্যবহৃত হয়)।
মূল কাজ: ১২৮টা vector instruction, বনাম ২০৪৮টা scalar instruction —
তাত্ত্বিক ১৬ গুণ কম instruction (৮-লেন × FMA-এর দ্বিগুণ কাজ)।
লেন: 0 1 2 3 4 5 6 7
a[i..i+7]: a₀ a₁ a₂ a₃ a₄ a₅ a₆ a₇
b[i..i+7]: b₀ b₁ b₂ b₃ b₄ b₅ b₆ b₇
× × × × × × × ×
+ + + + + + + +
acc₀ acc₁ acc₂ acc₃ acc₄ acc₅ acc₆ acc₇
(আটটা independent accumulator — hardware-এ একই cycle-এ সম্পন্ন)বাস্তব speedup সবসময় ১৬×-এর কম কেন — একটা গুরুত্বপূর্ণ সতর্কতা:
এই তাত্ত্বিক হিসাব ধরে নেয় CPU instruction-bound (compute-bound)।
বাস্তবে dot_scalar-ও ইতিমধ্যে out-of-order execution আর
compiler-এর নিজস্ব ILP-ব্যবহার থেকে সুবিধা পায় (আগের লেসনগুলো
থেকে মনে করুন)। আর memory bandwidth প্রায়ই আসল সীমাবদ্ধতা হয়ে
দাঁড়ায় — data এত দ্রুত memory থেকে আনা সম্ভব না যতটা দ্রুত
SIMD ইউনিট প্রসেস করতে পারে। তাই বাস্তব measured speedup সাধারণত
৩-৮× হয়, তাত্ত্বিক ১৬× না — পরের ভাগের experiment-এ এটা সরাসরি
মাপা হবে।
নিজে চালিয়ে দেখুন
Auto-vectorization চালু/বন্ধ করে গতির পার্থক্য মাপুন
// dotprod.c
#include \<stdio.h>
#include \<stdlib.h>
#include \<time.h>
#define N (1 \<\< 24) // ~১৬.৭ মিলিয়ন element
float dot(float *a, float *b, int n) {
float sum = 0.0f;
for (int i = 0; i \< n; i++) {
sum += a[i] * b[i];
}
return sum;
}
int main(void) {
float *a = malloc(N * sizeof(float));
float *b = malloc(N * sizeof(float));
for (int i = 0; i \< N; i++) { a[i] = 1.0001f; b[i] = 0.9999f; }
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
float result = 0;
for (int r = 0; r \< 20; r++) result += dot(a, b, N); // repeats, noise কমাতে
clock_gettime(CLOCK_MONOTONIC, &t1);
double dt = (t1.tv_sec - t0.tv_sec) + (t1.tv_nsec - t0.tv_nsec) / 1e9;
printf("result=%f time=%.4fs\n", result, dt);
return 0;
}তিনটা ভিন্ন compile flag দিয়ে তুলনা করি:
# ১. vectorization সম্পূর্ণ বন্ধ, pure scalar
gcc -O2 -fno-tree-vectorize -o dot_scalar dotprod.c
./dot_scalar
# ২. স্বাভাবিক -O3, auto-vectorization চালু (baseline SSE2, x86-64 default)
gcc -O3 -o dot_auto dotprod.c
./dot_auto
# ৩. -march=native দিয়ে — এই CPU-তে যতটা চওড়া SIMD সম্ভব (হয়তো AVX2/AVX-512)
gcc -O3 -march=native -o dot_native dotprod.c
./dot_nativeআর সবচেয়ে গুরুত্বপূর্ণ ধাপ — assembly output দেখে প্রমাণ করা compiler সত্যিই vector instruction ব্যবহার করেছে কি না:
gcc -O3 -march=native -S -o dot_native.s dotprod.c
grep -E "vaddps|vmulps|vfmadd" dot_native.sসাধারণ ফলাফল (একটা AVX2-সক্ষম CPU-তে):
scalar (-fno-tree-vectorize): time=0.412s
auto SSE2 (-O3 default): time=0.198s (~2.1× দ্রুত)
auto AVX2 (-march=native): time=0.108s (~3.8× দ্রুত)grep command-টা vfmadd231ps-এর মতো লাইন দেখাবে যদি vectorization
সফল হয়ে থাকে — এটাই নিশ্চিত প্রমাণ, শুধু timing অনুমান না।
একই C কোড, শুধু compiler flag বদলে (vectorization চালু/বন্ধ), বাস্তব measurable speedup দেখায় — SIMD একটা তাত্ত্বিক ধারণা না, সরাসরি পরিমাপযোগ্য প্রভাব।
নিজে বানান
Intrinsics দিয়ে হাতে-লেখা SIMD image brightness filter
- একটা scalar brightness-adjustment ফাংশন লিখুন যা প্রতিটা pixel byte-এ একটা constant যোগ করে (সীমা ছাড়িয়ে গেলে ২৫৫-এ clamp করে)
- সেই একই কাজ AVX2 intrinsics দিয়ে ৩২ byte একসাথে প্রসেস করে লিখুন, saturating add ব্যবহার করে
- দুটো সংস্করণ একটা বড় (কয়েক মেগাবাইট) সিন্থেটিক buffer-এ চালিয়ে সময় তুলনা করুন
- সংখ্যাগতভাবে যাচাই করুন দুটো সংস্করণের আউটপুট বাইট-বাই-বাইট অভিন্ন
- বাফারের দৈর্ঘ্য ৩২-এর গুণিতক না হলে বাকি byte কীভাবে সামলাচ্ছেন তা পরীক্ষা করুন
মূল ধারণা: একটা 8-bit grayscale ছবির প্রতিটা pixel-এ brightness যোগ করা — কিন্তু ২৫৫ ছাড়িয়ে গেলে overflow না করে ২৫৫-এই থামা (saturating arithmetic, যা “Integer Overflow” লেসনে unsigned wraparound-এর বিপরীত হিসেবে উল্লেখ হয়েছিল)।
#include \<immintrin.h>
#include \<stdio.h>
#include \<stdlib.h>
#include \<string.h>
#include \<time.h>
#define N (1 \<\< 24) // ~১৬.৭ মিলিয়ন byte, একটা বড় "ছবি" অনুকরণ করতে
void brighten_scalar(unsigned char *out, const unsigned char *in, int n, int delta) {
for (int i = 0; i \< n; i++) {
int v = (int)in[i] + delta;
out[i] = (unsigned char)(v > 255 ? 255 : v); // saturate — clamp, wrap না
}
}
void brighten_avx2(unsigned char *out, const unsigned char *in, int n, int delta) {
__m256i d = _mm256_set1_epi8((char)delta);
int i = 0;
for (; i + 32 \<= n; i += 32) {
__m256i v = _mm256_loadu_si256((__m256i*)&in[i]);
// _mm256_adds_epu8 ঠিক এটাই করে -- unsigned 8-bit saturating add,
// hardware নিজেই ২৫৫-এ clamp করে, আমাদের ম্যানুয়াল চেক লাগে না
__m256i r = _mm256_adds_epu8(v, d);
_mm256_storeu_si256((__m256i*)&out[i], r);
}
for (; i \< n; i++) {
int v = (int)in[i] + delta;
out[i] = (unsigned char)(v > 255 ? 255 : v);
}
}
int main(void) {
unsigned char *in = malloc(N), *out_s = malloc(N), *out_v = malloc(N);
srand(1);
for (int i = 0; i \< N; i++) in[i] = rand() % 256;
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
brighten_scalar(out_s, in, N, 40);
clock_gettime(CLOCK_MONOTONIC, &t1);
printf("scalar: %.4fs\n",
(t1.tv_sec-t0.tv_sec)+(t1.tv_nsec-t0.tv_nsec)/1e9);
clock_gettime(CLOCK_MONOTONIC, &t0);
brighten_avx2(out_v, in, N, 40);
clock_gettime(CLOCK_MONOTONIC, &t1);
printf("avx2: %.4fs\n",
(t1.tv_sec-t0.tv_sec)+(t1.tv_nsec-t0.tv_nsec)/1e9);
// Correctness যাচাই -- দুটো সংস্করণের ফলাফল অভিন্ন হওয়া আবশ্যক
int mismatches = memcmp(out_s, out_v, N) != 0;
printf("mismatch: %s\n", mismatches ? "হ্যাঁ -- বাগ আছে!" : "না -- অভিন্ন");
return 0;
}gcc -O2 -mavx2 -o brighten brighten.c && ./brightenপ্রত্যাশিত আউটপুট আকৃতি:
scalar: 0.0210s
avx2: 0.0041s
mismatch: না -- অভিন্ননিজে বাড়ান:
_mm256_subs_epu8দিয়ে saturating subtraction (darken) সংস্করণ লিখুনint8(signed) brightness delta সমর্থন করুন (negative delta মানে ছবি darker করা) —_mm256_adds_epi8ব্যবহার করে- AVX-512 পাওয়া গেলে (
__AVX512BW__macro চেক করে)_mm512_adds_epu8দিয়ে ৬৪ byte একসাথে প্রসেস করে তুলনা করুন - একটা RGB (৩-channel) ছবিতে প্রয়োগ করে দেখুন channel interleaving কীভাবে SIMD-কে জটিল করে তোলে — এখানেই “SoA vs AoS” (structure of arrays বনাম array of structures) design প্রশ্নটা বাস্তবে দেখা যায়
বাস্তব সিস্টেমে
যেখানে SIMD প্রতিদিন কাজ করে
Video/audio codec — MMX-এর মূল প্রেরণা। x264/x265 (H.264/HEVC encoder), FFmpeg — এদের motion estimation, DCT transform, pixel filtering কোড ব্যাপকভাবে হাতে-লেখা SIMD intrinsics দিয়ে লেখা। একটা 1080p video real-time এনকোড করা SIMD ছাড়া প্রায় অসম্ভব হতো আজকের CPU-তে।
Image processing — libjpeg-turbo, libpng। JPEG decode-এর IDCT (inverse discrete cosine transform) ধাপ SIMD দিয়ে libjpeg-turbo-তে বাস্তবায়িত, যা মূল libjpeg-এর চেয়ে ২-৪ গুণ দ্রুত — এই কারণেই বেশিরভাগ browser আর OS libjpeg-turbo ব্যবহার করে।
Machine learning-এর গাণিতিক কেন্দ্র। Matrix multiplication আর dot product — neural network-এর মূল অপারেশন — একদম SIMD-আকৃতির: একই multiply-accumulate অপারেশন হাজার হাজার weight-এ প্রয়োগ হয়। AVX-512-তে বিশেষভাবে VNNI (Vector Neural Network Instructions) যোগ হয়েছে — int8 precision-এ inference দ্রুত করতে, কারণ কম precision-এ SIMD lane-এ আরও বেশি সংখ্যা ধরে (int8-তে ৬৪টা লেন, float32-এর ৮ গুণ)। Level 13-এর AI/ML foundations module-এ আমরা দেখব কেন GPU (আরও বিশাল-স্কেল SIMD, হাজার হাজার লেন) deep learning-এর জন্য এত উপযুক্ত — মূল ধারণাটা এখানে যা শিখলেন তারই সম্প্রসারণ।
Scientific computing — BLAS, LAPACK। OpenBLAS, Intel MKL-এর মতো linear algebra library-র সবচেয়ে ভেতরের loop hand-tuned AVX/AVX-512 কোড — matrix multiplication-এর মতো O(n³) অপারেশনে সামান্য SIMD-উন্নতিও বিশাল সময় বাঁচায়।
Database vectorized execution engine। আধুনিক column-store database (ClickHouse, DuckDB) SIMD ব্যবহার করে একসাথে বহু row-এ filter/aggregate চালাতে — একে “vectorized query execution” বলা হয়, আর এটা traditional row-by-row interpretation-এর চেয়ে বহুগুণ দ্রুত।
Cryptography ও hashing-সংলগ্ন — AES-NI, CRC32। যদিও এগুলো
কঠোরভাবে “SIMD” না, একই দর্শনে ডিজাইন করা বিশেষ instruction —
x86-এর AESENC (এক instruction-এ একটা পুরো AES round), আর
SSE4.2-এর CRC32 instruction — hardware-এ সরাসরি বাস্তবায়িত,
কারণ এই অপারেশনগুলো এত ঘন ঘন হয় যে software loop-এর বদলে
dedicated instruction দেওয়াই লাভজনক।
Compression — zlib-ng, libdeflate। আধুনিক zlib বিকল্পগুলো checksum (Adler-32, CRC32) আর pattern-matching ধাপে SIMD ব্যবহার করে, মূল zlib-এর চেয়ে বহুগুণ দ্রুত compression/decompression দেয়।
Rust-এর std::simd ও পোর্টেবল SIMD API। আধুনিক ভাষাগুলো
(Rust, C++-এর std::experimental::simd) portable SIMD abstraction
দেয় — একই কোড লিখে বিভিন্ন target (SSE, AVX, NEON)-এ compile
করা যায়, প্রতিটার জন্য আলাদা intrinsics না লিখে।
যে ভুলগুলো সবাই করে
“SIMD আর multithreading/multi-core একই জিনিস — দুটোই 'parallel'।”
দুটোই parallelism, কিন্তু সম্পূর্ণ ভিন্ন স্তরে।
SIMD একটা single core-এর ভেতরে, একটা single instruction-এর মধ্যেই ঘটে — কোনো thread তৈরি হয় না, কোনো OS scheduler জড়িত থাকে না, কোনো synchronization লাগে না। এটা একটা single-threaded প্রোগ্রামেও পুরোপুরি কাজ করে।
Multithreading OS-স্তরের বা library-স্তরের parallelism — একাধিক thread, সম্ভবত একাধিক core-এ, প্রতিটার নিজস্ব instruction stream, synchronization primitive (mutex, atomic) দরকার হয় shared state-এর জন্য।
একটা প্রোগ্রাম দুটোই একসাথে ব্যবহার করতে পারে — উদাহরণস্বরূপ, ৮টা thread, প্রতিটা একটা core-এ, প্রতিটা thread নিজের কাজে AVX2 SIMD ব্যবহার করছে। এটা করলে তাত্ত্বিক speedup দুটোর গুণফল হতে পারে (৮ core × ৮-লেন SIMD = ৬৪× সম্ভাব্য), যদিও বাস্তবে memory bandwidth-এর সীমা এর অনেক আগেই আসে।
“Compiler সবসময় auto-vectorize করবে যদি কোড আসলেই vectorize-যোগ্য হয়।”
এই লেসনের “Under the Hood” ভাগেই বিস্তারিত দেখানো হয়েছে — এটা প্রায়ই মিথ্যা প্রমাণিত হয়, আর সবচেয়ে বিপজ্জনক দিক হলো এই ব্যর্থতা নীরব — প্রোগ্রাম compile হয়ে যায়, কোনো warning ছাড়াই, কিন্তু কাঙ্ক্ষিত speedup আসে না।
Pointer aliasing অনিশ্চয়তা, floating-point reduction-এর IEEE
754 কঠোরতা, জটিল control flow, এমনকি শুধু compiler-এর heuristic-এর
সীমাবদ্ধতা — সবকিছুই silent vectorization failure-এর কারণ হতে
পারে। এই কারণেই performance-critical কোডে শুধু “compiler ভালো
কাজ করবে ধরে নেওয়া” যথেষ্ট না — -fopt-info-vec-missed (GCC)
বা -Rpass-missed=loop-vectorize (Clang)-এর মতো flag দিয়ে
সরাসরি compiler-কে জিজ্ঞেস করা উচিত কোথায় সে ব্যর্থ হয়েছে, আর
কেন।
“যত চওড়া SIMD (AVX-512), তত ভালো — সবসময় ব্যবহার করা উচিত।”
বাস্তবে AVX-512-এর একটা কুখ্যাত সমস্যা আছে: অনেক Intel CPU-তে AVX-512 instruction চালানো CPU-কে সাময়িকভাবে frequency throttle করতে বাধ্য করে (বেশি power draw সামলাতে ঘড়ির গতি কমানো)। যদি একটা প্রোগ্রামে মাত্র অল্প কিছু AVX-512 instruction থাকে, বাকি সবই সাধারণ scalar/AVX2 কোড, তাহলে সেই throttling-এর প্রভাব পুরো প্রোগ্রামের গড় গতি কমিয়ে দিতে পারে — বিশেষত mixed workload-এ, যেখানে CPU ক্রমাগত উচ্চ আর নিম্ন frequency-র মধ্যে দোলে।
এই সমস্যা এতটাই বাস্তব ছিল যে অনেক production সিস্টেম (যেমন কিছু বড় ডেটাসেন্টার অপারেটর) সচেতনভাবে AVX-512 ব্যবহার সীমিত বা বন্ধ রেখেছে নির্দিষ্ট workload-এ, শুধু AVX2-এই থেমে থেকে, কারণ পরিমাপে দেখা গেছে সামগ্রিক throughput ভালো হচ্ছিল। পরবর্তী Intel প্রজন্ম (Ice Lake ও তার পরে) এই throttling সমস্যা অনেকটা কমিয়েছে, কিন্তু নীতিটা থেকে যায়: প্রশস্ততা নিজে কোনো গ্যারান্টি না — measure করেই সিদ্ধান্ত নিতে হয়, ঠিক যেমন asymptotic complexity নিজে কোনো গতির গ্যারান্টি দেয় না।
বুঝেছেন কি না দেখুন
1“SIMD আসলে out-of-order execution-এরই আরেকটা রূপ, কারণ দুটোই
একসাথে বেশি কাজ করার চেষ্টা।” — এই বক্তব্যের কোন অংশ ভুল, আর
কীভাবে সংশোধন করবেন?
যুক্তি
“একসাথে বেশি কাজ করা” অংশটা সঠিক — উদ্দেশ্যের দিক থেকে দুটোই throughput বাড়ানোর কৌশল। কিন্তু কীভাবে তারা এটা করে, সেটা সম্পূর্ণ ভিন্ন প্রক্রিয়া, আর এই পার্থক্যটাই গুরুত্বপূর্ণ।
Out-of-order execution একাধিক আলাদা, ভিন্ন instruction সমান্তরালে চালায় — প্রতিটার নিজস্ব opcode, নিজস্ব operand, ভিন্ন execution unit-এ। এটা কাজ করে instruction-এর মধ্যে dependency সম্পর্ক বিশ্লেষণ করে — কোনটা কার উপর নির্ভরশীল তা ট্র্যাক করে (রেজিস্টার renaming, ROB — আগের লেসন)।
SIMD একটা একক instruction একসাথে বহু data element-এ প্রয়োগ করে — একই opcode, একই অপারেশন, শুধু data ভিন্ন। এখানে কোনো dependency বিশ্লেষণ লাগে না কারণ lane-গুলো ডিজাইন অনুযায়ীই independent — hardware শুধু একটা wide execution unit-এর মধ্যে সেগুলো পাশাপাশি বসিয়ে দেয়।
সংশোধিত বক্তব্য: “SIMD আর out-of-order execution parallelism-এর দুইটা স্বাধীন অক্ষ — একটা বিভিন্ন instruction সমান্তরাল করে (ILP), আরেকটা একই instruction-কে বিভিন্ন data-তে সমান্তরাল করে (data parallelism)। একটা modern CPU দুটোই একসাথে প্রয়োগ করে — এমনকি একটা single SIMD instruction নিজেই out-of-order ভাবে issue হতে পারে অন্য instruction-এর সাপেক্ষে।”
2একটা ২৫৬-bit AVX register-এ কতগুলো int16 element ধরে? আর
একটা ৫১২-bit AVX-512 register-এ কতগুলো int8 element? দেখান
হিসাব।
প্রয়োগ
int16 element ধরে? আর
একটা ৫১২-bit AVX-512 register-এ কতগুলো int8 element? দেখান
হিসাব।সূত্র সহজ: register-এর মোট bit / একটা element-এর bit।
২৫৬-bit register, int16 (১৬ bit প্রতি element):
টা element
৫১২-bit register, int8 (৮ bit প্রতি element):
টা element
লক্ষ্য করুন দ্বিতীয়টা প্রথমটার চারগুণ — register প্রস্থ দ্বিগুণ (২৫৬→৫১২) আর element আকার অর্ধেক (১৬→৮ bit) হলে, লেন সংখ্যা মোট চারগুণ বাড়ে (২× থেকে register width, ২× থেকে element size)। এই কারণেই ছোট data type (byte-ভিত্তিক image processing, int8 ML inference) SIMD-এ সবচেয়ে বেশি “lane density” পায় — একই register-এ একসাথে সবচেয়ে বেশি সংখ্যক element প্রসেস হয়।
3নিচের দুইটা loop-এর মধ্যে কোনটা auto-vectorize হওয়ার সম্ভাবনা
বেশি, আর কেন? (দুটোই একই কাজ করছে বলে মনে হয়)
// Loop A
for (int i = 0; i \< n; i++) {
c[i] = a[i] * 2.0f + b[i];
}
// Loop B
for (int i = 1; i \< n; i++) {
c[i] = c[i-1] * 2.0f + b[i];
}
যুক্তি
// Loop A
for (int i = 0; i \< n; i++) {
c[i] = a[i] * 2.0f + b[i];
}
// Loop B
for (int i = 1; i \< n; i++) {
c[i] = c[i-1] * 2.0f + b[i];
}Loop A vectorize হওয়ার সম্ভাবনা অনেক বেশি — Loop B প্রায় অসম্ভব (সহজ vectorization-এ)।
Loop A প্রতিটা iteration-এ শুধু a[i] আর b[i] পড়ে, c[i]
লেখে — কোনো iteration আরেকটার output-এর উপর নির্ভর করে না।
প্রতিটা iteration সম্পূর্ণ independent, তাই compiler নিরাপদে
৮টা (বা যত লেন সম্ভব) iteration একসাথে packed register-এ
প্রসেস করতে পারে (ধরে নিলে a, b, c overlap করে না, যা
restrict দিয়ে নিশ্চিত করতে হবে)।
Loop B-তে c[i] নির্ভর করে c[i-1]-এর উপর — এটা একটা
loop-carried dependency। c[5] হিসাব করার আগে c[4] জানা
থাকতেই হবে, c[4]-এর আগে c[3], ইত্যাদি — এটা একটা কঠোরভাবে
sequential chain, ঠিক যেমন লেসন ১৪-তে RAW dependency চেইন।
একটা SIMD instruction একসাথে ৮টা লেনে কাজ করে ধরে নেয় লেনগুলো
independent — কিন্তু এখানে লেন ১-এর ইনপুট লেন ০-এর আউটপুটের
উপর নির্ভর করে, যা একটা single instruction-এর মধ্যে সম্ভব না।
কম্পাইলার Loop B-কে vectorize করার চেষ্টা করলে হয় সম্পূর্ণ ব্যর্থ হবে (scalar-ই থাকবে), অথবা যদি এটা একটা পরিচিত reduction প্যাটার্নের সাথে মেলে (যেমন শুধু যোগফল জমা করা), বিশেষ কৌশল প্রয়োগ করতে পারে — কিন্তু এই সাধারণ recurrence (প্রতিটা মান আগেরটার একটা function) সেই বিশেষ প্যাটার্নে পড়ে না।
শিক্ষা: শুধু “একটা লুপ দেখতে সহজ” মানেই vectorize-যোগ্য না — iteration-গুলো সত্যিই independent কি না সেটাই আসল প্রশ্ন, ঠিক যেমন এই module-এর আগের লেসনে (register renaming) true বনাম false dependency আলাদা করাই ছিল মূল কাজ।
4একটা কাজে scalar কোড ১০০ ms নেয়। আপনি ৮-লেন AVX2 (float32) ব্যবহার
করে vectorize করলেন, কিন্তু measured সময় দাঁড়াল ৩০ ms, তাত্ত্বিক
১২.৫ ms (100/8) না। এই পার্থক্যের সম্ভাব্য দুইটা কারণ দিন।
প্রয়োগ
তাত্ত্বিক ৮× speedup পাওয়া একটা upper bound ধরে — বাস্তবে প্রায় সবসময় এর কম হয়। দুইটা সাধারণ কারণ:
১. Memory bandwidth সীমাবদ্ধতা। SIMD compute unit ডেটা প্রসেস করতে যত দ্রুত সক্ষম, memory থেকে সেই ডেটা আনতে তার চেয়ে বেশি সময় লাগতে পারে। যদি কাজটা memory-bound হয় (compute কম, data movement বেশি — যেমন সাধারণ addition-এর মতো), তাহলে compute unit ৮× দ্রুত হলেও memory bus-এর গতি একই থাকে, তাই সামগ্রিক speedup memory bandwidth দিয়ে সীমাবদ্ধ হয়ে যায়। এটা ঠিক memory hierarchy লেসনের সেই থিম — বেশি compute ক্ষমতা থাকলেও, ডেটা আনার গতিই আসল বাধা হতে পারে।
২. Overhead — loop setup, tail handling, horizontal reduction। প্রতিটা SIMD ব্লকের শুরুতে load, শেষে store, আর যদি কাজটা একটা reduction হয় (যেমন dot product), শেষে ৮টা লেনের ফলাফল একসাথে যোগ করার (horizontal sum) একটা অতিরিক্ত, তুলনামূলক ধীর ধাপ লাগে। এছাড়া array-র দৈর্ঘ্য ৮-এর গুণিতক না হলে বাকি element-গুলো scalar কোডে আলাদাভাবে প্রসেস করতে হয়, যা সামগ্রিক গড় speedup কমায়।
আরও সম্ভাব্য কারণ (অতিরিক্ত): Frequency throttling (বিশেষত AVX-512-এ, misconception অংশে আলোচিত), অথবা data যদি ১৬-byte বা ৩২-byte boundary-তে aligned না হয়, unaligned load/store কিছুটা ধীর হতে পারে alignment-sensitive পুরনো hardware-এ (আধুনিক CPU-তে এই পার্থক্য কম, কিন্তু শূন্য না)।
মূল শিক্ষা: তাত্ত্বিক speedup (লেন সংখ্যার সমানুপাতিক) একটা সীমা, প্রতিশ্রুতি না — ঠিক যেমন asymptotic notation বলে “কীভাবে scale করে,” বাস্তব সময় বলে না। এই module-এর memory hierarchy লেসনের মতো, এখানেও measurement ছাড়া প্রকৃত speedup জানার উপায় নেই।
5একটা ফাংশনে প্রতিটা element-এ একটা জটিল, ডেটা-নির্ভর branch
আছে (যেমন প্রতিটা element ভিন্ন algorithm পথে যায় তার মানের
উপর ভিত্তি করে, একাধিক nested condition সহ)। SIMD দিয়ে vectorize
করা কি সবসময় লাভজনক? সিদ্ধান্ত নেওয়ার প্রক্রিয়া বর্ণনা করুন।
ডিজাইন
না, সবসময় লাভজনক না — এটা একটা প্রকৌশলগত trade-off যা measure করে সিদ্ধান্ত নিতে হয়, লেসনের “Under the Hood” ভাগে দেখানো predicated execution-এর খরচ মাথায় রেখে।
সিদ্ধান্ত নেওয়ার প্রক্রিয়া:
১. Branch-এর দুই (বা তার বেশি) পথের কাজের পরিমাণ কতটা ভারসাম্যপূর্ণ? যদি একটা পথ সহজ (কয়েকটা instruction) আর আরেকটা জটিল (শত শত instruction) হয়, SIMD predicated execution (সব পথ সব লেনে চালিয়ে mask দিয়ে বাছা) মানে সব লেনকেই সবচেয়ে ভারী পথের খরচ বহন করতে হবে, এমনকি যেসব লেনের সহজ পথে যাওয়ার কথা তাদেরও। এটা scalar branch-এর চেয়ে ধীর হতে পারে যদি বেশিরভাগ element সহজ পথে যায়।
২. কতগুলো ভিন্ন পথ (branch-এর branch, nested condition)
আছে? প্রতিটা অতিরিক্ত পথ predicated execution-এর মোট কাজ
বাড়ায় — nটা mutually exclusive পথ থাকলে, প্রতিটা লেনকে
কার্যত সবগুলো পথের যোগফলের সমান কাজ করতে হয় (ঠিক ভুল পথের
ফলাফল mask দিয়ে বাতিল করা ছাড়া)।
৩. Data-র বাস্তব distribution কেমন? যদি বাস্তব ইনপুটে element-গুলো সাধারণত একই পথে “cluster” করে (যেমন একটা sorted বা pre-filtered dataset-এ), scalar branch prediction (আগের লেসনের বিষয়) হয়তো এমনিতেই ভালো কাজ করবে — misprediction কম হবে, তাই SIMD-এর বাড়তি জটিলতার প্রয়োজনই নাও হতে পারে।
৪. বিকল্প: ডেটা পুনর্বিন্যাস (data reorganization)। কখনো কখনো সেরা সমাধান হলো SIMD-friendly করার জন্য ডেটা আগে থেকেই sort/partition করে ফেলা — একই পথে যাওয়া element-গুলোকে একসাথে জড়ো করে, তারপর প্রতিটা গ্রুপে আলাদাভাবে (সরল, branch-free) SIMD চালানো। এই partition-এর খরচ যদি একবারই হয় আর পরে বহুবার পুনর্ব্যবহার হয়, সামগ্রিকভাবে লাভজনক হতে পারে।
সিদ্ধান্ত: প্রথমে সহজ scalar সংস্করণ লিখুন, profile করুন এটা সত্যিই bottleneck কি না। যদি হয়, branch-এর ভারসাম্য আর data distribution পরীক্ষা করুন। শুধু “SIMD সবসময় দ্রুত” ধরে নিয়ে জটিল branchy কোড জোর করে vectorize করা প্রায়ই সময়ের অপচয় — এবং কখনো কখনো সরাসরি ক্ষতিকর, ঠিক এই লেসনের misconception অংশের মতোই।
এরপর কী
যখন স্বাভাবিক ক্রম ভেঙে যায়
এই পর্যন্ত এই module-এর প্রতিটা লেসন ধরে নিয়েছে CPU একটা মসৃণ, predictable পথে চলে — pipeline-এ instruction ঢোকে, বের হয়, branch predict হয়, out-of-order চলে, SIMD একসাথে বহু data প্রসেস করে — কিন্তু সবসময় CPU নিজে থেকেই, ধারাবাহিকভাবে পরের instruction ঠিক করে।
বাস্তবে এটা সবসময় সত্য না। একটা keyboard press হঠাৎ আসতে পারে
program-এর মাঝখানে, সম্পূর্ণ অপ্রত্যাশিতভাবে। একটা divide
instruction হঠাৎ শূন্য দিয়ে ভাগ করার চেষ্টা করতে পারে। একটা
প্রোগ্রাম ইচ্ছাকৃতভাবে kernel-এর সাহায্য চাইতে পারে একটা ফাইল
পড়ার জন্য। এই তিনটাই — সম্পূর্ণ ভিন্ন কারণে হলেও — CPU-কে
স্বাভাবিক sequential execution থেকে সরিয়ে অন্য কোথাও পাঠায়।
পরের লেসনে আমরা এই তিনটা প্রক্রিয়া — interrupt, exception, ও trap — এর প্রতিটার সুনির্দিষ্ট পার্থক্য দেখব, আর দেখব কীভাবে এই লেসনেরই সহোদর ধারণা — গত লেসনের reorder buffer — এই “হঠাৎ দিকবদল”-কে নিরাপদ, precise, আর পূর্বাভাসযোগ্য করে তোলে।
আরও পড়ুন
- Intel Intrinsics Guide · প্রতিটা SSE/AVX/AVX-512 intrinsic-এর interactive রেফারেন্স
- Computer Architecture: A Quantitative Approach, Chapter 4 — Hennessy & Patterson · Data-level parallelism (SIMD, vector, GPU) নিয়ে প্রামাণ্য আলোচনা
- ARM NEON Programmer's Guide — ARM Limited · ARM-এর SIMD extension-এর architecture reference
- Auto-vectorization in GCC · কম্পাইলার কখন auto-vectorize করতে পারে আর কখন পারে না তার প্রযুক্তিগত বিবরণ