-
Notifications
You must be signed in to change notification settings - Fork 724
性能调优与最佳实践
Hikyuu量化交易系统通过工作窃取机制实现高性能并行计算,特别适用于回测引擎等计算密集型场景。本指南深入分析其线程池实现机制,提供基于工作窃取的性能优化策略,帮助开发者合理配置线程池参数、优化任务粒度、监控系统状态,并通过实际案例展示优化效果。
Hikyuu系统中的线程池设计充分考虑了硬件资源的最优利用。GlobalStealThreadPool类提供了两种构造方式:默认构造函数会自动根据系统CPU核心数创建相应数量的线程,而自定义构造函数允许开发者指定线程数量。
classDiagram
class GlobalStealThreadPool {
-atomic_bool m_done
-size_t m_worker_num
-bool m_running_until_empty
-condition_variable m_cv
-mutex m_cv_mutex
-vector<InterruptFlag*> m_interrupt_flags
-ThreadSafeQueue<task_type> m_master_work_queue
-vector<unique_ptr<WorkStealQueue>> m_queues
-vector<thread> m_threads
+GlobalStealThreadPool()
+GlobalStealThreadPool(size_t n, bool until_empty = true)
+worker_num() size_t
+remain_task_count() size_t
+submit(FunctionType f) auto
+stop() void
+join() void
}
class WorkStealQueue {
-deque<data_type> m_queue
-mutable mutex m_mutex
+push_front(data_type&& data) void
+push_back(data_type&& data) void
+empty() bool
+size() size_t
+clear() void
+try_pop(data_type& res) bool
+try_steal(data_type& res) bool
}
class FuncWrapper {
-unique_ptr<impl_base> impl
+FuncWrapper()
+FuncWrapper(F&& f)
+operator()() void
+FuncWrapper(FuncWrapper&& other)
+operator=(FuncWrapper&& other) FuncWrapper&
+isNullTask() bool
}
GlobalStealThreadPool --> WorkStealQueue : "包含"
GlobalStealThreadPool --> FuncWrapper : "使用"
图源
- GlobalStealThreadPool.h
- WorkStealQueue.h
- FuncWrapper.h
建议配置策略:
- 默认配置:使用默认构造函数,让系统自动匹配CPU核心数,这是最安全且高效的配置方式。
- I/O密集型任务:如果任务包含大量I/O操作,可以适当增加线程数(如CPU核心数的1.5-2倍),以充分利用等待时间。
- CPU密集型任务:对于纯计算任务,线程数应等于或略小于CPU核心数,避免过多的上下文切换开销。
- 混合型任务:根据任务中CPU计算和I/O等待的比例动态调整线程数。
线程池状态监控方法:
-
worker_num():获取当前工作线程数量 -
remain_task_count():获取剩余任务总数,包括主队列和各工作线程队列中的任务 -
done():检查线程池是否已结束运行
节源
- GlobalStealThreadPool.h
- GlobalStealThreadPool.h
任务粒度是影响并行计算性能的关键因素。过细的任务会导致频繁的任务调度和上下文切换开销,而过粗的任务则可能导致负载不均衡。
Hikyuu系统通过parallelIndexRange函数智能地将大任务分解为适当大小的子任务:
flowchart TD
Start([开始]) --> CalculateCPU["计算CPU核心数"]
CalculateCPU --> DetermineRange["确定任务范围"]
DetermineRange --> CheckSize{"任务范围 < 1000?"}
CheckSize --> |是| DirectSplit["直接分割为CPU核心数个子任务"]
CheckSize --> |否| OptimizeSplit["优化分割:前CPU核心数-1个任务均分,剩余任务按1000大小分割"]
DirectSplit --> AdjustRemainder["调整余数分配"]
OptimizeSplit --> AdjustRemainder
AdjustRemainder --> DistributeTasks["分配任务到各线程"]
DistributeTasks --> ExecuteTasks["执行并行任务"]
ExecuteTasks --> End([结束])
图源
- algorithm.h
任务分解策略:
- 小任务(<1000):直接平均分割为与CPU核心数相等的子任务块。
-
大任务(≥1000):采用混合策略,前
CPU核心数-1个任务尽可能平均分配,剩余任务以1000为单位进行分割,确保每个子任务有足够的计算量。 - 递归任务:对于存在递归调用的任务,采用工作窃取机制,优先在本地线程队列中执行,减少跨线程通信开销。
优化建议:
- 避免过细分割:确保每个子任务的执行时间远大于任务调度开销(通常建议至少1ms以上)。
-
负载均衡:通过
parallel_for_index函数自动实现负载均衡,避免某些线程空闲而其他线程过载。 - 内存局部性:尽量让相关数据的操作集中在同一个子任务中,提高缓存命中率。
节源
- algorithm.h
- PerformanceOptimalSelector.cpp
有效的监控是性能调优的基础。Hikyuu系统提供了多种工具来监控线程池状态和任务执行情况。
classDiagram
class ThreadPoolMonitor {
+worker_num() size_t
+remain_task_count() size_t
+done() bool
+stop() void
+join() void
}
class PerformanceMetrics {
+execution_time double
+task_count size_t
+cpu_usage double
+memory_usage double
}
class TimerManager {
+size() size_t
+empty() bool
+stopped() bool
+stop() void
}
ThreadPoolMonitor --> PerformanceMetrics : "生成"
TimerManager --> PerformanceMetrics : "生成"
图源
- ThreadPool.h
- TimerManager.h
Hikyuu提供了强大的SpendTimer工具用于精确测量代码执行时间:
sequenceDiagram
participant Developer as "开发者"
participant CodeBlock as "代码块"
participant SpendTimer as "SpendTimer"
participant Output as "输出"
Developer->>CodeBlock : 使用SPEND_TIME宏
CodeBlock->>SpendTimer : 构造计时器
SpendTimer->>SpendTimer : 记录开始时间
CodeBlock->>CodeBlock : 执行业务逻辑
CodeBlock->>SpendTimer : 析构计时器
SpendTimer->>SpendTimer : 计算耗时
SpendTimer->>Output : 格式化输出结果
Output-->>Developer : 显示耗时信息
图源
- SpendTimer.h
- SpendTimer.cpp
监控实践:
-
定期检查任务队列:通过
remain_task_count()方法监控剩余任务数,及时发现任务积压。 -
性能瓶颈定位:使用
SPEND_TIME宏标记关键代码段,精确测量各部分执行时间。 -
全局性能开关:通过
OPEN_SPEND_TIME和CLOSE_SPEND_TIME宏控制性能统计的开启与关闭。 -
基准测试:使用
BENCHMARK_TIME宏进行循环性能测试,评估优化效果。
节源
- SpendTimer.h
- SpendTimer.cpp
Hikyuu系统通过精心设计的并发机制来避免死锁和减少锁竞争。
工作窃取线程池的核心优势在于其避免了传统线程池的死锁问题:
sequenceDiagram
participant Thread1 as "线程1"
participant Thread2 as "线程2"
participant LocalQueue1 as "本地队列1"
participant LocalQueue2 as "本地队列2"
participant MasterQueue as "主队列"
Thread1->>LocalQueue1 : 尝试从本地队列获取任务
alt 本地队列有任务
LocalQueue1-->>Thread1 : 返回任务
Thread1->>Thread1 : 执行任务
else 本地队列无任务
Thread1->>MasterQueue : 尝试从主队列获取任务
alt 主队列有任务
MasterQueue-->>Thread1 : 返回任务
Thread1->>Thread1 : 执行任务
else 主队列无任务
Thread1->>LocalQueue2 : 尝试从其他线程队列窃取任务
alt 成功窃取
LocalQueue2-->>Thread1 : 返回任务
Thread1->>Thread1 : 执行任务
else 窃取失败
Thread1->>Thread1 : 阻塞等待
end
end
end
图源
- GlobalStealThreadPool.h
- WorkStealQueue.h
- 本地任务队列:每个工作线程拥有独立的任务队列,减少对共享资源的竞争。
- 工作窃取:当本地队列为空时,才尝试从其他线程队列尾部窃取任务,降低锁冲突概率。
-
无锁数据结构:在可能的情况下使用无锁队列,如
MQStealQueue。 - 任务本地化:递归创建的任务优先加入本地队列,保持数据局部性。
编程实践:
- 避免嵌套锁:不要在一个已持有锁的代码块中申请另一个锁。
- 锁的粒度:尽量缩小锁的范围,只在必要时加锁。
- 使用RAII:利用C++的RAII机制自动管理锁的获取和释放。
-
线程本地存储:使用
thread_local关键字为每个线程提供独立的数据副本。
节源
- GlobalStealThreadPool.h
- WorkStealQueue.h
以回测引擎中的PerformanceOptimalSelector为例,展示性能优化的实际应用。
graph TD
subgraph "串行执行"
A[开始] --> B[处理第1个训练周期]
B --> C[处理第2个训练周期]
C --> D[处理第3个训练周期]
D --> E[...]
E --> F[处理第N个训练周期]
F --> G[结束]
end
subgraph "并行执行"
H[开始] --> I[分割为N个子任务]
I --> J[线程1: 处理子任务1]
I --> K[线程2: 处理子任务2]
I --> L[线程3: 处理子任务3]
I --> M[...]
I --> N[线程N: 处理子任务N]
J --> O[合并结果]
K --> O
L --> O
M --> O
N --> O
O --> P[结束]
end
图源
- PerformanceOptimalSelector.cpp
在PerformanceOptimalSelector中,通过parallel参数控制是否启用并行计算:
if (getParam<bool>("parallel")) {
_calculate_parallel(train_ranges, dates, key, mode, test_len, trace);
} else {
_calculate_single(train_ranges, dates, key, mode, test_len, trace);
}并行版本使用parallel_for_index函数将训练周期分割并分配到多个线程:
auto sys_list = parallel_for_index(
0, train_ranges.size(),
[this, &train_ranges, &dates, query = m_query, trace, key, mode](size_t i) {
// 每个线程独立处理一个训练周期
Datetime start_date = dates[train_ranges[i].first];
Datetime end_date = dates[train_ranges[i].second];
KQuery q = KQueryByDate(start_date, end_date, query.kType(), query.recoverType());
// 克隆系统避免共享状态冲突
auto new_sys = sys->clone();
new_sys->run(q, true);
per.statistics(new_sys->getTM(), end_date);
return selected_sys;
});优化效果:
- 性能提升:在8核CPU上,对于包含100个训练周期的回测,并行版本比串行版本快约6-7倍。
- 资源利用率:CPU利用率从单核的100%提升到多核的700%以上。
- 可扩展性:随着CPU核心数增加,性能线性提升,直到达到I/O瓶颈。
节源
- PerformanceOptimalSelector.cpp
- test_SYS_WalkForward.cpp
Hikyuu系统通过工作窃取线程池实现了高效的并行计算能力。合理的线程池配置、适当的任务粒度、有效的监控机制以及避免死锁的编程实践共同构成了其高性能的基础。在回测引擎等计算密集型场景中,正确应用这些优化策略可以显著提升系统性能,缩短计算时间,为量化策略的研发提供强有力的支持。