001 Notes
SIMD Mapping: From Normal Code to Vector Code
What do I do to this one value?
Can I do the same thing to 4, 8, 16, or 32 values at once?
score = price * quantity + fee
if (score > threshold) {
count++;
total += score;
}
1. Scalar Instructions Already Hint At SIMD
float add(float a, float b) {
return a + b;
}
addss
ss = scalar single-precision
ps = packed single-precision
scalar:
addss one float + one float
packed:
addps four floats + four floats
vaddps eight floats + eight floats with AVX2
How do I make one add faster?
Where can I line up many independent adds so one instruction can do all of them?
2. The Running Scalar Example
score = price * quantity + fee
struct ScoreSummary {
int count;
float total;
};
ScoreSummary summarize_scalar(
const float* price,
const float* quantity,
const float* fee,
int n,
float threshold
) {
ScoreSummary result{0, 0.0f};
for (int i = 0; i < n; ++i) {
float score = price[i] * quantity[i] + fee[i];
if (score > threshold) {
++result.count;
result.total += score;
}
}
return result;
}
score0 depends on price0, quantity0, fee0
score1 depends on price1, quantity1, fee1
score2 depends on price2, quantity2, fee2
price[i] -> load 8 prices
quantity[i] -> load 8 quantities
fee[i] -> load 8 fees
threshold -> broadcast threshold to 8 lanes
score -> compute 8 scores
score > x -> compare 8 scores, producing 8 mask lanes
count++ -> count true mask bits
total += score -> add only selected score lanes
3. First Fix The Data Shape
struct Order {
float price;
float quantity;
float fee;
int customer_id;
char symbol[16];
};
Order orders[N];
price0 quantity0 fee0 cold0 cold0 ...
price1 quantity1 fee1 cold1 cold1 ...
price2 quantity2 fee2 cold2 cold2 ...
price0 price1 price2 price3 price4 price5 price6 price7
qty0 qty1 qty2 qty3 qty4 qty5 qty6 qty7
fee0 fee1 fee2 fee3 fee4 fee5 fee6 fee7
struct OrdersSoA {
float* price;
float* quantity;
float* fee;
int count;
};
const float* price;
const float* quantity;
const float* fee;
SIMD wants contiguous same-type values.
4. Registers, Lanes, And AVX2 Names
32 x 8-bit integers
16 x 16-bit integers
8 x 32-bit integers
4 x 64-bit integers
8 x 32-bit floats
4 x 64-bit doubles
| Type | Width | Common use |
|---|---|---|
__m128 | 128-bit | 4 floats |
__m128d | 128-bit | 2 doubles |
__m128i | 128-bit | integer bytes/words/dwords/qwords |
__m256 | 256-bit | 8 floats |
__m256d | 256-bit | 4 doubles |
__m256i | 256-bit | integer bytes/words/dwords/qwords |
_mm256_add_ps
| | |
| | packed single-precision floats
| add
256-bit AVX operation
| Suffix | Meaning |
|---|---|
ps | packed single-precision floats |
pd | packed double-precision floats |
epi8 | packed 8-bit integer lanes |
epi16 | packed 16-bit integer lanes |
epi32 | packed 32-bit integer lanes |
epi64 | packed 64-bit integer lanes |
si128 | raw 128-bit integer vector |
si256 | raw 256-bit integer vector |
__m256 vprice = _mm256_loadu_ps(price + i);
__m256 vqty = _mm256_loadu_ps(quantity + i);
__m256 vfee = _mm256_loadu_ps(fee + i);
__m256 vscore = _mm256_add_ps(_mm256_mul_ps(vprice, vqty), vfee);
load 8 prices
load 8 quantities
load 8 fees
multiply price and quantity lane-wise
add fee lane-wise
5. The Four-Step SIMD Loop
load chunk
compute chunk
store or accumulate chunk
handle tail
for (; i + 8 <= n; i += 8) {
__m256 vprice = _mm256_loadu_ps(price + i);
__m256 vqty = _mm256_loadu_ps(quantity + i);
__m256 vfee = _mm256_loadu_ps(fee + i);
__m256 vscore = _mm256_add_ps(_mm256_mul_ps(vprice, vqty), vfee);
// mask/count/sum comes next
}
for (; i < n; ++i) {
float score = price[i] * quantity[i] + fee[i];
if (score > threshold) {
++result.count;
result.total += score;
}
}
6. Branches Become Masks
if (score > threshold) {
++count;
total += score;
}
__m256 vthreshold = _mm256_set1_ps(threshold);
__m256 mask = _mm256_cmp_ps(vscore, vthreshold, _CMP_GT_OQ);
score: [12.0 4.0 9.0 30.0 1.0 50.0 8.0 10.0]
threshold: [10.0 10.0 10.0 10.0 10.0 10.0 10.0 10.0]
mask: [true false false true false true false false]
int bits = _mm256_movemask_ps(mask);
count += __builtin_popcount((unsigned)bits);
__m256 selected = _mm256_and_ps(vscore, mask);
total_vec = _mm256_add_ps(total_vec, selected);
if (score > threshold) {
total += score;
}
make unselected lanes zero
add all lanes to vector accumulator
uint32_t a = arr[i];
uint32_t b = arr[j];
uint32_t result = select(cond, a, b);
7. Full AVX2 Version Of The Running Example
#include <immintrin.h>
struct ScoreSummary {
int count;
float total;
};
static float hsum_avx2(__m256 v) {
__m128 lo = _mm256_castps256_ps128(v);
__m128 hi = _mm256_extractf128_ps(v, 1);
__m128 sum = _mm_add_ps(lo, hi);
sum = _mm_hadd_ps(sum, sum);
sum = _mm_hadd_ps(sum, sum);
return _mm_cvtss_f32(sum);
}
ScoreSummary summarize_avx2(
const float* price,
const float* quantity,
const float* fee,
int n,
float threshold
) {
int i = 0;
int count = 0;
__m256 vthreshold = _mm256_set1_ps(threshold);
__m256 total_vec = _mm256_setzero_ps();
for (; i + 8 <= n; i += 8) {
__m256 vprice = _mm256_loadu_ps(price + i);
__m256 vqty = _mm256_loadu_ps(quantity + i);
__m256 vfee = _mm256_loadu_ps(fee + i);
__m256 vscore = _mm256_add_ps(_mm256_mul_ps(vprice, vqty), vfee);
__m256 mask = _mm256_cmp_ps(vscore, vthreshold, _CMP_GT_OQ);
int bits = _mm256_movemask_ps(mask);
count += __builtin_popcount((unsigned)bits);
__m256 selected = _mm256_and_ps(vscore, mask);
total_vec = _mm256_add_ps(total_vec, selected);
}
float total = hsum_avx2(total_vec);
for (; i < n; ++i) {
float score = price[i] * quantity[i] + fee[i];
if (score > threshold) {
++count;
total += score;
}
}
return ScoreSummary{count, total};
}
| Scalar code | SIMD mapping |
|---|---|
price[i] | load 8 prices |
quantity[i] | load 8 quantities |
fee[i] | load 8 fees |
threshold | broadcast to 8 lanes |
price * quantity + fee | lane-wise multiply and add |
score > threshold | vector compare mask |
count++ | movemask plus popcount |
total += score | mask unselected lanes to zero, vector add |
final total | horizontal sum once |
| leftover elements | scalar tail |
8. Normal Code To AVX2 Cheatsheet
| Normal code idea | AVX2 operation |
|---|---|
x = 0 | _mm256_setzero_ps(), _mm256_setzero_si256() |
x = value repeated | _mm256_set1_ps(x), _mm256_set1_epi32(x) |
x = a[i] | _mm256_loadu_ps(a + i), _mm256_loadu_si256(...) |
out[i] = x | _mm256_storeu_ps(out + i, v), _mm256_storeu_si256(...) |
a + b | _mm256_add_ps(a, b), _mm256_add_epi32(a, b) |
a - b | _mm256_sub_ps(a, b), _mm256_sub_epi32(a, b) |
a * b | _mm256_mul_ps(a, b), _mm256_mullo_epi32(a, b) |
a / b for floats | _mm256_div_ps(a, b), _mm256_div_pd(a, b) |
a * b + c for floats | _mm256_fmadd_ps(a, b, c) |
min(a, b) | _mm256_min_ps(a, b), _mm256_min_epi32(a, b) |
max(a, b) | _mm256_max_ps(a, b), _mm256_max_epi32(a, b) |
| bitwise AND | _mm256_and_si256(a, b) |
| bitwise OR | _mm256_or_si256(a, b) |
| bitwise XOR | _mm256_xor_si256(a, b) |
a == b | _mm256_cmpeq_epi32(a, b), _mm256_cmp_ps(...) |
a > b | _mm256_cmpgt_epi32(a, b), _mm256_cmp_ps(...) |
condition ? a : b | compare to mask, then blend or bitwise select |
| sum array | vector accumulators, then horizontal reduction |
| find matching bytes | byte compare plus _mm256_movemask_epi8 |
| reorder lanes | _mm256_shuffle_*, _mm256_permute_* |
| indexed load | _mm256_i32gather_* |
9. Reductions: Stay Vector As Long As Possible
float total = 0.0f;
for (int i = 0; i < n; ++i) {
total += a[i];
}
for (int i = 0; i + 8 <= n; i += 8) {
__m256 v = _mm256_loadu_ps(a + i);
total += hsum_avx2(v);
}
load vector
add into vector accumulator
repeat
reduce accumulator once at the end
total_vec = _mm256_add_ps(total_vec, selected);
total_vec:
[sum0 sum1 sum2 sum3 sum4 sum5 sum6 sum7]
float total = hsum_avx2(total_vec);
Vertical work in the hot loop.
Horizontal work at the end.
10. Integer Masks Use The Same Idea
int count_gt_scalar(const int* a, int n, int x) {
int count = 0;
for (int i = 0; i < n; ++i) {
count += a[i] > x;
}
return count;
}
#include <immintrin.h>
int count_gt_avx2(const int* a, int n, int x) {
int i = 0;
__m256i vx = _mm256_set1_epi32(x);
__m256i ones = _mm256_set1_epi32(1);
__m256i counts = _mm256_setzero_si256();
for (; i + 8 <= n; i += 8) {
__m256i v = _mm256_loadu_si256((const __m256i*)(a + i));
__m256i mask = _mm256_cmpgt_epi32(v, vx);
__m256i inc = _mm256_and_si256(mask, ones);
counts = _mm256_add_epi32(counts, inc);
}
alignas(32) int tmp[8];
_mm256_store_si256((__m256i*)tmp, counts);
int count = tmp[0] + tmp[1] + tmp[2] + tmp[3] +
tmp[4] + tmp[5] + tmp[6] + tmp[7];
for (; i < n; ++i) {
count += a[i] > x;
}
return count;
}
__m256i inc = _mm256_and_si256(mask, ones);
false: 0x00000000
true: 0xFFFFFFFF
false: 0
true: 1
11. Strings Map To Byte Scans
"hello,world\n"
h e l l o , w o r l d \n
load 32 bytes
compare all bytes against target
turn byte comparison into bitmask
use scalar bit operations on the mask
handle tail
const char* find_char_scalar(const char* s, int n, char target) {
for (int i = 0; i < n; ++i) {
if (s[i] == target) {
return s + i;
}
}
return nullptr;
}
#include <immintrin.h>
const char* find_char_avx2(const char* s, int n, char target) {
int i = 0;
__m256i vt = _mm256_set1_epi8(target);
for (; i + 32 <= n; i += 32) {
__m256i chunk = _mm256_loadu_si256((const __m256i*)(s + i));
__m256i cmp = _mm256_cmpeq_epi8(chunk, vt);
int mask = _mm256_movemask_epi8(cmp);
if (mask != 0) {
int offset = __builtin_ctz(mask);
return s + i + offset;
}
}
for (; i < n; ++i) {
if (s[i] == target) {
return s + i;
}
}
return nullptr;
}
byte comparison:
[00 00 FF 00 FF 00 00 FF ...]
movemask:
00101001...
__m256i comma = _mm256_set1_epi8(',');
__m256i newline = _mm256_set1_epi8('\n');
__m256i chunk = _mm256_loadu_si256((const __m256i*)ptr);
__m256i is_comma = _mm256_cmpeq_epi8(chunk, comma);
__m256i is_newline = _mm256_cmpeq_epi8(chunk, newline);
__m256i either = _mm256_or_si256(is_comma, is_newline);
int mask = _mm256_movemask_epi8(either);
12. Awkward Code Does Not Map Cleanly
| Scalar shape | SIMD fit | Reason |
|---|---|---|
out[i] = a[i] + b[i] | Good | Lane-independent and contiguous |
score = price[i] * qty[i] + fee[i] | Good | Elementwise arithmetic |
count += a[i] > x | Good | Compare maps to mask |
find '\n' in a buffer | Good | Byte compare maps to movemask |
sum += a[i] | Medium | Needs final horizontal reduction |
out[i] = a[i - 1] + a[i] + a[i + 1] | Medium | Needs neighboring lanes or overlapping loads |
node = node->next | Bad | Pointer chasing |
obj[i].virtual_call() | Bad | Indirect calls and per-object control flow |
map[key] traversal | Bad | Scattered memory and branches |
[a b c d | e f g h]
[a0 a1 a2 a3 | a4 a5 a6 a7]
+
[b0 b1 b2 b3 | b4 b5 b6 b7]
0 cross-lane operations per vector iteration:
excellent
1 occasional cross-lane operation:
usually fine
1 cross-lane operation every iteration:
benchmark carefully
multiple dependent cross-lane operations every iteration:
likely expensive
13. Workflow And Checklist
write clear scalar code
benchmark it
check compiler auto-vectorization
fix memory layout
remove unpredictable branches if needed
write intrinsics only for hot loops
benchmark again
g++ -O3 -march=native main.cpp -o main
g++ -O3 -fopt-info-vec main.cpp -o main
clang++ -O3 -Rpass=loop-vectorize main.cpp -o main
perf stat ./bench
cycles
instructions
branches
branch-misses
cache-misses
L1-dcache-load-misses
Is the data contiguous?
Is the same operation repeated?
Are lanes independent?
Can branches become masks?
Can reductions be delayed?
Can the tail be handled safely?
Is this loop actually hot?
Scalar value -> SIMD lane
Scalar loop -> chunk loop
Scalar constant -> broadcast vector
Scalar branch -> mask or blend
Scalar byte scan -> compare plus movemask
Scalar reduction -> vector accumulators plus final horizontal reduction
Object layout -> usually fix layout before writing intrinsics
same operation
many contiguous elements
minimal branching
minimal lane communication
Comments