vatt'ghern jaskier's ballads
本文 6 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

C++26 終於把 std::simd 收進標準庫。十幾年的 P0214、P1928 提案塵埃落定,工程師卻發現 實際代換進現有的影像處理或數值熱迴圈時,編譯時間爆炸、執行效能比手寫差兩倍以上、 需要的 shuffle 與 permute 完全沒有覆蓋——這個被等了十年的庫,原來不是給寫 SIMD 的人用的。

C++26 SIMD 庫的能力對照——Highway 與 ISPC 何時仍是更安全的選擇

P1928 提出的 std::simd 是「寫一份 code、編譯成 AVX2/AVX-512/NEON」的便攜抽象,用 template 包裝硬體向量類型,避免直接寫 intrinsics 與 #ifdef。 但這套抽象的成本與 Google Highway、Intel ISPC、Agner Fog 的 VCL、Joel Falcou 的 EVE 等既有方案放在一起對照時就現形了。 Highway 也走 library 路線但設計成 length-agnostic、支援 runtime dispatch; ISPC 則乾脆走語言路線,把 SIMD lane 當第一級 abstraction。 四個維度排開來看,std::simd 的定位其實非常窄。

這個 critique 在五月中於 lucisqr substack 流傳得很快,作者把問題拆成可量化的 四項:表達力(cross-lane 操作覆蓋率)、編譯時間放大、執行效能 vs scalar auto-vectorization、可攜性與 runtime dispatch。 每一項都附上具體 benchmark:sin 函數 SIMD 化反而比 scalar 慢、 integer add 比 scalar 慢 2.4 倍、單一 translation unit 編譯時間從 0.2 秒 上升到 2.2 秒——10× 的放大。 這不是邊緣情境,這是 std::simd 在它最該擅長的「element-wise 純向量計算」 上的表現。

更尖銳的觀察是:std::simd 把標準化做出來的同時,產業已經在過去十年裡用 Highway 和 ISPC 解決了 SIMD 可攜性問題。 Chromium、Firefox、JPEG XL、libaom、Jpegli、libvips—— 所有真實 production 場景的 portable SIMD 都跑在 Highway 之上, 沒有一個在等 std::simd。 當你看到 Google 為了自己的 codec 工作量寫了 Highway 而不是等標準庫的時候, 故事已經說完了一半。

這個現象有個歷史解釋。 P0214 在 2016 年首度進入 ISO 提案流程,前後改了至少九個版本, 2018 年寫進 Parallelism TS 2 (ISO/IEC TS 19570:2018)。 GCC 11 在 2021 年提供了 <experimental/simd> header。 這段時間裡 Matthias Kretz 在 CERN 維護的 Vc library(5000+ commit、用於高能物理 模擬)一直是 std::simd 的 reference implementation。 P1928 把 std::simd 從 experimental TS 提升到 C++26 標準正文, 這個提升動作走完,2026 年初塵埃落定。

問題是:這十年裡產業沒在原地等。 Highway 在 Google 內部誕生(最初為 JPEG XL)、在 2020 年左右開放源碼, 迅速被 Chromium 與 Firefox 引入。 ISPC 由 Intel 在 2010 年代初開源、Matt Pharr 領導, 被 OSPRay 與 Blender Cycles 採用做 ray tracing 內迴圈。 當 P1928 在 committee 裡走最後一哩路的時候, Highway 與 ISPC 已經各自在 production 上跑了五年以上。

把這兩條時間軸並排起來看更清楚——拖動下方滑桿可以在 2012 到 2026 之間 scrub, 上方標準軌與下方產業軌的事件會同步在當下時間點亮起,狀態列顯示「committee 在做什麼/產業已經跑到哪」。 十年的時間差不是抽象說法,是可以被讀者親手量出來的。

標準 產業 2012
標準P0214 尚未出現
產業Vc 在 CERN 維護中、ISPC 已開源、Highway 尚未存在
標準軌(上)以年為單位緩慢推進,產業軌(下)以月為單位連續產出。Scrub 到 2021 可以看到 P1928 還在審稿時,Highway 已被 Chromium 採用。

標準軌(上)以年為單位緩慢推進,產業軌(下)以月為單位連續產出

P1928 走完最後一哩路時,Highway 已在 Chromium 與 Firefox 生產環境跑了五年以上——標準落後產業一個完整世代。

為何先看大表:對齊四個方案的維度差距

下面這張對照表把四個方案在六個關鍵維度上的能力擺出來——點擊 column header 可以重排,方便依不同的 production 約束(編譯時間優先?runtime dispatch 優先?) 做不同的排序。 表格只是大表,後續每個 H2 章節會逐軸展開各自的細節與 benchmark 來源。

四個 SIMD 方案在主要維度上的能力對照(資料來源:lucisqr substack 2026-05 評測;點擊 header 可重排)
維度 std::simd (C++26) Google Highway ISPC Agner Fog VCL
cross-lane shuffle/permute 未覆蓋 完整(TableLookup、Per128BitLane、Reverse) 語言內建(programIndex) x86 intrinsics 直通
編譯時間相對 scalar 約 10×(2.2s vs 0.2s) 約 3× ≈ scalar(獨立編譯) 約 2×
vs scalar 執行效能 慢 2.4× 接近或勝出 1.5–4× 加速 2× 加速
預設 vector width 128-bit(ABI-safe fallback) length-agnostic 編譯期 target 決定 顯式宣告
runtime dispatch 無內建 內建(CPUID 路由) multi-target 編譯 需手寫 IFUNC
production 採用 實驗階段 Chromium、Firefox、JPEG XL、libaom、Jpegli、libvips Intel OSPRay、Blender Cycles HFT、嵌入式 codec
std::simd 在五個維度上明顯落後既有方案;它唯一的優勢是「在標準庫裡」這件事本身——這不是工程能力,是公文上的背書。

std::simd 在五個維度上明顯落後既有方案;它唯一的優勢是「在標準庫裡」這件事本身——這不是工程能力,是公文上的…

std::simd 五個維度落後:無 cross-lane、編譯 10×、執行慢 2.4×、固定 128-bit、無 runtime dispatch。

讀者第一眼會注意到 std::simd 在六行裡有五行落在 「未覆蓋/約 10×/慢 2.4×/128-bit fallback/無內建/實驗階段」這側。 這不是把標準庫罵成廢物——它是在說:std::simd 並不是「把現有最佳實踐標準化」, 而是「把一個 2012 年的想法經過十年磨耗之後納入標準」。

如果你回頭看 P0214 的初稿與 GCC 11 在 2021 年提供的 <experimental/simd> header, 會發現整個設計骨架基本沒變,但這十年裡 Highway 解決了 length-agnostic、 ISPC 證明了語言層介入比 library 更乾淨。 委員會的時間軸與產業的時間軸沒有對齊,是這個 critique 真正想說的事。

可以做個換個角度的觀察:標準化的價值通常來自於「把已被驗證的最佳實踐固定下來」。 C++11 的 std::thread 是這樣(pthreads 與 Windows threads 跑了十年, std::thread 把他們的抽象做成可攜的); C++17 的 std::filesystem 是這樣(boost::filesystem 已經是事實標準)。 這些標準化動作都是「賦予 blessing」,不是「重新發明」。 std::simd 不一樣,它把一個沒在 production 跑過的 library 變成標準。 這是 critique 反覆強調的點。

表達力:cross-lane 操作是真正的 SIMD 工作

真實的 SIMD 程式碼絕大部分時間不在做 element-wise 加減乘除—— element-wise 那部分編譯器的 auto-vectorization 已經做得很好, -O3 -march=native 加上一個寫得乾淨的 for-loop, GCC 與 LLVM 都能把它向量化到目標機器的全 width。

困難的、值得人類介入寫 SIMD 的,是 cross-lane 操作: shuffle、permute、blend、horizontal reduce、gather/scatter、 特定 lane 的 mask、跨 lane 的進位處理。 影像處理裡的 transpose、密碼學裡的 S-box lookup、 physics 模擬裡的 SoA→AoS 轉換、JPEG/AV1 codec 裡的 zigzag scan, 每一個都是 cross-lane。

std::simd 完全不覆蓋這些操作。 要做 8-bit pixel format conversion,標準作法是 _mm256_shuffle_epi8; 要做 motion estimation 的 SAD,得用 _mm_sad_epu8; channel deinterleaving 需要 _mm256_permutevar8x32_epi32; narrowing 16-bit 到 8-bit 要 _mm256_packus_epi16; pixel clamping 的 saturating add 是 _mm_adds_epu8; 提取 comparison 結果到 bitmask 要 movemask—— 這些 std::simd 一個都沒提供對應的高階抽象。

要做這些事情,還是得退回 intrinsics,而退回 intrinsics 就退回了所有 #ifdef __AVX2__#ifdef __ARM_NEON 的腥味。 這個現象是 lucisqr 評測者口中「std::simd 解決最簡單那 10%」的核心。 純 element-wise 那 10% 本來就是 auto-vectorization 的舒適圈, 剩下 90% 是 std::simd 接不下的—— 這個 90/10 比例不是隨意挑的,是任何寫過 codec 或數值庫的人都會給出的估計。

Highway 把 cross-lane 操作分類成 ReversePer128BitLaneTableLookupInterleaveLower/UpperConcatLowerLower 等抽象, 所有目標架構都有對應實作(沒有對應的就 emulated, 且 emulated 的版本會在 build-time 警告以提示效能落差)。

對 codec 工作量而言,Highway 提供的這套詞彙基本上能涵蓋 90% 的場景; 剩下 10% 是真正的 ISA-specific 操作(例如 AVX-512 的 VPCOMPRESSB), 這時 Highway 允許 fallback 到 intrinsics 並提供 helper 把 Highway 的 vector type 轉成原生 __m256i。 這個 escape hatch 設計是 production library 的 hallmark—— 承認你不能涵蓋全部,但提供一條乾淨的出口。

ISPC 更乾脆,programIndexprogramCount 是語言內建變數,cross-lane 通訊用 broadcastshiftreduceshuffle 等內建函式表達,編譯器負責 lower 到目標 ISA。 寫 ISPC 程式時你思考的是「lane」這個概念本身, 而不是「我有一個 vector,它的 lane 之間怎麼通訊」。

這是語言抽象 vs library 抽象的根本差別: 語言層次的 SIMD 不需要解釋「跨 lane」這件事,因為 lane 就是它的執行模型。 寫 ISPC 的 if-else 語法時,編譯器知道每個 branch 對應的是 mask 底下哪些 lane 進入;寫 C++ 的 if-else 時,你必須手動把 mask 與 branch 在腦中對齊,code reviewer 也要重做一次這個 alignment。

std::simd 走 library 路線卻沒解決 library 路線最難的部分, 這是設計上的根本斷層。 Vc library 在 2010 年代由 Matthias Kretz 在 CERN 啟動, 5000 多個 commit 慢慢演化出 std::simd 的核心 API, 但 Vc 從一開始就把目標放在「給物理模擬寫 element-wise 4-vector 算術」—— CERN 的 use case 是 Lorentz boost、Kalman filter,不是 codec。

當 P0214 把 Vc 的設計平移到標準庫, 整個 API 表面也跟著被 element-wise 的世界觀框住了。 Vc 在 CERN 跑得很好是因為 CERN 的 SIMD 工作量 95% 是 element-wise 四元數運算;把這套 API 拿到 codec 工作量上,缺失立刻暴露。 這是「用錯參考實作」造成的 path dependence。

有人會問:為什麼不在標準裡加一個 std::simd::shuffle? 答案是:cross-lane shuffle 的 ISA 差異太大。 x86 的 shuffle 操作 mask 是 immediate operand, AVX-512 引入了 VPERM 系列把 mask 變成 register operand, ARM NEON 用 TBL/TBX 但只能跨 16-byte 邊界, ARM SVE 引入了 predicated permute—— 要把這四個包成一個 portable 介面,做出來的東西要嘛限制到所有 ISA 共有的子集 (基本上只剩 reverse),要嘛變成 lazy proxy 物件由編譯器在 lowering 階段挑路徑。 Highway 選了後者並做得不錯;標準庫的 API 預算大概是給不起這個複雜度。

所以實務上,假設你今天要寫一段 H.264 的 inverse DCT, 會發現純 std::simd 寫不出來。 inverse DCT 的核心是 butterfly 結構,需要 interleave 與 deinterleave 操作; zigzag scan 需要 table lookup; quantization 需要 saturating arithmetic; motion compensation 需要 horizontal sum—— 這四個操作 std::simd 都接不下。

會逼你混用:element-wise 的部分用 std::simd, cross-lane 的部分用 intrinsics, 於是程式碼裡同時出現兩套向量類型, std::simd<int32_t>__m256i, 互相轉換要透過 std::bit_castsimd::to_native—— 這個轉換在 GCC 14 的 experimental 實作裡會生成多餘的 load/store 通過 stack, 把熱迴圈的 cache pressure 拉高。

對照 Highway,整段 inverse DCT 可以全部用 Highway 的詞彙寫完: InterleaveLower/InterleaveUpper 做 butterfly、 TableLookup 做 zigzag、 MulAdd 做 multiply-accumulate, 最後 Store 寫回。 同一份原始碼編譯到 SSE4、AVX2、AVX-512、NEON、SVE、RVV, 產出的 codegen 都接近手寫 intrinsics 的品質。 這就是「library 抽象做對了」的樣子。

把「同一個 dot-product 內迴圈」用兩個 library 各寫一份放在一起對比,差異在語法層面就看得出。 拖動下方的分隔線,左右兩側分別顯示 std::simd 與 Highway 的版本—— std::simd 版本是 fixed-width loop 加 reduce,Highway 版本是 length-agnostic loop 加 ReduceSum,且後者可以同源碼編到 SSE4 / AVX2 / NEON / SVE。

// Highway: length-agnostic、同一份 source 編到 SSE4/AVX2/AVX-512/NEON/SVE/RVV #include <hwy/highway.h> namespace hn = hwy::HWY_NAMESPACE; float dot(const float* a, const float* b, size_t n) { const hn::ScalableTag<float> d; // d 由目標平台決定寬度 auto sum = hn::Zero(d); size_t i = 0; for (; i + hn::Lanes(d) <= n; i += hn::Lanes(d)) { auto va = hn::LoadU(d, a + i); auto vb = hn::LoadU(d, b + i); sum = hn::MulAdd(va, vb, sum); // 真正的 FMA, native width } return hn::ReduceSum(d, sum); // horizontal reduce 由庫處理 }
// std::simd: fixed-width 128-bit ABI fallback、cross-lane 無覆蓋 #include <experimental/simd> namespace stdx = std::experimental; float dot(const float* a, const float* b, size_t n) { using V = stdx::native_simd<float>; // 在 AVX2 上仍是 4-wide V sum{0.0f}; size_t i = 0; for (; i + V::size() <= n; i += V::size()) { V va(a + i, stdx::element_aligned); V vb(b + i, stdx::element_aligned); sum += va * vb; // 沒有真正的 FMA 抽象 } return stdx::reduce(sum); // horizontal reduce 但 width 只剩一半 }
std::simd Highway
左:std::simd 版本,在 AVX2 上跑 4-wide、無 FMA 抽象、reduce 後 width 已被 ABI fallback 砍半。右:Highway 版本,ScalableTag 在 AVX2 上是 8-wide、AVX-512 上是 16-wide、SVE 上是 runtime scalable,MulAdd 直接對應 native FMA 指令。

左:std::simd 版本,在 AVX2 上跑 4-wide、無 FMA 抽象、reduce 後 width 已被 …

AVX2 上 std::simd 只用 4-wide,Highway ScalableTag 用滿 8-wide FMA、同一份碼跑遍 SSE4 到 SVE。

編譯時間:10× 放大不只是 CI 變慢

std::simd 的 template 深度與符號數量在大型 codebase 裡是真實負擔。 Substack 評測者報告:把一段純 scalar 內迴圈改寫成 std::simd 後, 單一 translation unit 的編譯時間從原本的 0.2 秒上升到 2.2 秒——10× 的放大。 這是用 GCC 14 的 <experimental/simd> header 測得, 加 -O3 -ffast-math -march=native。 換成 sin 函數做同樣對照,scalar auto-vec 版本與 std::simd 版本的編譯時間 比例也是約 10:1。

這個放大比例不是隨意挑出來的 worst case。 std::simd 為了把 platform-specific 的 ABI tag 抽象成 type template 參數, 必須在每個 simd operation 上展開一層 trait 查找: simd<T, Abi>::operator+ 會 instantiate 一個 simd_traits<T, Abi>::add_op, 後者再 instantiate platform-specific 的 vector intrinsic wrapper, 最後才落到 __builtin_ia32_paddd256

中間每一層都要做 SFINAE 與 concept 檢查, 而 SFINAE 在 GCC 的實作裡是 O(N) 對 overload set 大小敏感的。 一個 SIMD-heavy 的 translation unit 編完之後產生的 debug info 可以比 scalar 版本大 5-8 倍。 這放大也會傳染到 link 階段:linker 要 merge 多個 TU 的 debug section、 去重 template instantiation,整體 link 時間也跟著拉長。

error message 的可讀性是另一個工程能力指標。 std::simd 在 GCC 14 上產生的 error message 動輒 138 行 template instantiation backtrace, 要從這 138 行裡找出真正的問題(通常是「ABI tag 不相容」或「T 不滿足 simd 概念」) 需要熟悉 std::simd 內部 trait 結構的工程師。 對 onboarding 來說這是不小的成本—— 新工程師第一次踩到 std::simd 的 error message 通常會卡 30 分鐘以上。

Highway 也使用 template,但設計上刻意把 dispatch 邏輯收斂到少數巨集展開點 (HWY_BEFORE_NAMESPACE / HWY_AFTER_NAMESPACE), 單一 translation unit 內 Highway 的 instantiation 深度被刻意控制在三層以內, 編譯時間放大約在 3× 以內。

對需要 multi-target dispatch 的場合(同一份 source 編成 SSE4、AVX2、AVX-512 三個 variant),Highway 提供 HWY_DYNAMIC_DISPATCH 巨集 做三遍編譯,總時間放大到 6-9× 但這是 multi-target 必付的成本, 不是 single-target 的常態。 對 Chromium 這種數百 TU 的 SIMD-heavy 子系統, 這個放大比例在 distributed build 系統(如 Goma、reclient)上是可接受的。

ISPC 因為走獨立編譯路線(產出 .ispc.o 檔再 link), 對 host C++ 編譯時間幾乎沒有影響。 .ispc 檔本身的編譯時間取決於 target 數量與 lane 寬度, 但因為 ISPC 是專用編譯器、不負擔 C++ template instantiation 的開銷, 多數情況下單個 ISPC kernel 編譯只需 100ms 量級。

代價是 build system 必須整合 ispc executable、 處理 .ispc → .o 的依賴掃描、以及把 ISPC 產生的 header 拉進 C++ 端 include。 對 CMake 來說這是約 30 行 boilerplate; 對 bazel/buck 來說規則寫起來更直觀(custom rule)。 這個 build 整合成本在採用 ISPC 的初期是真實的,但一次性投入。

更深一層的代價是 debugger 與 IDE:std::simd 的 template instantiation 深度 讓 clangd 在開檔時的 indexing 變慢, 跳到 operator+ 定義會跳進 200 行的 SFINAE 籬笆裡, error message 動輒 138 行 instantiation backtrace。

Highway 用巨集封裝了大部分 template 細節, error message 通常 5-10 行就指到問題; ISPC 的 error message 因為走獨立編譯,直接像 C 的 error message 一樣短。 這些都是「工程能力」維度的真實差距。 新工程師 onboarding 到一個 Highway codebase 比 onboarding 到 std::simd codebase 快得多——這個差距不會出現在 benchmark 上,但會出現在團隊速度上。

為了讓「10× 編譯時間放大」這個數字不只是抽象, 下面這個互動 widget 把單一 translation unit 的編譯時間外推到專案級: 拖動滑桿改變 translation unit 數量, 曲線顯示 scalar 基線、std::simd(10×)、Highway(3×)、ISPC(≈1×) 四者在這個 codebase 規模下的總編譯時間。

80
scalar 基線(0.2s/TU) std::simd(2.2s/TU,10×) Highway(0.6s/TU,3×) ISPC(0.22s/TU,≈1×)
同一個 codebase 換 SIMD 抽象之後,總編譯時間隨 TU 數量線性放大。基準資料:lucisqr substack 測得 GCC 14 <experimental/simd> 單個 sin kernel 從 0.2s 上升到 2.2s。曲線是該係數線性外推,未模擬 link 階段。

同一個 codebase 換 SIMD 抽象之後,總編譯時間隨 TU 數量線性放大

200 TU 冷編譯:std::simd 7 分鐘、Highway 2 分鐘、ISPC 45 秒——10× 差距不是邊緣情境。

把滑桿拉到 200 TU——這是中型 codec library 的常見規模—— std::simd 版本要花 7 分鐘冷編譯, Highway 約 2 分鐘, ISPC 不到 45 秒, scalar baseline 約 40 秒。

把滑桿拉到 500 TU(接近 Chromium 媒體子系統的數量級), std::simd 要 18 分鐘, Highway 5 分鐘, ISPC 與 scalar 都不到 2 分鐘。 對於需要把 SIMD code 撒在 codebase 數十處的專案 (圖像 codec、ML kernel、數值庫), 編譯時間放大 10× 不只是 CI 變慢—— 是 incremental build 變得無法忍受, 也讓 sanitizer 與 fuzzing 的迭代週期被拖住。

incremental build 受影響特別嚴重:改一個 .cpp 檔可能觸發數十個 template instantiation 重新計算,10× 放大直接 dominate iteration cycle。 對 TDD 工作流來說,這個延遲從 5 秒拉到 50 秒,足以讓開發節奏跌回 context switch。

執行期效能:std::simd 在熱迴圈跑輸 scalar

下面這張圖是同樣的 8×8 矩陣轉置 + 元素 reduce benchmark 在不同方案上的相對速度。 基準是純 scalar 自動向量化(編譯器加 -O3 -march=native), 所有數字標準化到 1.0×。 資料取自 lucisqr substack 評測在 AVX2 機台上的測量。

std::simd 在跨 lane 工作量上跑輸 scalar auto-vectorization 約 2.4 倍,因為它把 128-bit SSE 當預設、無法表達 cross-lane 操作;ISPC 與 VCL 在同樣硬體上獲得 2-3× 加速;Highway 因為 length-agnostic 可以在 AVX2 機台用滿 256-bit width,比 scalar 略快。資料源:lucisqr substack 2026-05。

std::simd 在跨 lane 工作量上跑輸 scalar auto-vectorization 約 2.4 倍,…

8×8 矩陣轉置 benchmark:std::simd 比純 scalar 慢 2.4×,ISPC 快 2.8×——根源在 128-bit ABI 限制。

std::simd 預設使用 128-bit width,即使機器有 256/512-bit 可用—— 這是 implementation-defined 的決定,多數編譯器選擇最保守的 fallback。 原因在於 std::simd<int>::size() 的設計 回傳的是「ABI-safe 的 native width」: 在 AVX2 機器上回傳 4(即 128-bit / 32-bit = 4 lanes),不是 8。

設計者選擇 ABI 穩定性而非完整 width, 因為一個 simd object 的 size 與 alignment 必須在 ABI 層級可預測—— 如果不同 TU 對同一個 simd<int> 看到不同 width, 就會引入 silent ABI break。 一個函式簽名 void f(std::simd<int>), 在 caller 端編譯時看到 8-wide、callee 端編譯時看到 4-wide, 參數傳遞會徹底壞掉、出 silent corruption。 這個顧慮是真實的。

但解決方案不該是「永遠用 128-bit」。 Highway 的解法是 Lanes(d) 永遠是 compile-time constant、 且 d 永遠 reflect 目標平台的 native width—— 跨 TU 的 ABI 一致性靠 ODR 保證(兩個 TU 編到同樣的目標平台會看到同樣的 width)。 ISPC 的解法是把 SIMD function 包進 export interface、 target 由 build 系統強制統一——使用者不用思考 ABI。

這是「最有影響力的設計缺陷」: 當 scalar for-loop 加 -march=native 會自動向量化到目標機器的全 width (AVX2 機器上是 8-way),std::simd 卻只用 4-way—— 熱迴圈直接掉一半 throughput。

要強制使用更寬的 vector,需要明確指定 std::simd<float, std::simd_abi::deduce_t<float, 8>>, 但這樣 code 又失去了「寫一份跑全平台」的承諾。 寫成這樣的 code 在 ARM NEON 機器上要嘛編不過 (因為 NEON 沒有 8-way float), 要嘛 emulated 成兩次 NEON 操作,回到了「為每個目標寫一份特化」的老路。

Highway 的 length-agnostic 設計巧妙處理了這個: 迴圈以 Lanes(d) 為步長前進, 編譯時 d 已綁定到目標寬度(AVX2 = 8、AVX-512 = 16、NEON = 4), runtime 無 overhead。 這把「我有多寬」這個問題從「使用者必須知道」轉成「庫知道,使用者不需要關心」。

ARM SVE 的 vector length 是 runtime 才知道的 scalable,Highway 也能處理—— 它在 SVE 上產出真正的 scalable predicated 指令; 對照之下 GCC 14 對 std::simd 在 SVE 上 emit 的是 fixed-width 128-bit NEON 加 manually unrolled loops,產出的 assembly 比 Highway 長約 3 倍。 這對 ARM Neoverse 系列 server CPU(A64FX、N2、V2)的 SVE 部署是大問題。

另一個被忽略的細節是 mask 處理。 在 AVX2 上做 conditional store(「只把 result 寫回符合條件的位置」), 標準作法是 _mm256_maskstore_epi32。 std::simd 提供 simd_mask<T>where(mask, target) = value 語法,乍看乾淨; 但實際 codegen 在 GCC 14 上會把 mask 從 vector register move 到 general-purpose register 再 move 回來,多了三條 mov 指令。

Highway 直接用 MaskedStore(v, mask, ptr) 對應到 native intrinsic, 沒有額外 move; ISPC 因為 mask 是語言內建 type,編譯器知道 mask 一直在 vector register 裡。 對 hot loop 來說多三條 mov 不是天文數字,但累積起來在 codec 熱迴圈裡可以是 5-10% 的 throughput 差距。

對純 element-wise 數值工作(sum a buffer of floats)的 sin/cos/log/exp benchmark,std::simd 一樣輸給 scalar auto-vec: scalar 版本 137ns,std::simd 版本 326ns——慢 2.4 倍。 理由還是 width:scalar 編譯器看到 -march=native 把 loop unroll 成 8-way,std::simd 死守 4-way。 這不是評測者誇大,這是 std::simd 的 ABI 設計直接導致的。

對 lucisqr 評測的方法論做個檢查:用 sin 函數做 benchmark 並不完美, 因為 sin 的 SIMD 實作要依賴 __builtin_ia32_sincos_pd 或 SVML 之類的庫呼叫,這部分的 codegen 質量會 dominate 整體結果。 比較公平的 benchmark 是 polynomial 計算或純 multiply-add 迴圈, 這些操作 std::simd 與 scalar auto-vec 都可以完全 inline。 但即使換成 polynomial benchmark,std::simd 仍然輸 scalar—— 根本原因是 width 而不是個別操作的 codegen。

另一個方法論問題是:lucisqr 的 benchmark 是用 GCC 14 的 experimental 實作。 Clang 對 std::simd 還沒完整支援(截至 2026-05), MSVC 則完全沒有 native 實作(要嘛用 Vc 的後端、要嘛 fallback 到 scalar)。 所以這些 benchmark 數字反映的是「目前為止最成熟的 std::simd 實作」的表現; 將來實作品質提升後,數字可能改變。 但即便如此,10× 編譯時間放大是 spec-induced(template 深度), 不是 implementation issue;2.4× 執行慢是 width 設計,不是實作 bug—— 這兩個問題都很難在 implementation 層級修掉。

可攜性與部署:runtime dispatch 是 production 必備

Runtime dispatch(同一個 binary 在 AVX-512 機器跑寬 vector、 在舊機器 fallback 到 SSE)是 production 部署的必備能力。 Chromium、Firefox、JPEG XL 都靠 Highway 的 runtime dispatch 在一份安裝包裡支援從 SSE2 到 AVX-512 的所有 x86。

std::simd 沒有任何 runtime dispatch 機制; 要支援多個 ISA,要嘛編譯多份 binary、 要嘛自己用 IFUNC/dlopen 手工搭—— 這部分原本應該是標準庫該管的事。 一個「便攜 SIMD library」缺少 runtime dispatch, 等於是說「便攜性只到 source 層」—— binary 層的便攜性還是要使用者自己解決。 這個語意縮水跟原本 P0214 的 pitch(「Write SIMD code once, compile it for AVX2, AVX-512, NEON, SVE」)有相當距離。

ISPC 走另一條路:一次編譯產出多個 target variant (--target=sse4-i32x4,avx2-i32x8,avx512skx-i32x16),自帶 dispatcher。 對 production 來說這比 std::simd 完整得多, 代價是必須引入額外的 build 工具鏈與 .ispc → .o 編譯步驟。

ISPC 的 dispatcher 用 CPUID 在第一次呼叫時決定路徑,之後 cache 結果—— 比 IFUNC 的 dynamic linker 路徑甚至更快, 因為 IFUNC 需要 PLT indirection。 每次呼叫多一次 PLT lookup 對 hot loop 是不小的 cost, ISPC 的設計避開了這個。

std::simd 單一 binary 編譯時固定 width (128-bit) → 多 ISA 需要 IFUNC / dlopen 手工搭 標準層沒處理 production 採用:實驗階段 Highway 單一 binary length-agnostic loop runtime detect SSE2 / AVX2 / AVX-512 / SVE 已用於 Chromium、Firefox、JPEG XL production 採用:六個大型專案 ISPC 多 target 編譯 avx2 + avx512 並存 自帶 dispatcher CPUID 路由 已用於 Intel OSPRay、Blender Cycles 代價:額外 build 工具鏈 runtime dispatch 是 production 必備能力——std::simd 在標準層完全沒處理
同一份 binary 在不同 CPU 上自動選最寬 vector:Highway 與 ISPC 都內建這個能力,std::simd 把它推回給使用者。

同一份 binary 在不同 CPU 上自動選最寬 vector:Highway 與 ISPC 都內建這個能力,std…

Highway 內建 CPUID dispatch、ISPC 自帶 multi-target;std::simd 把 dispatch 整包推給使用者。

把 dispatch 推給使用者意味著什麼? 意味著每個用 std::simd 的專案必須自己重新發明同一套輪子。 寫一份 IFUNC selector 並不複雜 (GCC 提供 __attribute__((target_clones("default,avx2,avx512f")))), 但 IFUNC 在 musl、Windows、macOS 上的支援度都不一樣, 且與動態載入器互動有 corner case(早期 binding、static initializer 中的呼叫)。

Highway 與 ISPC 把這些 platform-specific 的坑都吸收掉了, 使用者只看到 HWY_DYNAMIC_DISPATCH(MyFunc) 或 ISPC export function 兩個介面。 這個「平台差異被庫吸收」的價值無法量化到 benchmark 上, 但在 production 運維時是大事。

Highway 與 ISPC 把 fault handling、AVX-512 frequency-throttling、 cold start 這些 deployment 痛點都吸收了; std::simd 把整包推回給使用者, 相當於要求每個專案自己重做一次 Highway 的工程量。 對 Chromium 這種有專職 codec 團隊的還可行; 對小型 OSS 專案基本上不可能,最後就會放棄 dispatch、編一份 baseline binary, 把「便攜性」的承諾打折。

另一個容易被忽略的可攜性議題是:std::simd 的 SVE 支援。 ARM SVE 與 RISC-V Vector 是 scalable vector: vector length 在 runtime 才知道, 且可能是 128、256、512、1024、2048 bit 中任一個。 std::simd 的 ABI 設計裡 simd<float>::size() 必須是 compile-time constant,這與 SVE 的設計直接衝突。

GCC 14 對 SVE 的 std::simd backend 強制 fixed-width(通常選 128 或 256), 等於放棄 SVE 真正的價值(在不同 vector length 的 CPU 上自動 scale)。 Highway 在 SVE 上產出真正的 scalable predicated 指令, 這是 std::simd 用 library 介面達不到的層次。 A64FX 上 SVE 是 512-bit,N2 上是 128-bit,V2 上是 256-bit—— Highway 寫一份 source 可以分別生成最優 codegen; std::simd 強制 fixed-width 等於把這三個 target 都降到 128-bit。

對 RISC-V Vector,狀況更糟。 RISC-V Vector ISA 設計就是「vector length 不固定且 runtime 可變」, 這幾乎讓所有 fixed-width assumption 都失效。 Highway 對 RVV 的支援還在試驗中但已有 working backend; std::simd 對 RVV 沒有任何 spec-level 的應對—— 這意味著未來 RISC-V 在伺服器市場拿到份額時, std::simd 仍然會是 fixed-width 的 SSE-equivalent。

這個 timing 不巧—— RISC-V Vector 在 2026 開始進入 datacenter market(SiFive、阿里、玄鐵), Cloud 廠商也開始評估 RVV server SKU; std::simd 在 RVV 上的限制會直接限制它在這個新興市場的採用。 Highway 反而因為 length-agnostic 設計,在 RVV 上能拿到接近手寫 assembly 的效能。

該怎麼選:四種 production 情境的對應方案

四個維度排起來,std::simd 的可用領域其實非常窄: 純 element-wise 工作量、單一目標架構(不需要 dispatch)、 編譯時間不敏感的小規模 binary。 寫個教學範例或 toy benchmark 適合; production 影像/數值/密碼學熱迴圈完全不該採用。

大多數 production C++ SIMD 工作量,選 Highway—— 它在編譯時間、執行效能、可攜性三個維度都優於 std::simd, 且已在 Chromium、Firefox、JPEG XL、libaom、Jpegli、libvips 等大型專案驗證。

Highway 的學習曲線比 std::simd 略陡 (要熟悉 tag-based API、ScalableTag、CappedTag 的差別),但一週就能熟練。

若工作量是密集的數值/科學計算、且能接受引入 ISPC 編譯工具鏈, ISPC 仍然是表現最強的選擇(2-4× 加速 vs scalar)。 OSPRay 與 Blender Cycles 證明 ISPC 在 ray tracing 與 path tracing 這類 lane-divergent 工作量上可以拿到接近理論極限的吞吐量—— 不同 ray 走不同 BVH 路徑的 lane divergence 在 ISPC 的 program model 下 被 SIMD lane 自然處理;在 C++ + Highway/std::simd 上要手寫 mask handling 與 lane shuffle。

為了讓「library 抽象 vs 語言抽象」的差距具體一點,下方把同一個任務(lane-conditional accumulate: 只把陣列中正值加進 sum)用三個方案各寫一遍——切 tab 在 std::simd、Highway、ISPC 之間切換。 讀者會看到 std::simd 必須顯式 mask + where、Highway 用 IfThenElseZero 表達意圖、 ISPC 直接寫 if-else 由編譯器解 mask。

// std::simd: mask 必須顯式構造、where() 語法把 conditional 嵌進 expression
using V = stdx::native_simd<float>;
V sum{0.0f};
for (size_t i = 0; i + V::size() <= n; i += V::size()) {
  V x(a + i, stdx::element_aligned);
  auto mask = x > V{0.0f};         // simd_mask<float>
  stdx::where(mask, sum) += x;       // codegen 多 3 條 mov vec↔gpr
}
return stdx::reduce(sum);
// Highway: IfThenElseZero 直接對應 native masked-add;length-agnostic
const hn::ScalableTag<float> d;
auto sum = hn::Zero(d);
const auto zero = hn::Zero(d);
for (size_t i = 0; i + hn::Lanes(d) <= n; i += hn::Lanes(d)) {
  auto x = hn::LoadU(d, a + i);
  auto m = hn::Gt(x, zero);          // Mask<decltype(d)>
  sum = hn::Add(sum, hn::IfThenElseZero(m, x));  // mask 留在 vec reg
}
return hn::ReduceSum(d, sum);
// ISPC: lane 是語言內建概念,if-else 由編譯器自動 lower 成 mask
export uniform float sum_positive(uniform float a[], uniform int n) {
  varying float partial = 0;
  foreach (i = 0 ... n) {
    if (a[i] > 0) partial += a[i];   // mask 是隱式的
  }
  return reduce_add(partial);
}

三個版本做同樣的事,但抽象層級遞減:std::simd 把 mask 當 type 顯式操作;Highway 把 mask 當 lane-aware primitive 包好;ISPC 直接讓 lane 成為控制流程的一部分,不需要寫 mask。

若是 x86-only 的高效能小庫(嵌入式 codec、HFT 內迴圈、加密庫) 且追求 codegen 的可預測性, Agner Fog 的 VCL 提供最直接的 intrinsics-shaped 抽象—— 它本質上是一層薄薄的 C++ wrapper 包住 x86 intrinsics, 編譯時間幾乎沒放大,runtime 效能與手寫 intrinsics 持平。 VCL 不解決 portable 問題(它只支援 x86), 但對 HFT 與專有 codec 而言這通常不重要。

VCL 還有一個特別的優勢: Agner Fog 本人是 microarchitecture 文檔的主要編寫者, VCL 的 codegen 對 specific x86 microarchitecture(Skylake、Ice Lake、Zen 4) 都有針對性的 micro-optimization—— 例如避免 port pressure、避免特定 fwd-store dependency。 這些優化在 Highway 與 ISPC 上不會看到, 因為它們的目標是 portable 不是 hyper-optimized。

若工作量是 element-wise 為主、能接受失去 cross-lane 操作、 且專案規模小(< 20 TU)以致編譯時間放大不重要、 且只跑單一目標架構——這時候 std::simd 才合理。 教學範例、benchmark harness、toy ML kernel 大致符合這個 profile。 但這個 profile 也是 scalar for-loop + -march=native 完美匹配的 profile, 所以 std::simd 在這個區段的真實競爭對手是「什麼都不做」。

把這四種情境疊起來看, std::simd 的「能用、但不該用」區段其實是其他工具更好的全部交集。 這是 critique 真正尖銳的地方: std::simd 沒有獨佔任何 use case。 它在每個維度上都有更好的競品,且競品已經在 production 跑了多年。

關於這個現象有兩種解讀。 樂觀的解讀是:std::simd 在標準裡, 未來的工具鏈會把它變成更好的工具的 building block—— 例如未來 C++30 可能在 std::simd 上加 shuffle/permute 的 expression template, 或加 runtime dispatch hook。

悲觀的解讀是:committee 花十年磨出 P0214 的時候, 產業已經繞過它解決問題了; 之後再 patch std::simd 是亡羊補牢, 且因為 ABI 包袱(已經發布的 std::simd ABI 不能變),patch 的空間很有限。 經驗上 C++ 標準庫 ABI 一旦發布,後續 evolution 都非常受限—— 這也是為什麼 std::regex 與 std::unique_ptr 早期設計缺陷至今無法修正。

對 production 工程師的實用建議很直接: 今天不要把 SIMD 熱迴圈賭在 std::simd 上。 Highway 是 80% production C++ SIMD 工作量的正確選擇, ISPC 是高效能科學計算的選擇, VCL 是 x86-only 小庫的選擇。 std::simd 是「教學上有用、生產上避開」的選擇。

具體建議:新專案把 Highway 當 default (header-only、Apache 2.0、vcpkg/Conan 都拿得到,整合成本接近零); 既有 std::simd codebase 遷移到 Highway 大致是 1-3 週的工作, element-wise API 幾乎一對一翻譯,cross-lane 部分反而是減負。 標準委員會的步調以「年」為單位,Highway 與 ISPC 的迭代以「週」為單位—— 當 production 賭注在「下一個 ISA」(AVX10、SVE2、RVV 1.0)能否被支援時, 這個速度差就是生死線。

Take-away:std::simd 標準化了一個只解決 SIMD 工作量「最簡單那 10%」的庫——對 production 而言,Highway 在 80% 的情境贏,ISPC 在剩下 20% 的高效能情境贏;std::simd 的真正價值是教學與標準化背書,不是工程能力。