經過前五天理論的洗禮,今天來做點實驗~~~~
規格沒寫編譯器怎麼想,只能編譯成組合語言,看它輸出什麼指令啦
不是 D2 圖裡 AVX2 的 8 lane,也沒有 AVX-512 的
k0mask register。D2 舉的vmulps ymm、broadcast 進ymm再 AND,在這台要翻譯成fmul.4s、dup/mov.b、and.16b所以不能拿 x86 的圖去對這份組合語言,機制一樣(lane 必須 lockstep),但指令名字不一樣
編譯器是 Apple clang 21(終端機打 gcc 也是它),不是 GNU GCC 所以不會輸出 control flow in loop,而是輸出 vectorized loop 或 cost-model indicates that vectorization is not beneficial。
| kernel | null 怎麼放 | 今天要看什麼 |
|---|---|---|
| k1 | 沒有,每個 float 都當有值 | 基準線 |
| k2 | 每列一個 byte,迴圈裡 if |
D2 說的branch 擋向量化 |
| k3 | bitmap,每列抽 1 bit 再乘 | 展開能不能 SIMD |
| k3r | 跟 k3 一樣,參數加 restrict |
失敗是不是其實 alias,不是 bitmap |
| k3b | 值迴圈不碰 validity;validity 另外 AND | Arrow 比較像的拆法 |
| k4 | 跟 k1 一樣,加 restrict |
少掉的是重疊檢查,不是 SIMD 本身 |
六個 kernel 算的是同一個式子,result = price * (1 - discount),NULL 位置填 0,差別只在 validity 怎麼存、怎麼合併。用 8 列當範例,第 0、2、5 列是 NULL:
price = [10, 20, 30, 40, 50, 60, 70, 80]
discount = [.1, .1, .1, .1, .1, .1, .1, .1]
# 存法一:每列一個 byte,k2 用這個
flag = [0, 1, 0, 1, 1, 0, 1, 1]
# 存法二:bitmap,一列一個 bit,LSB 是 row 0,k3 / k3r / k3b 用這個
bitmap = 0b11011010
# 兩種存法算出來都一樣
out = [0, 18, 0, 36, 45, 0, 63, 72]
k1、k4 完全不看 validity,等於把 flag / bitmap 都當成全 1,輸出會是 [9, 18, 27, 36, 45, 54, 63, 72]。
編譯時加上 -Rpass=loop-vectorize,clang 會對每個迴圈印輸出,說明是否有向量化、為什麼
接下來一個一個看 kernel 怎麼做
void k1(const float *price, const float *disc, float *out, int n) {
for (int i = 0; i < n; i++) {
out[i] = price[i] * (1.0f - disc[i]);
}
}
寫 i++ clang 輸出:
vectorized loop (width: 4, interleaved count: 4)
width: 4 是 NEON 一次算 4 個 float,interleaved count: 4 是編譯器把這套動作連做 4 回再跳回迴圈開頭,所以一輪走完 4 × 4 = 16 列
組合語言語輸出:
ldp q1, q2, [x10, #-32]
ldp q3, q4, [x10], #64
fsub.4s v5, v0, v5
fmul.4s v1, v1, v5
subs x13, x13, #16
最後那個 subs #16 就是一輪 16 列的證據,fmul.4s 一條乘 4 個 float,出現四次剛好對上 interleaved 4
void k2(const float *price, const float *disc, const u8 *flag,
float *out, int n) {
for (int i = 0; i < n; i++) {
if (flag[i]) {
out[i] = price[i] * (1.0f - disc[i]);
} else {
out[i] = 0.0f;
}
}
}
flag[i] == 0 當 null,不是去 float 裡找 NaN,迴圈是:
ldrb w15, [x11, #-3]
cbz w15, LBB1_7
ldur s2, [x14, #-16]
fmul s2, s2, s3
讀一個 byte,是 0 就跳過乘法
一次一列,還展開成 8 組,每組自己一個 cbz,沒有 fmul.4s。
k3 把 bitmap 展開寫進同一條值迴圈,沒有 if,用三元運算選 0 或乘積:
void k3(const float *price, const float *disc, const u8 *valid,
float *out, int n) {
for (int i = 0; i < n; i++) {
float v = price[i] * (1.0f - disc[i]);
unsigned bit = (valid[i >> 3] >> (i & 7)) & 1u;
out[i] = bit ? v : 0.0f;
}
}
但 clang 仍會輸出 cannot identify array bounds
ldr s2, [x0, x8, lsl #2] ; price[i]
fmul s2, s2, s3
lsr w10, w8, #3 ; i / 8
ldrb w10, [x2, x10] ; valid[i/8]
lsr w10, w10, w11 ; >> (i % 8)
fcsel s2, s1, s2, eq ; 用比較選值,沒 branch
同一份迴圈,參數加上 restrict(k3r),變成 vectorized width: 8
ldrb w15, [x12], #1 ; 讀一個 bitmap byte(8 列)
mov.b v6[0], w15 ; 廣播到向量的四個位置
mov.b v6[4], w15
ushl.4s v17, v4, v20 ; 位移,把想要的 bit 拉到最低位
cmeq.4s v6, v6, #0 ; 展成 0 或全 1 的 mask
bic.16b v6, v7, v6 ; 用 mask 把 null 位置歸零
fmul.4s v16, v16, v17 ; 值的乘法
Arrow 比較像的是 k3b:值迴圈完全不看 validity
validity 另外做一條 AND:
void k3b_values(const float *price, const float *disc, float *out, int n) {
for (int i = 0; i < n; i++) {
out[i] = price[i] * (1.0f - disc[i]);
}
}
void k3b_validity(const u64 *a, const u64 *b, u64 *out, int nwords) {
for (int i = 0; i < nwords; i++) {
out[i] = a[i] & b[i];
}
}
乘法那個函式編譯指令跟 k1 一樣,都是 fmul.4s(一次乘 4 個 float)
合併 null 標記的那個,真正反覆跑、吃掉最多時間的那一圈裡有四條 and.16b。
一條 and.16b 一次處理 16 byte,四條就是 64 byte,bitmap 一列只佔 1 bit,64 byte 等於一次把 512 列的「有值 / null」AND 完
ldp q0, q1, [x10, #-32]
ldp q2, q3, [x10], #64
and.16b v0, v4, v0 ; 一條指令合併 128 個 validity bit
and.16b v1, v5, v1
and.16b v2, v6, v2
and.16b v3, v7, v3
null 不進乘法,合併是另一條 AND,不是在值迴圈裡逐列抽 bit。
k4 跟 k1 算同一個式子,只是指標標了 restrict:
void k4_restrict(const float *restrict price, const float *restrict disc,
float *restrict out, int n) {
for (int i = 0; i < n; i++) {
out[i] = price[i] * (1.0f - disc[i]);
}
}
等於告訴編譯器三塊記憶體不重疊,clang 一樣輸出 width 4、interleaved 4
差在開頭,k1 有記憶體不重疊的檢查,失敗就走純量,但 k4 標restrict 意思是保證不重疊,直接進向量迴圈
(Rust 的 &[f32] 在 LLVM IR 會帶 noalias,效果跟這裡的 restrict 同類)好餓QAQ
六個 kernel :
| kernel | clang 輸出 |
|---|---|
| k1 | vectorized loop (width: 4, interleaved count: 4) |
| k2 | vectorization is not beneficial |
| k3 | cannot identify array bounds,沒向量化 |
| k3r | vectorized loop (width: 8, interleaved count: 1) |
| k3b 值 | vectorized loop (width: 4, interleaved count: 4) |
| k3b validity | vectorized loop (width: 2, interleaved count: 4) |
| k4 | vectorized loop (width: 4, interleaved count: 4) |
編譯器這層結束,但認真說乘法好解決除法比較麻煩XXD
明天拿 DataFusion 的 EXPLAIN ANALYZE 來玩玩
那就明天見~