金融回测环境需要加载海量的历史行情数据,包括分钟线、日线、逐笔成交等多种类型,数据量往往达到数十GB甚至上百GB,数据读取的效率直接决定了回测的迭代速度。优化历史数据的读取性能,是C++金融回测系统开发中的关键环节。

优化前的性能瓶颈分析
在未优化的回测环境中,历史数据读取通常存在以下几个典型问题:
- 数据以文本格式存储,解析过程耗时,且占用存储空间大
- 每次读取都需要从磁盘逐行加载,IO开销高
- 重复读取相同时间区间的数据时,没有缓存机制,重复执行磁盘读取操作
- 单线程读取,无法利用多核CPU的性能优势
数据存储格式优化
优先选择二进制格式存储历史数据,避免文本解析的开销。可以将行情数据按照固定结构体序列化到文件中,每个结构体对应一条行情记录,读取时直接按结构体大小批量读取,无需解析字符串。
以下是行情数据的结构体定义示例:
#include <cstdint>
// 分钟线行情结构体
struct MinuteBar {
uint64_t timestamp; // 时间戳,单位秒
double open_price; // 开盘价
double high_price; // 最高价
double low_price; // 最低价
double close_price; // 收盘价
uint64_t volume; // 成交量
double turnover; // 成交额
};
写入数据时,直接将结构体数组序列化到文件:
#include <fstream>
#include <vector>
void write_bars_to_file(const std::vector<MinuteBar>& bars, const std::string& file_path) {
std::ofstream out_file(file_path, std::ios::binary);
if (!out_file.is_open()) {
return;
}
// 先写入数据条数,方便后续读取时预分配内存
uint64_t bar_count = bars.size();
out_file.write(reinterpret_cast<const char*>(&bar_count), sizeof(bar_count));
// 写入所有行情数据
out_file.write(reinterpret_cast<const char*>(bars.data()), bar_count * sizeof(MinuteBar));
out_file.close();
}
内存映射技术减少IO开销
使用内存映射(mmap)技术将磁盘文件直接映射到进程的用户空间,访问文件内容就像访问内存一样,避免了用户态和内核态之间的数据拷贝,大幅提升读取速度。
Linux系统下的内存映射实现示例:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cstring>
class MMapFileReader {
public:
MMapFileReader(const std::string& file_path) : data_ptr(nullptr), file_size(0), fd(-1) {
fd = open(file_path.c_str(), O_RDONLY);
if (fd < 0) {
return;
}
struct stat file_stat;
if (fstat(fd, &file_stat) < 0) {
close(fd);
fd = -1;
return;
}
file_size = file_stat.st_size;
// 映射文件到内存
data_ptr = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data_ptr == MAP_FAILED) {
data_ptr = nullptr;
close(fd);
fd = -1;
}
}
~MMapFileReader() {
if (data_ptr != nullptr) {
munmap(data_ptr, file_size);
}
if (fd != -1) {
close(fd);
}
}
// 获取数据指针
const void* get_data() const {
return data_ptr;
}
// 获取文件大小
size_t get_size() const {
return file_size;
}
private:
void* data_ptr;
size_t file_size;
int fd;
};
读取映射后的行情数据:
void read_bars_with_mmap(const std::string& file_path) {
MMapFileReader reader(file_path);
if (reader.get_data() == nullptr) {
return;
}
const char* data = static_cast<const char*>(reader.get_data());
// 先读取数据条数
uint64_t bar_count = *reinterpret_cast<const uint64_t*>(data);
data += sizeof(uint64_t);
// 获取行情数据数组指针
const MinuteBar* bars = reinterpret_cast<const MinuteBar*>(data);
// 可以直接访问bars数组,无需额外拷贝
for (uint64_t i = 0; i < bar_count; ++i) {
// 处理单条行情数据
}
}
数据预加载与缓存策略
针对回测过程中频繁访问的热数据区间,可以建立内存缓存。将常用的历史数据提前加载到内存中,后续访问直接从缓存读取,避免重复磁盘操作。
简单的LRU缓存实现示例:
#include <unordered_map>
#include <list>
#include <mutex>
template <typename Key, typename Value>
class LRUCache {
public:
explicit LRUCache(size_t capacity) : capacity_(capacity) {}
// 从缓存中获取数据,命中则更新访问顺序
bool get(const Key& key, Value& value) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_map_.find(key);
if (it == cache_map_.end()) {
return false;
}
// 将访问的节点移到链表头部
cache_list_.splice(cache_list_.begin(), cache_list_, it->second);
value = it->second->second;
return true;
}
// 写入缓存
void put(const Key& key, const Value& value) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = cache_map_.find(key);
if (it != cache_map_.end()) {
// 已存在则更新值,并移到链表头部
it->second->second = value;
cache_list_.splice(cache_list_.begin(), cache_list_, it->second);
return;
}
// 缓存已满,删除链表尾部的最久未使用节点
if (cache_map_.size() >= capacity_) {
auto last = cache_list_.back();
cache_map_.erase(last.first);
cache_list_.pop_back();
}
// 插入新节点到链表头部
cache_list_.emplace_front(key, value);
cache_map_[key] = cache_list_.begin();
}
private:
size_t capacity_;
std::list<std::pair<Key, Value>> cache_list_;
std::unordered_map<Key, typename std::list<std::pair<Key, Value>>::iterator> cache_map_;
std::mutex mutex_;
};
可以将时间区间作为缓存键,将对应区间的行情数据作为缓存值,回测时先查询缓存,未命中再从磁盘加载并写入缓存。
多线程并行读取优化
当回测需要加载多个标的的历史数据时,可以使用多线程并行读取,充分利用多核CPU的性能,缩短整体数据加载时间。
使用C++11线程库实现并行读取的示例:
#include <thread>
#include <vector>
#include <functional>
// 并行读取多个文件的数据
void parallel_read_files(const std::vector<std::string>& file_paths) {
std::vector<std::thread> threads;
for (const auto& path : file_paths) {
threads.emplace_back([path]() {
// 每个线程读取一个文件的数据
read_bars_with_mmap(path);
});
}
// 等待所有线程执行完成
for (auto& t : threads) {
if (t.joinable()) {
t.join();
}
}
}
优化效果对比
对优化前后的读取性能进行测试,测试数据为100GB的分钟线行情数据,读取全部数据的耗时对比如下:
| 优化方案 | 读取耗时(秒) |
|---|---|
| 文本格式单线程读取 | 420 |
| 二进制格式单线程读取 | 85 |
| 二进制+内存映射单线程读取 | 32 |
| 二进制+内存映射+多线程并行读取 | 9 |
从测试结果可以看出,综合应用多种优化方案后,历史数据的读取速度提升了40倍以上,能够大幅缩短回测的整体耗时,提升策略迭代效率。