HOME ABOUT CONTACT

1. 前言top

繼之前寫的「軟韌體工程師面試考題」之後,今年又有參與一些面試,主要是針對 EDA 和 GPU 相關軟體職缺的面試準備。雖然有些重點也在「軟韌體工程師面試考題」做了更新和分享,但仍有一些尚未分享,因此特別再寫一篇做個紀錄;一方面也能依據較新的面試資訊做區分。

2. Smart Pointer 相關top

2.1 概念:RAII

RAII(Resource Acquisition Is Initialization):將資源生命週期與物件或程式區塊的生命週期綁在一起。

資源除了 Memory,還有 File Handle、Socket、Mutex 和 GPU Resource(例如 Buffer、Texture)等。

2.2 定義與宣告寫法

建議使用 std::make_unique,而不是直接使用 new:


// 不建議
std::unique_ptr<int> ptr(new int(5));

// 建議
std::unique_ptr<int> ptr = std::make_unique<int>(5);
                

直接使用 new 會將資源配置與 smart pointer 的建立拆成不同步驟;若中間發生例外,可能使 raw pointer 無法被釋放。make_unique 會在單一操作中建立物件及 unique_ptr,例外安全性較佳。

2.3 STL Container 可儲存 std::unique_ptr


std::vector<std::unique_ptr<int>> values;
values.push_back(std::make_unique<int>(5));

auto value = std::make_unique<int>(10);
values.push_back(std::move(value));
                

2.4 unique_ptr 與 shared_ptr 的 Memory Layout

unique_ptr 通常只保存指向資源的指標;shared_ptr 除了指向資源,還會共享一個控制區塊(control block),其中保存強引用計數、弱引用計數、deleter 等資訊。因此 shared_ptr 有額外記憶體與原子計數成本。

2.5 shared_ptr 如何導致 Memory Leak?如何避免?


class B;
class A {
public:
    std::shared_ptr<B> b;
};

class B {
public:
    std::shared_ptr<A> a;
};
                

當 A 與 B 的成員彼此擁有對方的 shared_ptr 時,會形成循環引用(Circular Reference)。即使外部對 A、B 呼叫 reset,兩個物件仍會互相維持對方的引用計數,因而無法釋放。

解決方式是讓不擁有資源的一側改用 std::weak_ptr,並透過 lock() 確認資源是否仍存在:


std::shared_ptr<Widget> shared = std::make_shared<Widget>(101);
std::weak_ptr<Widget> weak = shared;

if (std::shared_ptr<Widget> locked = weak.lock()) {
    // 資源仍存在,可暫時取得 shared_ptr 使用權
}
                

3. STL Container 相關top

3.1 std::vector

std::vector 是 C++ STL 提供的動態陣列(Dynamic Array)。

典型實作包含 Data Pointer(可由 smart pointer 管理)、Size(當前元素數量)與 Capacity(已配置的記憶體空間)。Vector 與 list 的 Insert、Find 時間複雜度通常只從軟體角度衡量,並未考慮 Cache Locality;實務上 vector 在多數情況下的實際執行時間較少。

Move Semantics:


std::vector<Buffer> buffers;
buffers.push_back(Buffer());

// Buffer 未定義 move constructor 時,會 copy。
// Buffer 定義 move constructor 時,會 move。
                

3.2 std::unordered_map

std::unordered_map 是基於 Hash Table 實作的 Associative Container。

4. 左值與右值top

左值(LValue):代表存在於記憶體中、具有穩定身分的物件。
右值(RValue):暫時存在、通常不具有穩定身分的值。
Reference:分為左值引用(LValue Reference)與右值引用(RValue Reference)。


int a = 10;
int& lvalue_ref = a;  // 正確:a 是具體物件
// int& invalid_ref = 10;  // 錯誤:10 是暫時值

int&& rvalue_ref = 10;  // 正確:T&& 可綁定 temporary object
                

結論:T& 接受左值,T&& 接受右值。

Move Constructor 與 Copy Constructor


class Buffer {
public:
    Buffer(const Buffer& other) {
        data = new int[1000];
        std::memcpy(data, other.data, 1000 * sizeof(int));
    }

    Buffer(Buffer&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

    int* data = nullptr;
};
                

5. Template 相關top

Template 泛型在 Compile Time 決定,而非 Runtime 決定。缺點是編譯時間與生成的 Binary File 大小可能增加。

5.1 Template T&& 是什麼?

此稱為 Forwarding Reference(舊稱 Universal Reference),可在函式定義中接受左值與右值。


template<typename T>
void process(T&& arg) {
    // arg 是 forwarding reference
}

int a = 10;
process(a);   // T 推導為 int&,最終折疊為 int&
process(20);  // T 推導為 int,最終型態為 int&&
                

Reference Collapsing:
& + & → &
& + && → &
&& + & → &
&& + && → &&

Perfect Forwarding:第一層 forwarding reference 函式中的具名參數本身是左值,因此要用 std::forward<T>() 保留原本的左、右值型態並傳遞到下一層。

5.2 Template Specialization

Template 的目的在於讓不同型態共用程式碼,但仍允許對特定型態進行特別處理。


template <typename T>
void print(T value) {
    std::cout << value << std::endl;
}

template <>
void print<const char*>(const char* value) {
    std::cout << std::strlen(value) << std::endl;
}
                

Compiler 會先尋找 Specialization,若沒有才使用 Generic Template。

5.3 Partial Specialization

Partial Specialization 是將 Template Specialization 的概念用於 Class 定義,例如同一個 container 對物件與物件指標提供不同處理方式。

6. 其他修飾詞相關top

6.1 constexpr(C++11)

constexpr 修飾變數或函式,讓值能在編譯期計算完成,提升執行效能並可能降低記憶體使用。若數值由使用者輸入,則仍在 Runtime 計算。

6.2 if constexpr(C++17)

一般 if-else 的兩個分支都必須通過編譯,由 Runtime 決定執行哪一塊;if constexpr 則在編譯期判斷,未選擇的分支不會被實例化。適合版本或型態等可於編譯期確定的單純條件,可避免不必要的分支。

7. Pipeline 相關top

CPU 將一個 Instruction 拆成多個 Stage,每個 Stage 可同時處理不同 Instruction,如同工廠生產線。目標不是降低單一 Instruction 的執行時間,而是提高整體 Instruction 的吞吐量(Throughput)。

7.1 Single Cycle vs. Multiple Cycle

Single Cycle:單一 cycle 負責所有 stage,優點是簡單,缺點是 clock 必須配合最慢指令而較慢。
Multiple Cycle:每個 stage 交由一個 cycle 處理,但後續工作可能需要等待前一階段。
Latency:完成一個 instruction 所需的 cycle。
Throughput:單位時間內可完成的 instruction 數量。

7.2 Pipeline 的五個 Stages

IF(Instruction Fetch)、ID(Instruction Decode / Register Read)、EX(Execute)、MEM(Memory Access)、WB(Write Back)。

7.3 Pipeline Hazard

Instruction 之間的依賴或硬體資源衝突會讓 pipeline 無法正常前進。

簡易處理方式是插入 Stall(等待)或 Bubble(空指令)。

7.4 Forwarding

Forwarding 是解決 Data Hazard 的硬體機制,讓後面的 Instruction 可從 pipeline 中間階段取得前一個 Instruction 的運算結果,而不用等到寫回 Register File。它不能解決所有 Hazard,通常仍要搭配 Stall。

7.5 Control Hazard

Branch 是否成立通常要到後面 stage 才能判斷,但 Fetch stage 已需要下一個 instruction 的位址,因此可能 fetch 到錯誤路徑,必須 stall 或 flush pipeline。常見解法包括 Branch Prediction、Static Branch Prediction 與依據歷史資料的 Dynamic Branch Prediction。

7.6 CPI

CPI(Cycle Per Instruction)為平均執行一個 Instruction 需要多少 Cycle:$CPI = \frac{Total\ Clock\ Cycles}{Number\ of\ Instructions}$。

7.7 Superscalar Architecture

透過增加 pipeline 數量,使一個 cycle 可以執行多個 instruction。是否能同時執行取決於 ILP(Instruction Level Parallelism),也就是找出程式中彼此沒有依賴、可平行執行的 instructions。

7.8 Out-of-Order

In-Order 執行時,只要前面 instruction 卡住,後面即使可執行也需等待;Out-of-Order 讓沒有資料依賴的 instruction 先執行。典型硬體還需要 Register Renaming、Reservation Station 與 Reorder Buffer(ROB),以消除 false dependency 並確保結果仍按照原始程式順序提交。


IF -> ID -> Register Renaming -> ROB + RS -> Issue -> EX -> WB -> Commit
                

8. Memory Hierarchy 相關top

Memory Hierarchy 將不同速度、容量與成本的記憶體分層排列。距離 CPU 越近通常速度越快、容量越小、成本越高;目的是利用程式的 Locality,讓多數存取在高速記憶體中完成。

Register 使用 Flip-Flop 實作,一個 cell 需要多個 transistor;DRAM 的一個 cell 只需要一個 transistor 與一個 capacitor,因此 register 成本及功耗通常更高。

8.1 Locality

Temporal Locality:剛使用的資料很可能很快再被使用,例如 loop 對同一個 variable 的操作。
Spatial Locality:存取某個位址後,很可能再存取附近位址,例如 array access。
Cache Hit / Miss:所需資料是否存在於 Cache 中。

8.2 LRU Cache 程式實作

典型成員為 Hash Table、Capacity、Size 與雙向鏈結串列。核心函式包括 insert_head(新增節點到前端)、update_head(把最近 get/put 的節點移到 head)以及 erase_the_least_used_node(容量已滿時刪除最久未使用節點)。

Cache 的最小存取單位是 Cache Line(Block)。Cache Miss 時從 DRAM 讀取 Cache Line;Tag 驗證資料是否正確,Index 找到 Cache Line,Offset 定位該 line 中的 byte。

8.3 Memory Hierarchy Performance

Average Memory Access Time(AMAT):$AMAT = Hit\ Time + Miss\ Rate \times Miss\ Penalty$。Bandwidth 是單位時間輸出的資料量;Latency 是取得一筆資料的耗時。

8.4 Cache Coherence

多核心中,每個 core 都有自己的 cache。若不同 core 同時 cache 同一個 memory address,且其中一個修改資料,其他 core 的副本可能過期,因此需要快取一致性。

Coherence:同一位址資料的一致性。
Consistency:多個操作的執行順序,例如每個 thread 是否能觀察到其他 thread 更新後的資料。

常見實作有 Snooping Protocol(小型 shared bus)、Directory Based Protocol(大型系統)與 MESI Protocol。

8.5 MESI Protocol

M(Modified)為資料已修改;E(Exclusive)為只有本 cache 擁有;S(Shared)為多個 cache 共享;I(Invalid)為資料已被其他 core 修改而不可用。

False Sharing:兩個 core 雖共享同一個 cache line,但讀寫不同資料,仍互相發送 invalidation。可加入 padding,讓不同 thread 使用不同 cache line,亦即對齊處理。

8.6 Write Back vs. Write Through

Write Back:先更新 cache,稍後才寫回 memory。
Write Through:更新 cache 時同步更新 memory。

9. Multi-thread 相關top

9.1 Atomic

Atomic 是多執行緒中不可分割的最小操作單元,確保操作要嘛完整執行、要嘛不執行,以避免資料錯亂和 Race Condition。

9.2 Mutex vs. Atomic

Atomic:透過 CPU 指令直接操作資料,通常不需要讓 thread 睡眠或等待,速度較快,是 lock-free 設計的基礎之一。
Mutex:是作業系統的阻塞機制,用來保護一整段複雜程式區域。

9.3 Process vs. Thread

每個 process 有各自的 Virtual Memory、Heap、Global Data。同一個 process 中的 threads 共享 Heap、Global Variable、Static Variable,但各自擁有 Stack、Register 與 Program Counter。

9.4 Race Condition

Race Condition 指多執行緒同時存取同一個 Memory Address。Mutex 用來保護共用變數;Condition Variable 讓 thread 睡眠並等待特定條件,以節省 CPU 資源。


#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex;
std::condition_variable condition;
bool ready = false;

void worker_thread() {
    std::unique_lock<std::mutex> lock(mutex);
    condition.wait(lock, [] { return ready; });
    std::cout << "Worker thread is running.\n";
}

int main() {
    std::thread worker(worker_thread);
    {
        std::lock_guard<std::mutex> lock(mutex);
        ready = true;
    }
    condition.notify_one();
    worker.join();
}
                

10. 程式碼考題top

10.1 使用 Bit Manipulation 實作 Swap,且不使用額外記憶體


void swap(int& a, int& b) {
    if (&a == &b) {
        return;
    }
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
}
                

10.2 不用 sizeof() 計算 struct 使用的記憶體空間


struct Node {
    int a;
    char b;
    bool c;
};

int main() {
    Node nodes[2];
    std::printf("Byte distance: %td\n",
        reinterpret_cast<char*>(&nodes[1]) - reinterpret_cast<char*>(&nodes[0]));
}
                

10.3 Array & Pointer


void func(char a[100]) {
    std::cout << sizeof(a) << std::endl;
}

int main() {
    char a[6] = "Hello";
    char b[6] = "Hello";
    char c[] = "Hello";
    char* ptr = a;
    char* ptr2 = b;
    char* ptr3 = reinterpret_cast<char*>(&c + 1) - 2;

    std::cout << (a == b) << std::endl;       // false,位址不同
    std::cout << (ptr == ptr2) << std::endl;  // false,位址不同
    std::cout << sizeof(c) << std::endl;      // 6,包含 '\0'
    std::printf("ptr3 value: %c\n", *ptr3);  // c[4],即 'o'
    std::printf("%c\n", *ptr + 1);           // 'I'
    func(a);                                   // array decay 成 pointer
}
                

Array 傳入函式後會 decay 成 pointer,因此 func 中的 sizeof(a) 是 pointer 的大小;在 64 位元系統通常為 8。

10.4 Class Rule of Five

Rule of Five 包含 Destructor、Copy Constructor、Copy Assignment Operator、Move Constructor 與 Move Assignment Operator。若類別自行管理資源,就需要正確定義或明確刪除這些特殊成員函式。


class Buffer {
public:
    Buffer() = default;
    ~Buffer() { delete[] data; }

    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        std::copy(other.data, other.data + size, data);
    }

    Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            Buffer copy(other);
            std::swap(data, copy.data);
            std::swap(size, copy.size);
        }
        return *this;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }

private:
    int* data = nullptr;
    std::size_t size = 0;
};
                

10.5 Why extern "C"?

C++ 為了支援 Function Overloading,compiler 會對函式名稱進行 Name Mangling,因此編譯後的 symbol 名稱與 C 不同。若要連結由 C 編譯的函式,使用 extern "C" 告訴 C++ compiler 不要做 C++ linkage 的 name mangling,linker 才能找到 C compiler 產生的 symbol。

10.6 巢狀迴圈排序題

若內層迴圈次數極少,例如只有 5 次,對二維矩陣的短小區段排序時,Insertion Sort 通常是較合適的選擇。它對小型資料量常數成本低,也可利用近乎排序完成的資料特性。

Last updated: