Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

低延遲 C++ 應用程式開發指南

簡介

延遲(Latency)是指從流程啟動到完成之間的延遲時間。在高頻交易和即時系統中,最小化這種延遲對於維持競爭優勢和系統可靠性至關重要。本文提供一些實用策略來優化您的 C++ 應用程式以達到低延遲,涵蓋高效資料處理、並發管理和系統層級優化等關鍵領域。

高效資料處理

高效的資料處理是降低 C++ 應用程式延遲的基礎。高效處理資料意味著最小化記憶體存取、複製和資料結構操作所花費的時間。以下是一些深入的技術考量:

記憶體管理

適當的記憶體管理可以顯著影響應用程式的效能。以下是一些策略:

避免動態記憶體配置

動態記憶體配置(使用 newdelete)會因為管理堆積(heap)的開銷而引入延遲。相反,優先使用堆疊配置或使用記憶體池。堆疊配置要快得多,因為它只涉及簡單的指標運算。記憶體池預先配置一大塊記憶體並管理它,減少頻繁配置和釋放的開銷。

void process() {
    int buffer[1024];
    // 使用堆疊記憶體
}

class MemoryPool {
public:
    MemoryPool(size_t size) : poolSize(size), pool(new char[size]), offset(0) {}
    ~MemoryPool() { delete[] pool; }

    void* allocate(size_t size) {
        if (offset + size > poolSize) throw std::bad_alloc();
        void* ptr = pool + offset;
        offset += size;
        return ptr;
    }

    void deallocate() {
        // 重置整個池
        offset = 0;
    }

private:
    size_t poolSize;
    char* pool;
    size_t offset;
};

MemoryPool pool(1024 * 1024); // 1MB 記憶體池

使用連續記憶體

存取連續記憶體(例如陣列或向量)由於快取區域性(cache locality)而更快。快取區域性確保存取一個資料片段會將其他附近的資料帶入快取,減少快取未命中的次數。

std::vector<int> data(1000);
for (int i = 0; i < 1000; ++i) {
    data[i] = i;
}

記憶體對齊

確保您的資料結構正確對齊到 CPU 的快取行。未對齊的資料可能導致額外的記憶體存取週期,稱為快取行分裂(cache line splits)。使用 alignas 說明符來確保對齊。

struct alignas(64) AlignedData {
    int data[16];
};

資料結構

選擇正確的資料結構可以大大影響效能:

固定大小容器

優先使用固定大小容器(例如 std::array)而不是動態容器(例如 std::vector)。固定大小容器避免了動態調整大小的開銷,並提供更好的快取效能。

std::array<int, 1000> fixedData;

快取友善結構

設計您的資料結構為快取友善。例如,使用結構陣列(SoA, Structure of Arrays)而不是陣列結構(AoS, Array of Structures)來改善快取利用率。這種佈局確保存取多個物件的欄位會導致更少的快取未命中。

// AoS - 陣列結構
struct Particle {
    float x, y, z;
    float vx, vy, vz;
};
std::vector<Particle> particles;

// SoA - 結構陣列(更快取友善)
struct Particles {
    std::vector<float> x, y, z;
    std::vector<float> vx, vy, vz;
};
Particles particlesSoA;

最小化複製

減少資料複製的次數以改善效能:

傳遞參考

透過參考傳遞大型物件而不是傳值,以避免不必要的複製。這對於處理大型資料結構的函式特別重要。

void processLargeObject(const LargeObject& obj) {
    // 處理物件
}

移動語意

使用移動語意(C++11 引入)在不複製的情況下轉移資源的所有權。移動語意允許您將物件的狀態轉移到另一個物件,而無需深度複製的開銷。

#include <vector>
#include <algorithm>

class Data {
public:
    Data(int size) : data_(size) {}
    Data(Data&& other) noexcept : data_(std::move(other.data_)) {}
    Data& operator=(Data&& other) noexcept {
        if (this != &other) {
            data_ = std::move(other.data_);
        }
        return *this;
    }

private:
    std::vector<int> data_;
};

複製省略

現代 C++ 編譯器經常執行複製省略(copy elision),這消除了物件的不必要複製,特別是在 return 語句和初始化物件時。確保您的程式碼結構允許複製省略可以改善效能。

透過實施這些資料處理技術,您可以降低延遲並改善 C++ 應用程式的效能,使其更適合高頻交易系統和即時應用程式。

並發管理

高效的並發管理對於低延遲應用程式至關重要,特別是在多執行緒環境中。有效管理並發涉及最小化同步開銷、避免競爭,並優化執行緒使用。以下是一些技巧和技術:

無鎖程式設計

鎖可能由於上下文切換和競爭而引入顯著的延遲。考慮使用無鎖程式設計技術來緩解這些問題:

原子操作

使用原子操作進行簡單的共享資料更新。std::atomic 函式庫提供原子型別和操作,可用於執行無鎖更新。

#include <atomic>

std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1, std::memory_order_relaxed);
}

無鎖資料結構

無鎖資料結構透過使用原子操作來避免鎖定機制的開銷,使並發執行緒能夠在沒有傳統鎖的情況下互動。例如無鎖佇列、堆疊和其他資料結構,在需要高並發性的環境中可以更有效率。

#include <atomic>
#include <memory>

template<typename T>
class LockFreeQueue {
private:
    struct Node {
        T data;
        std::shared_ptr<Node> next;
        Node(T value) : data(value) {}
    };

    std::atomic<std::shared_ptr<Node>> head;
    std::atomic<std::shared_ptr<Node>> tail;
    std::atomic<int> size;

public:
    LockFreeQueue() {
        auto dummyNode = std::make_shared<Node>(T());
        head.store(dummyNode, std::memory_order_relaxed);
        tail.store(dummyNode, std::memory_order_relaxed);
        size.store(0, std::memory_order_relaxed);
    }

    void enqueue(T value) {
        auto newNode = std::make_shared<Node>(value);
        auto oldTail = tail.load(std::memory_order_acquire);

        while (!tail.compare_exchange_weak(
            oldTail, newNode, std::memory_order_release, std::memory_order_acquire)) {
        }

        oldTail->next = newNode;
        size.fetch_add(1, std::memory_order_relaxed);
    }

    bool dequeue(T& result) {
        auto oldHead = head.load(std::memory_order_acquire);
        auto nextNode = oldHead->next;

        if (!nextNode) {  // 佇列為空
            return false;
        }

        result = nextNode->data;

        if (head.compare_exchange_strong(oldHead, nextNode, std::memory_order_release)) {
            size.fetch_sub(1, std::memory_order_relaxed);
            return true;
        }

        return false;
    }
};

執行緒親和性

將執行緒綁定到特定的 CPU 核心(執行緒親和性)可以減少由上下文切換引起的延遲並改善快取效能:

設定執行緒親和性

使用平台特定的 API 設定執行緒親和性。在 Linux 上,您可以使用 pthread_setaffinity_np()

#include <pthread.h>
#include <iostream>

void setThreadAffinity(int core_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(core_id, &cpuset);

    pthread_t current_thread = pthread_self();
    int result = pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset);
    if (result != 0) {
        std::cerr << "設定執行緒親和性錯誤: " << result << std::endl;
    }
}

即時排程

使用即時排程策略來優先處理關鍵執行緒,確保它們獲得所需的 CPU 時間而不會延遲:

即時策略

在 Linux 上,使用 sched_setscheduler() 設定即時排程策略,如 SCHED_FIFOSCHED_RR

#include <sched.h>
#include <iostream>

void setRealTimeScheduling() {
    struct sched_param param;
    param.sched_priority = 99; // 最高優先權

    int result = sched_setscheduler(0, SCHED_FIFO, &param);
    if (result != 0) {
        std::cerr << "設定即時排程錯誤: " << result << std::endl;
    }
}

優先處理關鍵執行緒

將更高的優先權分配給處理關鍵任務的執行緒,確保它們不會被較不重要的執行緒搶佔。

void prioritizeThread(pthread_t thread, int priority) {
    struct sched_param param;
    param.sched_priority = priority;

    int result = pthread_setschedparam(thread, SCHED_FIFO, &param);
    if (result != 0) {
        std::cerr << "設定執行緒優先權錯誤: " << result << std::endl;
    }
}

執行緒池

使用執行緒池可以幫助管理執行緒建立和銷毀的開銷,提高效率並降低延遲:

執行緒池實作

實作執行緒池來管理固定數量的執行緒,可以處理多個任務。

#include <vector>
#include <thread>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>

class ThreadPool {
public:
    ThreadPool(size_t numThreads);
    ~ThreadPool();
    void enqueueTask(std::function<void()> task);

private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;

    void workerThread();
};

ThreadPool::ThreadPool(size_t numThreads) : stop(false) {
    for (size_t i = 0; i < numThreads; ++i) {
        workers.emplace_back(&ThreadPool::workerThread, this);
    }
}

ThreadPool::~ThreadPool() {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        stop = true;
    }
    condition.notify_all();
    for (std::thread& worker : workers) {
        worker.join();
    }
}

void ThreadPool::enqueueTask(std::function<void()> task) {
    {
        std::unique_lock<std::mutex> lock(queueMutex);
        tasks.push(std::move(task));
    }
    condition.notify_one();
}

void ThreadPool::workerThread() {
    while (true) {
        std::function<void()> task;
        {
            std::unique_lock<std::mutex> lock(queueMutex);
            condition.wait(lock, [this] { return stop || !tasks.empty(); });
            if (stop && tasks.empty()) {
                return;
            }
            task = std::move(tasks.front());
            tasks.pop();
        }
        task();
    }
}

使用執行緒池透過重用固定數量的執行緒來處理多個任務,有效地管理工作負載。程式碼範例中的 ThreadPool 類別初始化一個工作執行緒池,不斷等待任務。任務被排入受互斥鎖保護的共享佇列中,以確保執行緒安全。當任務被加入佇列時,其中一個等待的工作執行緒會被通知並提取任務執行。這種方法避免了為每個任務建立和銷毀執行緒的開銷,從而實現更有效的並發管理和降低延遲。workerThread 函式確保每個執行緒處理佇列中的任務,直到池被停止,允許動態和靈活的任務管理。

透過採用這些並發管理技術,您可以最小化延遲並改善 C++ 應用程式的效能。無鎖程式設計、設定執行緒親和性、即時排程和使用執行緒池是在多執行緒環境中實現超低延遲的基本策略。

系統層級優化

系統層級優化可以進一步降低延遲並改善 C++ 應用程式的效能。這些優化涉及調整底層作業系統、網路堆疊和硬體以支援超低延遲需求。以下是一些關鍵技術:

網路優化

對於網路繫結的應用程式,優化網路堆疊設定可以顯著降低延遲:

TCP_NODELAY

停用 Nagle 演算法以立即發送小封包而不延遲。這在低延遲至關重要的場景中特別重要,例如即時通訊或交易系統。

#include <sys/types.h>
#include <sys/socket.h>

void disableNagle(int sockfd) {
    int flag = 1;
    setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
}

減少緩衝區大小

調整 socket 緩衝區大小以符合您的應用程式需求。較小的緩衝區可以減少緩衝區管理所涉及的延遲,確保資料處理更快。

#include <sys/types.h>
#include <sys/socket.h>

void setSocketBufferSize(int sockfd, int size) {
    setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
    setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
}

優化網路堆疊

調整網路堆疊的各種參數,例如 TCP 視窗大小和網路介面卡(NIC)上的中斷聚合設定,可以減少延遲。請參閱您的作業系統和 NIC 文件以了解特定的調整選項。

核心繞過

對於超低延遲應用程式,考慮繞過核心以避免核心空間網路處理的開銷:

DPDK (Data Plane Development Kit)

DPDK 允許從使用者空間直接存取網路介面卡(NIC),繞過核心。這減少了與核心空間處理和上下文切換相關的延遲。

// DPDK 初始化範例
#include <rte_eal.h>
#include <rte_ethdev.h>

int main(int argc, char *argv[]) {
    rte_eal_init(argc, argv);

    // 初始化 DPDK 環境
    // 設定網路介面卡
}

RDMA (Remote Direct Memory Access)

RDMA 允許從一台電腦的記憶體直接存取另一台電腦的記憶體,而無需涉及作業系統。這在高效能運算和低延遲網路應用程式中很有用。

// RDMA 設定範例
#include <rdma/rdma_cma.h>

void setupRDMA() {
    struct rdma_event_channel *ec = rdma_create_event_channel();

    // 設定 RDMA 連接
    // 配置緩衝區和資源
}

硬體優化

利用硬體特性進一步降低延遲:

CPU 頻率調整

停用動態 CPU 頻率調整(例如 Intel SpeedStep)以防止頻率變化引起的延遲峰值。將 CPU 頻率鎖定在其最大效能級別確保一致和可預測的處理時間。

# 設定 CPU 為效能模式
cpupower frequency-set -g performance

NUMA (Non-Uniform Memory Access) 感知

透過在將要存取記憶體的 CPU 本地配置記憶體來優化 NUMA。這減少了跨節點記憶體存取延遲。使用平台特定的 API 來控制記憶體配置。

#include <numa.h>

void allocateNUMAMemory() {
    // 在特定 NUMA 節點上配置記憶體
    void* ptr = numa_alloc_onnode(size, node);
}

使用大頁面

大頁面透過使用更大的頁面大小來減少記憶體管理的開銷。這可以減少 TLB(轉譯後備緩衝區)未命中並改善記憶體存取效能。

# 設定大頁面
echo 1024 > /proc/sys/vm/nr_hugepages

快取管理

在存取資料之前將資料預取到快取中可以降低延遲。使用編譯器特定的內建函式或組合語言指令來預取資料。

#include <xmmintrin.h>

void prefetchData(const void* ptr) {
    _mm_prefetch(static_cast<const char*>(ptr), _MM_HINT_T0);
}

即時作業系統(RTOS)調整

如果您的應用程式在即時作業系統(RTOS)上執行,額外的調整可以確保最小延遲:

優先權反轉處理

實作機制來處理優先權反轉,即較低優先權的任務持有較高優先權任務所需的資源。使用 RTOS 提供的優先權繼承協定。

確定性排程

確保 RTOS 提供確定性排程保證。這確保高優先權任務以可預測和一致的方式排程。

透過實施這些系統層級優化,您可以顯著降低 C++ 應用程式的延遲,使其非常適合高頻交易、即時系統和其他效能關鍵環境。

結論

撰寫低延遲 C++ 應用程式需要一個全面的方法,包括高效的資料處理、有效的並發管理和徹底的系統層級優化。透過應用這些策略,您可以顯著降低延遲並改善應用程式的效能,使其適合高頻交易系統和即時環境。透過專注於最小化開銷和最大化效率,您可以確保您的應用程式滿足超低延遲需求的嚴格要求。

參考資源

  1. C++ Reference
  2. Data Plane Development Kit (DPDK)
  3. Remote Direct Memory Access (RDMA)
  4. Thread Building Blocks (TBB)
  5. Real-Time Operating Systems (RTOS)
  6. Google Benchmark

原文作者: Alexander Obregon 發表日期: 2024年6月28日 翻譯說明: 本文翻譯自 Medium 文章 "Writing Low-Latency C++ Application"