# SOO :::warning 前情提要 [Facebook](https://www.facebook.com/groups/system.software2022/posts/2081326045382128/) [微中子的貼文](https://tigercosmos.xyz/post/2022/06/c++/smallvec/) ::: SSO and SOO 我相信大家都不陌生,詳細可以看 Mes 鄭教授的[這篇介紹](https://hackmd.io/@Mes/Miner_Magic_SSO)。不過對於[微中子的貼文鏡像](http://archive.today/Aw9tT)中的測試方式第一眼我是有疑慮的 這邊有幾點需要考慮: 1. [物件可不可以被優化,而搬到迴圈之外?](#物件可不可以被優化,而搬到迴圈之外?) 2. [如何在 stack 上生成 array ?](#如何在stack上生成array?) 3. [更好的 SSO 評估方式](#更好的SSO評估方式) > 有點抱歉中英文沒有加空白鍵,讀起來有一點點痛苦,這是為了要實現 href 讓你方便直接跳到那個 tag ## 物件可不可以被優化,而搬到迴圈之外? 假定你已經熟悉這優化,也知道 RAII,我們就直接來看組語吧! [Godbolt](https://godbolt.org/z/q8xs49Tsx) 先看 C++ ```cpp= template <typename T, size_t N = 10> class SmallVec { public: SmallVec() = default; void push_back(T const & value) { if(cur < N) m_data[cur++] = value; else m_v.push_back(value); } private: std::vector<T> m_v; std::array<T, N> m_data; size_t cur = 0; }; ``` 對照抽取的部份 asm ```asm= .L19: mov QWORD PTR [rsp+80], 0 xor eax, eax xor r14d, r14d xor ebp, ebp xor r15d, r15d xor ebx, ebx jmp .L15 .L39: lea rdx, [rax+1] mov QWORD PTR [rsp+80], rdx mov DWORD PTR [rsp+40+rax*4], ebx .L3: add ebx, 1 cmp ebx, 10 je .L14 .L40: mov rax, QWORD PTR [rsp+80] .L15: mov r12, r14 sub r12, rbp cmp rax, 9 jbe .L39 cmp r14, r15 je .L4 mov DWORD PTR [r15], ebx add ebx, 1 add r15, 4 cmp ebx, 10 jne .L40 .L14: test rbp, rbp je .L16 mov rsi, r12 mov rdi, rbp call operator delete(void*, unsigned long) sub r13, 1 jne .L19 ``` 注意到 .L14 這邊就是 dtor,可以明顯看到我們確實讓他在 .L15 的 Line 26 mov DWORD PTR [r15], ebx 把資料般到 r15 之後,如過超出迴圈範圍,會跳到 .L14 呼叫 dtor 我們把他加上一個 std::vector 作為 member,這樣當 sv 需要被解構的時候必須要去解構 std::vector,這樣就無法被 compiler 化簡 dtor。 所以我們得到 std::vector 跟 SmallVec 效能相似的結果 ![](https://i.imgur.com/EVGRDO3.png) 而效能比 std::vector 好,我認為是分支比較少的關係,因為我們可以觀察到: sv 的 push_back 相當簡單,只有一個分支而後就可以搬動資料 ```asm= .L19: mov QWORD PTR [rsp+80], 0 xor eax, eax xor r14d, r14d xor ebp, ebp xor r15d, r15d xor ebx, ebx jmp .L15 .L71: lea rdx, [rax+1] mov QWORD PTR [rsp+80], rdx mov DWORD PTR [rsp+40+rax*4], ebx ``` 而,std::vector 的 push_back 分支處理就比較多次: ```asm= .L31: mov r12, r14 sub r12, r15 cmp rbx, r14 jne .L74 movabs rcx, 2305843009213693951 mov rax, r12 sar rax, 2 cmp rax, rcx je .L75 cmp rbx, r15 mov edx, 1 cmovne rdx, rax add rax, rdx jc .L25 test rax, rax jne .L76 xor r9d, r9d xor r14d, r14d xor r13d, r13d .L27: mov DWORD PTR [r13+0+r12], ebp lea rbx, [r13+4+r12] test r12, r12 jg .L77 test r15, r15 jne .L29 .L30: # Loopping add ebp, 1 mov r12, r9 mov r15, r13 cmp ebp, 10 jne .L31 ``` 觀察到 .L31 的邊界檢查以及移動邊界,分支失敗多次對效能是很有傷害的。 所以,即便 vector 的仍快取在 page table 上,也會有相較於純 array 1.5 倍的邊界負擔。 ## 如何在stack上生成array? [Godbolt](https://godbolt.org/z/Y8aj4Wcqs) 首先我們看到 Line 10: ```asm= main: push r15 mov eax, 3735928495 push r14 push r13 push r12 movabs r12, 2305843009213693950 push rbp push rbx sub rsp, 104 mov QWORD PTR [rsp+24], rax lea r13, [rsp+56] mov rbp, r13 ``` 對應的 Cpp 是: ```cpp= constexpr int K = 10; namespace { template <typename T, size_t N = 10> class SmallVec { public: explicit SmallVec(size_t size); void push_back(T const & value); T operator[](size_t index); private: T *m_head = nullptr; size_t m_size = 0; size_t m_capacity = N; std::array<T, N> m_data; }; }; ``` 104 是被 alignment 優化的,優化 stack 往上增長以 8 的倍數做考量(因為是 64-bit)。我們可以明顯看到 sub rsp, 104 就是推移 stack pointer 104 byte 預留給這個 array 跟 member data。 ### Array 的運作方式 如上是直接推移 stack pointer 沒有對 memory 做額外的 allocation,基本上跟 [alloca](https://man7.org/linux/man-pages/man3/alloca.3.html) 等價。 而 std::vector 每次都會去 heap allocation 所以會有微中子這篇的結果: ![](https://user-images.githubusercontent.com/18013815/174789476-7c2693bd-55b1-47cd-a588-2be5c36d8dbc.png) ### 迴圈與函式呼叫 會有這議題是因為 sv 物件生命週期在迴圈的 scpoe 當中,所以我們期待要將 sv 物件在迴圈離開(或重入)的時候解構。但是這個 sv 物件的 stack 上成員其實是可以重複利用的,所以在 -fcombine-stack-adjustments (-O1 就有) 開啟的情況下,是可以將個一個 sv 物件的 array reference 到 stack 上 array。 而且迴圈不會推動 stack 的指標,所以這邊在建構及解構 sv 物件的時後,頂多是洗掉 array 中的值,也不會將 array re-allocation 去推動 stack pointer。 所以這觀念有點像 tail-recursion,用跳躍的方式去執行重複步驟,而不用額外推 stack 保存狀態。 ### 有一點奇怪的測試結果 考慮到上面是把 array 放在 stack 上,所以比反覆 malloc 在 heap 效能好,那麼我們把 array 放在 heap 上,同時也把 vector 的 reserve 打開(減少 space doubling 的誤差)。 這樣兩個效能應該要近似: ![](https://i.imgur.com/R6gQLx3.png) 確實是有點相似,但是為什麼 std::unique_ptr<std::array<T, N>> 會比 std::vector 還要差呢? 我還沒有好的解釋,std::unique_ptr 除了搬到 heap 上,應該是沒有其他 overhead 才對,歡迎告訴我。(其實我還沒去看 asm,但是我想可以從 asm 看出 std::unique_ptr 的問題) ## 更好的SSO評估方式 因為重點是小物件的分配可以放在 stack 上,然而 stack 是 fixed length 的,即便你跨越這個 function stack 預期的空間使用,你還是有可能會遇到超出 stack page 的 stack overflow,所以我們設計一個包裝 (wrapper) 用來包裝 array 跟 vector,然後跟指標指向 vector 比較,以減少 RAII dtor 對效能的衝擊。 > 其實我覺得 RAII 是好東西,但是對於最極度的優化(最佳化),是需要調整的。 上面用 std::unique_ptr 把 array 搬到 heap 是一個方式,這樣可以「控制變因」,讓兩個比較都有經過經過 heap 上相同的解構次數。 但是,更好的測定方式是將造成干擾的變因移除,不是加入干擾。 所以我們把 vector 搬到迴圈外,然後在迴圈內使用 clear 而不是 dtor,這樣可以有效的移除 dror 在迴圈結束的後,造成的衝擊。 ### 保留 std::unique_ptr 與 std::vector 的比較 ![](https://i.imgur.com/p5hy1n0.png) ```cpp= static void SmallVector(benchmark::State& state) { for (auto _ : state) { SmallVec<int> sv; // 使用 inline memory for(int i = 0 ; i < K; i++) { sv.push_back(i); } } } BENCHMARK(SmallVector); static void StdVector(benchmark::State& state) { std::vector<int> v; v.reserve(K); // 分配新的 Heap 記憶體 for (auto _ : state) { v.clear(); for(int i = 0 ; i < K; i++) { v.push_back(i); } } } BENCHMARK(StdVector); ``` 我們可以看到這樣 vector 就顯著的比原本好多了,可見如我們預期主要影響因子是 dtor。 ### 把 dtor 都消除,比較都在 heap 上的結果 ![](https://i.imgur.com/BYLkPnq.png) 可以看到 std::vector<> 的處理似乎比較好,但是原因尚未清楚。 看起來 mov 0xc(%rsp),%eax 是把 i 複製到 array 當中,花上了 50% 的效能 ![](https://i.imgur.com/EDKVgEg.png) #### 把 O3 改為 O2 ![](https://i.imgur.com/HroFjcb.png) 我對 O3 的魔法不清楚, [GNU 文件](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) 上面提到: > -fgcse-after-reload -fipa-cp-clone -floop-interchange -floop-unroll-and-jam -fpeel-loops -fpredictive-commoning -fsplit-loops -fsplit-paths -ftree-loop-distribution -ftree-partial-pre -funswitch-loops -fvect-cost-model=dynamic -fversion-loops-for-strides 但是究竟是什麼激進的提示,讓 compiler 可以作到上面化簡,不清楚。 ## 定長治百病 既然上面結果這樣,我們就可以嘗試把 std::unique_ptr 移除,讓 SOO 保留在 stack 上。也不要有動態長度。 ![](https://i.imgur.com/iArbvYm.png) 我們得到這樣的結果。太棒了,SOO 讓物件搬到 stack,這樣完全消除了容器的 overhead。 :::success 看到這精美的效能優勢, Yes, 有極致的效能就是身心舒暢。 ::: ![](https://i.imgur.com/N6lmJnF.png)