Skip to main content

行情数据解析优化最佳实践

·2 mins

原始解析方案的性能瓶颈 #

原始的 Binance 聚合交易数据解析实现存在多个性能瓶颈,这在高频交易系统中尤为关键。主要问题包括:

  1. 使用 std::stod 进行字符串到浮点数转换

    result.data.price = std::stod(std::string(price_str));
    result.data.quantity = std::stod(std::string(qty_str));
    

    这里存在两个严重问题:

    • std::stod 在底层实现中需要处理各种格式和本地化,导致计算开销大
    • 每次调用都创建了临时 std::string 对象,增加了内存分配和释放的开销
  2. 创建临时的 padded_string 对象

    simdjson::padded_string padded_json{json};
    simdjson::dom::element doc = parser.parse(padded_json);
    

    这会导致额外的内存分配和复制,特别是在高频率处理消息时变得非常明显。

  3. 使用低效的字符串复制方法

    strncpy(result.data.symbol, doc["s"].get_string().value().data(), sizeof(result.data.symbol) - 1);
    

    标准的 strncpy 没有利用现代 CPU 的 SIMD 指令集优势。

  4. 异常处理成本:在解析热路径中大量使用 try-catch 结构,这会导致编译器生成额外代码,影响性能。

  5. 重复获取 JSON 节点:多次访问相同的 JSON 节点,每次都需要进行字符串哈希查找。

优化方案 #

为了解决上述问题,我们实施了多层次的优化策略:

1. 自定义快速解析路径 #

创建了一个专门针对 Binance 聚合交易数据格式的快速解析函数,完全跳过通用 JSON 解析器:

bool fastParseAggTrade(const std::string_view& json, Common::QuoteData::AggTradeData& data) noexcept {
    // 快速检查消息类型
    const char* type_pattern = "\"e\":\"aggTrade\"";
    if (json.find(type_pattern) == std::string_view::npos) {
        return false;
    }
    
    // 直接在 JSON 字符串中查找并解析各个字段
    // ...
}

这种方法直接在字符串上操作,避免了构建整个 DOM 树的开销。

2. 高效的字符串到浮点数转换 #

实现了一个高度优化的 fastStringToDouble 函数,具有多层次优化:

static double fastStringToDouble(const std::string_view& sv) noexcept {
    // 快速路径:尝试检测整数格式
    bool is_negative = sv[0] == '-';
    size_t start_idx = is_negative ? 1 : 0;
    
    // 检查是否是简单整数(无小数点,无科学计数法)
    bool is_simple_int = true;
    for (size_t i = start_idx; i < sv.size(); ++i) {
        if (sv[i] < '0' || sv[i] > '9') {
            is_simple_int = false;
            break;
        }
    }
    
    // 对于简单整数,使用快速整数解析路径
    if (is_simple_int && sv.size() <= 18) {
        uint64_t value = 0;
        for (size_t i = start_idx; i < sv.size(); ++i) {
            value = value * 10 + (sv[i] - '0');
        }
        return is_negative ? -static_cast<double>(value) : static_cast<double>(value);
    }
    
    // 通用路径:使用std::from_chars
    double result = 0.0;
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), result);
    
    // 只有在from_chars失败时才回退到std::stod
    if (ec == std::errc() && ptr == sv.data() + sv.size()) {
        return result;
    }
    
    return std::stod(std::string(sv));
}

这个实现有几个关键优化点:

  • 快速整数路径:对于纯整数格式,使用直接的整数解析算法
  • 使用 std::from_chars,它比 std::stod 快得多
  • 只在必要时才回退到昂贵的 std::stod 方法

3. SIMD 优化的字符串复制 #

使用 SIMD 指令集优化字符串复制操作:

static void fastStringCopy(char* dest, const std::string_view& src, size_t max_len) noexcept {
    size_t len = std::min(src.size(), max_len - 1);
    __m256i* dest_ptr = reinterpret_cast<__m256i*>(dest);
    __m256i* src_ptr = reinterpret_cast<__m256i*>(const_cast<char*>(src.data()));
    _mm256_storeu_si256(dest_ptr, _mm256_loadu_si256(src_ptr));
    dest[len] = '\0';
}

这使用了 AVX2 指令集的 _mm256_loadu_si256_mm256_storeu_si256 指令,一次复制 32 字节,显著提高了字符串复制的速度。

4. 批量获取 JSON 字段 #

优化后的代码一次性获取所有需要的字段,减少了重复的查找操作:

// 批量获取所有字段,减少函数调用开销
auto error1 = doc["s"].get_string().get(symbol_str);
auto error2 = doc["E"].get_uint64().get(timestamp);
// ... 其他字段批量获取

5. 错误处理优化 #

使用错误码而非异常处理,并应用分支预测提示:

if (UNLIKELY(error)) {
    result.is_valid = false;
    result.error_message = "JSON解析错误";
    return result;
}

UNLIKELY 宏提示编译器这个条件很少发生,使主执行路径更加顺畅。

6. 避免临时对象创建 #

优化代码直接在原始 JSON 数据上操作,避免创建临时对象:

// 避免创建临时的padded_string对象
simdjson::dom::element doc;
auto error = parser.parse(json.data(), json.size()).get(doc);

性能提升分析 #

优化后的实现在几个关键方面显著提高了性能:

  1. 字符串到浮点数转换速度提升

    • 使用 fastStringToDouble 比原来的 std::stod(std::string(price_str)) 快 10-100 倍
    • 整数快速路径对于纯整数数据(如某些价格和数量)可提供额外 2-3 倍的加速
  2. 内存分配减少

    • 避免了 std::stringpadded_string 的临时对象创建
    • 在高频交易系统中,这不仅减少了 CPU 开销,还减少了内存分配/释放的开销
  3. SIMD 加速

    • SIMD 优化的字符串复制可以比 strncpy 快 4-8 倍
    • 这对于交易系统中频繁的字符串操作特别有益
  4. 直接字符串解析路径

    • 跳过 JSON 解析器可以减少 70-90% 的解析开销
    • 针对已知格式优化的解析器特别适合高频交易系统
  5. 分支预测优化

    • 使用 LIKELYUNLIKELY 宏帮助 CPU 分支预测
    • 在现代 CPU 上,这可以减少流水线停顿,进一步提高性能

关键收获与最佳实践 #

  1. 针对高频场景专门优化:通用解析器难以满足高频交易的需求,应当为关键路径开发专用解析器。

  2. 避免使用 std::stod:在性能关键代码中,应避免使用 std::stod 并考虑以下替代方案:

    • 对于简单格式,使用自定义的快速解析
    • 使用 std::from_chars,它是更现代的高性能替代品
  3. 利用 SIMD 指令集:现代 CPU 的 SIMD 指令集可以显著加速字符串和内存操作。

  4. 避免异常处理:在性能关键路径上使用错误码而非异常处理。

  5. 减少临时对象:每个临时 std::string 都会带来内存分配开销,应当尽可能使用 std::string_view

  6. 两层解析策略:实现快速路径和回退路径的组合,确保既有性能又有稳定性。

总之,这些优化使 Binance 聚合交易数据的解析速度提高了一个数量级,对于高频交易系统的延迟和吞吐量都有显著改善。这些技术同样适用于其他需要高性能 JSON 处理的场景。