← Back to Blog
C++PerformanceInterview

C++ 效能面試實戰:六道「誰比較快」

2026-03-31

某 Crypto / HFT 公司的 C++ 面試,六道 “分析這段 code 的效能” 題目。 前面被電爛,最後一題 Order Book 稍微扳回一城。 題目經過改寫,分析是我事後整理的。

Q1 - 建構子參數傳遞

有一個 struct,成員是 std::string,建構子需要把外部傳入的字串存下來。 比較以下三種寫法的效能差異:

cpp
// 寫法 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 + moveB: string_viewC: const ref
lvalue string1 copy + 1 move1 copy1 copy
rvalue string2 moves1 copy1 copy
string literal1 construct + 1 move1 construct1 construct (temp) + 1 copy
const char*1 construct + 1 move1 construct1 construct (temp) + 1 copy
Fastest
B (string_view) - 在所有情境下都只做一次 string construction(從 view 建構),沒有多餘的 move 或 copy。 string_view 本身只是 pointer + length,建構零成本。
OK
A (value + move)- rvalue 情境下接近零成本(兩次 move 幾乎免費), 但 lvalue 情境多了一次 copy。這是 Modern C++ 常見推薦的 “sink parameter” 模式。
Slower
C (const ref) - lvalue 時跟 B 一樣(1 copy),但 rvalue / literal 時無法利用 move, 永遠是 copy。對 string literal 還多一次隱式 temp construction。
實務上如果這個 constructor 只有一個 string 參數,A 是最平衡的選擇 - 程式碼簡單且 rvalue 效能最佳。 B 是效能最好的選擇,但 caller 必須知道傳的是 view(不能假設 ownership transfer)。 如果 caller 幾乎都傳 lvalue(例如從 config 讀值),B 和 C 效能相同。

Q2 - Lambda 捕獲模式分析

以下四種 lambda 寫法,分析各自的效能與正確性:

cpp
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);
}

逐一拆解

Dangerous
寫法 0:static + [&] - Undefined Behavior!
static 意味著 lambda 物件只初始化一次。第一次呼叫時 [&] 捕獲的是 第一次 msg 的 reference。之後每次呼叫,msg 的位址不同,但 lambda 仍然持有第一次的 reference - 這是一個 dangling reference。這是最嚴重的 bug。
OK
寫法 1:non-static + [&] - 安全且高效。
每次呼叫都重新建構 lambda(捕獲當前的 msg),reference 永遠有效。 Lambda 物件在 stack 上,compiler 幾乎一定會 inline。
Fastest
寫法 2:static constexpr + parameter - 最快且最安全。
Stateless lambda(no capture),compiler 可以當成普通 function pointer。constexpr 保證編譯期初始化,static 只是一個 hint(stateless lambda 沒有 state 可以 init)。 不依賴任何外部 reference,完全沒有 lifetime 問題。
OK
寫法 3:non-static + parameter - 安全,效能與寫法 2 幾乎相同。
也是 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 直接傳參數:

cpp
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);
    }
};

分析

Dangerous
寫法 0:static + [this] - 跟 Q2 一樣的問題,更危險。
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 永遠不更新。
OK
寫法 1:non-static + [this] - 安全。 每次呼叫都重新捕獲當前的 this,compile 後基本等同 inline function call。 唯一風險是如果 lambda 被存起來傳到外部(escape),this 可能 dangle - 但在 method 內直接呼叫沒問題。
Fastest
寫法 2:static constexpr + string_view - 最快且最安全。 Stateless lambda,不依賴任何 instance。string_view 傳遞只是 pointer + size,零 copy。
OK
寫法 3:non-static + const string& - 安全,效能接近寫法 2。 也是 stateless lambda。const string&string_view 在這個場景下效能幾乎一樣, 因為都只傳 reference / pointer,不做 copy。
Pattern:在 class method 裡的 lambda,能不 capture 就不 capture。 改用 parameter 傳入需要的資料,lambda 就是 stateless 的, compiler 優化空間最大,也不會有 lifetime 問題。

Q4 - Pointer 參數 vs Value 參數

函式需要對兩個字串做複雜操作,比較 pointer 和 value 傳遞:

cpp
// 寫法 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 safetyy 可能是 nullptr - 需要檢查不可能是 null
Aliasing*y 可能 alias x(同一物件)x 和 y 獨立,不會 alias
副作用可以修改 caller 的原始字串修改不影響 caller
Compiler 優化pointer aliasing 限制優化value 獨立,optimizer 更自由
Fastest
A (pointer) - 如果函式只需要讀取 y,pointer 省去了整個 copy。 這在 HFT 場景很重要 - 一次 string copy 可能觸發 heap allocation, 而 heap allocation 是 latency spike 的主要來源之一。
OK
B (value) - 更安全、語意更清楚、compiler 可以更自由地優化。 如果函式需要修改自己的副本,或 caller 傳的是 rvalue(move 進來零成本),B 更合理。
面試的重點不是選哪個 “對”,而是能不能完整列出差異: 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 空間:

cpp
// 簡化版 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)
};
text
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 bytes

SSO 的代價

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(訂單簿),支援高精度價格和兩種更新模式。

需求

text
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 的暗示, 這正好是要你挑戰的:

cpp
// Bad: floating point 比較不精確
std::map<double, double> bid;  // ✗

// 0.1 + 0.2 == 0.3 是 false - 這在 order book 裡是致命的
// 兩個 "相同" 價格可能被當成不同 key

Precision 18 意味著價格需要精確到 18 位小數。浮點數只有 ~15-16 位有效數字。 正確做法是轉成整數:

cpp
// 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;

兩種更新模式的實作

cpp
// 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 思維。

總結

題目核心觀念一句話
Q1String 傳遞string_view 最便宜,value+move 最通用
Q2Lambda 捕獲static + capture = dangling reference,stateless 最安全
Q3Class Lambdastatic + [this] 一樣會 dangle,用 parameter 取代 capture
Q4Pointer vs Value列出 copy/null/alias/side-effect/optimizer 五個面向
Q5SSOunion + inline buffer,短字串零 malloc
Q6Order Bookfixed-point key,snapshot vs delta,latency-aware

面試心得

Crypto / HFT 公司的 C++ 面試跟一般 backend 面試非常不同 - 不考 LeetCode, 考的是你對語言底層機制的理解深度。每一題都是 “這幾種寫法誰快、為什麼”, 需要你能即時分析 copy / move / allocation 的成本。 建議準備方向:move semantics、lambda 捕獲機制、memory layout、compiler optimization。

C++PerformanceInterviewHFTMove SemanticsLambdaOrder Book