Skip to main content

高频交易系统中的市场数据存储优化

·2 mins

1. 背景介绍 #

在高频交易系统中,市场数据的快速读取和处理是关键性能指标之一。我们的系统使用共享内存来存储和访问实时市场数据,其中 MarketDataStore 类负责管理这些数据。本文将讨论如何优化 MarketDataStore 中的 readLatestData 函数,以提高数据读取的效率。

2. 初始实现 #

最初的 readLatestData 函数实现如下:

std::optional<MappedTickerData> MarketDataStore::readLatestData(const std::string& symbol) const {
    std::shared_lock<std::shared_mutex> lock(mutex);
    
    size_t offset = calculateOffset(symbol);
    MappedTickerData data;
    
    if (dataFile->read(&data, offset, sizeof(MappedTickerData))) {
        if (data.timestamp != 0 && std::string(data.product_id) == symbol) {
            return data;
        } else {
            LOG_WARN("readLatestData symbol = {} failed", symbol);
            return std::nullopt;
        }
    } else {
        LOG_ERROR("Failed to read data for symbol = {}", symbol);
        return std::nullopt;
    }
}

这个实现存在几个性能瓶颈:

  1. 共享锁虽然允许并发读取,但在极高频率调用下其原子操作的开销仍不可忽视。
  2. 字符串比较效率低下,特别是创建临时 std::string 对象。
  3. 没有利用现代 CPU 的 SIMD 指令集。

3. 优化过程 #

3.1 字符串比较优化 #

首先,我们优化了字符串比较逻辑:

static inline bool compareProductId(const char* product_id, const std::string& symbol) {
    size_t symbolLength = symbol.length();
    if (symbolLength > sizeof(MappedTickerData::product_id) - 1) {
        return false;
    }
    
    if (memcmp(product_id, symbol.data(), symbolLength) != 0) {
        return false;
    }
    
    return product_id[symbolLength] == '\0';
}

这个优化避免了创建临时字符串对象,并使用了更高效的 memcmp 函数。

3.2 SIMD 指令优化 #

为了进一步提高性能,我们引入了 SIMD 指令来并行化字符串比较:

static inline bool compareProductIdSIMD(const char* product_id, const std::string& symbol) {
    size_t symbolLength = symbol.length();
    if (symbolLength > 15) {
        return false;
    }

    __m128i prod_id = _mm_loadu_si128(reinterpret_cast<const __m128i*>(product_id));
    
    char mask[16] = {0};
    memcpy(mask, symbol.data(), symbolLength);
    __m128i symbol_mask = _mm_loadu_si128(reinterpret_cast<const __m128i*>(mask));

    __m128i cmp_result = _mm_cmpeq_epi8(prod_id, symbol_mask);
    int match_mask = _mm_movemask_epi8(cmp_result);

    int should_match = (1 << symbolLength) - 1;

    if ((match_mask & should_match) != should_match) {
        return false;
    }

    return (match_mask & (1 << symbolLength)) != 0;
}

这个实现利用 SSE 指令集同时比较 16 个字节,显著提高了比较速度。

3.3 无锁读取 #

考虑到 readLatestData 函数被频繁调用,我们探讨了使用无锁读取技术:

std::optional<MappedTickerData> MarketDataStore::readLatestData(const std::string& symbol) const {
    size_t offset = calculateOffset(symbol);
    MappedTickerData data;
    
    std::atomic_thread_fence(std::memory_order_acquire);
    memcpy(&data, static_cast<char*>(mappedMemory) + offset, sizeof(MappedTickerData));
    std::atomic_thread_fence(std::memory_order_acquire);

    if (data.timestamp != 0 && compareProductIdSIMD(data.product_id, symbol)) {
        return data;
    }
    return std::nullopt;
}

这个版本移除了共享锁,使用内存屏障确保数据一致性。

4. 最终优化版本 #

综合以上优化,我们的最终版本如下:

class MarketDataStore {
private:
    void* mappedMemory;
    size_t memorySize;
    std::unordered_map<std::string_view, size_t> symbolOffsets;

    static inline bool compareProductIdSIMD(const char* product_id, const std::string& symbol) {
        // SIMD 比较实现(如前所示)
    }

public:
    inline std::optional<MappedTickerData> readLatestData(std::string_view symbol) const noexcept {
        auto it = symbolOffsets.find(symbol);
        if (it == symbolOffsets.end()) {
            return std::nullopt;
        }

        size_t offset = it->second;
        if (offset + sizeof(MappedTickerData) > memorySize) {
            return std::nullopt;
        }

        MappedTickerData data;
        
        std::atomic_thread_fence(std::memory_order_acquire);
        memcpy(&data, static_cast<char*>(mappedMemory) + offset, sizeof(MappedTickerData));
        std::atomic_thread_fence(std::memory_order_acquire);

        if (data.timestamp == 0) {
            return std::nullopt;
        }

        if (compareProductIdSIMD(data.product_id, std::string(symbol))) {
            return data;
        }

        return std::nullopt;
    }
};

5. 性能考虑和注意事项 #

  1. SIMD 指令:确保目标平台支持使用的 SIMD 指令集。
  2. 内存对齐:考虑将 MappedTickerData 结构体对齐到缓存线边界。
  3. 预计算偏移量:使用 symbolOffsets 哈希表预存储偏移量,避免重复计算。
  4. 无锁读取:在多线程环境中需要仔细考虑内存一致性问题。
  5. 字符串视图:使用 std::string_view 减少不必要的字符串拷贝。

6. 结论 #

通过这一系列优化,我们显著提高了 MarketDataStore 的读取性能。主要改进包括:

  • 使用 SIMD 指令加速字符串比较
  • 实现无锁读取减少线程竞争
  • 优化内存访问模式提高缓存效率

这些优化对于高频交易系统的整体性能有重要影响。然而,在实际部署前,务必进行全面的基准测试和压力测试,以确保在实际工作负载下的性能提升。

7. 未来工作 #

  1. 探索使用更高级的 SIMD 指令集(如 AVX-512)进一步优化。
  2. 实现自适应策略,根据数据特征动态选择最佳的比较方法。
  3. 考虑引入预取技术,进一步减少内存访问延迟。
  4. 持续监控和分析系统性能,识别新的优化机会。