C++ 效能面試實戰:六道「誰比較快」
2026-03-31
某 Crypto / HFT 公司的 C++ 面試,六道 “分析這段 code 的效能” 題目。 前面被電爛,最後一題 Order Book 稍微扳回一城。 題目經過改寫,分析是我事後整理的。
Q1 - 建構子參數傳遞
有一個 struct,成員是 std::string,建構子需要把外部傳入的字串存下來。 比較以下三種寫法的效能差異:
// 寫法 A:pass by value + move
struct Widget {
std::string name;
Widget(std::string n) : name{std::move(n)} {}
};
// 寫法 B:pass by string_view
struct Widget {
std::string name;
Widget(std::string_view n) : name{n} {}
};
// 寫法 C:pass by const reference
struct Widget {
std::string name;
Widget(const std::string& n) : name{n} {}
};分析
三種寫法各有不同的 copy / move 成本,且會因 caller 傳的是 lvalue 還是 rvalue 而改變:
| 傳入 | A: value + move | B: string_view | C: const ref |
|---|---|---|---|
| lvalue string | 1 copy + 1 move | 1 copy | 1 copy |
| rvalue string | 2 moves | 1 copy | 1 copy |
| string literal | 1 construct + 1 move | 1 construct | 1 construct (temp) + 1 copy |
| const char* | 1 construct + 1 move | 1 construct | 1 construct (temp) + 1 copy |
實務上如果這個 constructor 只有一個 string 參數,A 是最平衡的選擇 - 程式碼簡單且 rvalue 效能最佳。 B 是效能最好的選擇,但 caller 必須知道傳的是 view(不能假設 ownership transfer)。 如果 caller 幾乎都傳 lvalue(例如從 config 讀值),B 和 C 效能相同。
Q2 - Lambda 捕獲模式分析
以下四種 lambda 寫法,分析各自的效能與正確性:
void process(std::string_view msg) {
// 寫法 0:static + capture by reference
static const auto fn0 = [&]() {
std::cout << msg << "\n";
};
fn0();
}
void process(std::string_view msg) {
// 寫法 1:non-static + capture by reference
const auto fn1 = [&]() {
std::cout << msg << "\n";
};
fn1();
}
void process(std::string_view msg) {
// 寫法 2:static constexpr + parameter
static constexpr auto fn2 = [](std::string_view s) {
std::cout << s << "\n";
};
fn2(msg);
}
void process(std::string_view msg) {
// 寫法 3:non-static + parameter
const auto fn3 = [](std::string_view s) {
std::cout << s << "\n";
};
fn3(msg);
}逐一拆解
static 意味著 lambda 物件只初始化一次。第一次呼叫時 [&] 捕獲的是 第一次 msg 的 reference。之後每次呼叫,msg 的位址不同,但 lambda 仍然持有第一次的 reference - 這是一個 dangling reference。這是最嚴重的 bug。每次呼叫都重新建構 lambda(捕獲當前的
msg),reference 永遠有效。 Lambda 物件在 stack 上,compiler 幾乎一定會 inline。Stateless lambda(no capture),compiler 可以當成普通 function pointer。
constexpr 保證編譯期初始化,static 只是一個 hint(stateless lambda 沒有 state 可以 init)。 不依賴任何外部 reference,完全沒有 lifetime 問題。也是 stateless lambda,compiler 同樣會 inline。跟寫法 2 的差異在理論上可以忽略 - 少了
static 但 stateless lambda 本身就沒有初始化成本。關鍵教訓:static + capture by reference 是一個非常隱蔽的 bug。 面試官特別想看你能不能一眼看出寫法 0 是 UB。 在 hot path 上,stateless lambda(寫法 2/3)是首選 - compiler 會完全 inline 成 function call。Q3 - Class 內的 Lambda
把 lambda 放在 class method 裡,比較 [this] 捕獲 vs 直接傳參數:
class Handler {
std::string data;
public:
// 寫法 0:static + [this]
void run0() {
static const auto fn = [this]() {
std::cout << data << "\n";
};
fn();
}
// 寫法 1:non-static + [this]
void run1() {
const auto fn = [this]() {
std::cout << data << "\n";
};
fn();
}
// 寫法 2:static constexpr + string_view parameter
void run2() {
static constexpr auto fn = [](std::string_view s) {
std::cout << s << "\n";
};
fn(data); // string → string_view 隱式轉換
}
// 寫法 3:non-static + const string& parameter
void run3() {
const auto fn = [](const std::string& s) {
std::cout << s << "\n";
};
fn(data);
}
};分析
static lambda 只初始化一次,this pointer 被永久固定。 如果 Handler 物件被 move、destroy、或不同 instance 呼叫 run0(), lambda 裡的 this 指向的是第一個 instance 的位址 - dangling pointer,UB。 編譯器實際上會把
[this] 展開成類似 struct { Handler* thiz; void operator()() { ... thiz->data ... } }, 而 static 讓這個 pointer 永遠不更新。this,compile 後基本等同 inline function call。 唯一風險是如果 lambda 被存起來傳到外部(escape),this 可能 dangle - 但在 method 內直接呼叫沒問題。string_view 傳遞只是 pointer + size,零 copy。const string& 和 string_view 在這個場景下效能幾乎一樣, 因為都只傳 reference / pointer,不做 copy。Pattern:在 class method 裡的 lambda,能不 capture 就不 capture。 改用 parameter 傳入需要的資料,lambda 就是 stateless 的, compiler 優化空間最大,也不會有 lifetime 問題。
Q4 - Pointer 參數 vs Value 參數
函式需要對兩個字串做複雜操作,比較 pointer 和 value 傳遞:
// 寫法 A:第二個參數用 pointer
void transform(std::string x, std::string* y) {
// 對 x 和 *y 做複雜操作
}
// 寫法 B:兩個都 pass by value
void transform(std::string x, std::string y) {
// 對 x 和 y 做複雜操作
}差異分析
| 面向 | A: string* | B: string value |
|---|---|---|
| Copy 成本 | y 不 copy(只傳 8B pointer) | y 會被 copy(可能 heap alloc) |
| Null safety | y 可能是 nullptr - 需要檢查 | 不可能是 null |
| Aliasing | *y 可能 alias x(同一物件) | x 和 y 獨立,不會 alias |
| 副作用 | 可以修改 caller 的原始字串 | 修改不影響 caller |
| Compiler 優化 | pointer aliasing 限制優化 | value 獨立,optimizer 更自由 |
面試的重點不是選哪個 “對”,而是能不能完整列出差異: copy cost、null safety、aliasing、side effects、optimizer freedom。 在 HFT context 下,答案傾向 pointer(或 reference) - 因為 zero-copy 至上。
Q5 - SSO 底層實作
“std::string 底層怎麼實作 Small String Optimization?”
核心:union - 同一塊記憶體,兩種用途
std::string 物件內部有一塊固定大小的 union。當字串長度小於閾值,字元直接存在物件內部(stack / static 上); 超過閾值才 malloc heap 空間:
// 簡化版 std::string 結構(以 GCC libstdc++ 為概念)
class basic_string {
size_t _size;
union {
// 短字串模式:字元直接存在 16 bytes 裡
char _short_buf[16]; // 15 chars + \0
// 長字串模式:pointer + capacity
struct {
size_t _capacity;
char* _ptr; // → heap
} _long;
};
// sizeof ≈ 8 (size) + 16 (union) = 24~32(因 alignment)
};string 物件固定 32 bytes(64-bit, GCC)
┌────────┬────────────────────────┐
│ size │ union │
│ (8B) │ (16B) │
│ ├─ 短字串模式 ───────────┤
│ │ char buf[16] │ ← 字元直接存這裡
│ ├─ 長字串模式 ───────────┤
│ │ capacity(8B) + ptr(8B)│ ← ptr 指向 heap
└────────┴────────────────────────┘
↑ 同一塊記憶體,兩種解讀方式
SSO 閾值(常見 64-bit 實作):
GCC libstdc++: ~15 bytes
Clang libc++: ~22 bytes
MSVC: ~15 bytesSSO 的代價
SSO 不是免費的 - sizeof(std::string) 通常是 32 bytes(比 sizeof(std::vector) 的 24 bytes 大), 因為 union 要容納 inline buffer。這是用「更大的物件」換「更少的 heap allocation」。
這題是前面 Q1 的延伸 - 如果你知道 SSO,就能更精確地分析 string 傳遞的成本: 短字串(≤ 15 bytes)的 copy 不走 heap,成本就是 memcpy 32 bytes(stack 上), 遠比長字串的 malloc + memcpy 便宜。
Q6 - Order Book 資料結構設計
最後一題是系統設計。需求是實作一個 Order Book(訂單簿),支援高精度價格和兩種更新模式。
需求
Order Book 結構:
price quantity
130.000 1 ← 最高 bid
119.000 1
118.000 1
...
111.000 99
110.000 100 ← best bid
------------------------------
109.000 99 ← best ask
108.000 94
...
90.000 3 ← 最低 ask
限制條件:
- price precision: 18 位小數
- minimum tick size: 已知(如 0.5)
- 兩種更新模式:
Case 1: Snapshot - 一次收到完整 N 層報價,全部覆蓋
Case 2: Delta - 只收到有變動的 level
bid: [ { price: 111, qty: 0 }, { price: 111.5, qty: 60 } ]
ask: [ { price: 90, qty: 0 }, { price: 108, qty: 100 } ]
(qty = 0 代表移除該 level)為什麼不能用 double 當 key
面試中 interviewer 直接給了 std::map<double, double> bid, ask 的暗示, 這正好是要你挑戰的:
// Bad: floating point 比較不精確
std::map<double, double> bid; // ✗
// 0.1 + 0.2 == 0.3 是 false - 這在 order book 裡是致命的
// 兩個 "相同" 價格可能被當成不同 keyPrecision 18 意味著價格需要精確到 18 位小數。浮點數只有 ~15-16 位有效數字。 正確做法是轉成整數:
// Good: 把價格轉成整數 tick
// 如果 min tick = 0.5, precision = 18
// price 111.5 → tick_index = 111.5 / 0.5 = 223
// 或直接用 fixed-point integer: price * 10^18
using Price = int64_t; // fixed-point, 或 tick index
using Qty = int64_t;
// bid 要快速找最大 → 降序
std::map<Price, Qty, std::greater<Price>> bid;
// ask 要快速找最小 → 升序(預設)
std::map<Price, Qty> ask;兩種更新模式的實作
// Case 1: Snapshot - 全量更新
void applySnapshot(
std::map<Price, Qty, std::greater<Price>>& bid,
const std::vector<std::pair<Price, Qty>>& levels
) {
bid.clear();
for (const auto& [px, qty] : levels) {
if (qty > 0) bid[px] = qty;
}
}
// Case 2: Delta - 增量更新
void applyDelta(
std::map<Price, Qty, std::greater<Price>>& bid,
const std::vector<std::pair<Price, Qty>>& deltas
) {
for (const auto& [px, qty] : deltas) {
if (qty == 0) {
bid.erase(px); // qty=0 代表移除
} else {
bid[px] = qty; // 新增或更新
}
}
}進階考量
std::map vs 其他選擇
std::map(紅黑樹)O(log n) 查找/插入/刪除,cache locality 差。 如果 level 數量固定(如 top 20),sorted array + binary search 可能更快(cache friendly)。 如果需要極致效能,可用 flat sorted vector 或 B-tree variant。
Sequence number / 防亂序
實際系統中 delta 可能亂序到達。需要 sequence number 來偵測 gap, 遇到 gap 時 fallback 到 snapshot 重建。
Lock-free 設計
HFT 場景中 order book 更新在 hot path 上。常見做法是 single-writer(market data thread 獨佔更新), reader 用 seqlock 或 RCU 做 lock-free read。
這題的核心考點:(1) 不用 double 當 key - fixed-point 或 tick index; (2) snapshot vs delta 兩種模式的 trade-off; (3) 對 HFT 場景的 latency-aware 思維。
總結
| 題目 | 核心觀念 | 一句話 |
|---|---|---|
| Q1 | String 傳遞 | string_view 最便宜,value+move 最通用 |
| Q2 | Lambda 捕獲 | static + capture = dangling reference,stateless 最安全 |
| Q3 | Class Lambda | static + [this] 一樣會 dangle,用 parameter 取代 capture |
| Q4 | Pointer vs Value | 列出 copy/null/alias/side-effect/optimizer 五個面向 |
| Q5 | SSO | union + inline buffer,短字串零 malloc |
| Q6 | Order Book | fixed-point key,snapshot vs delta,latency-aware |
面試心得
Crypto / HFT 公司的 C++ 面試跟一般 backend 面試非常不同 - 不考 LeetCode, 考的是你對語言底層機制的理解深度。每一題都是 “這幾種寫法誰快、為什麼”, 需要你能即時分析 copy / move / allocation 的成本。 建議準備方向:move semantics、lambda 捕獲機制、memory layout、compiler optimization。