简介
金融服务客户经常需要解决方案能够尽可能快速地分析市场数据。 为此需要持续提升金融算法的性能。 金融算法包含多种模型,包括 Monte Carlo、Black-Scholes、Bonds 等等。 单指令多数据 (SIMD) 矢量编程可显著提高上述工作负载的运行速度。 是否能够实现最佳 SIMD 性能取决于高效的内存访问。 本文将用两种方法运行开源 Quantlib库中面向欧式期权估价的 Black-Scholes 工作负载,以执行数据布局优化。 第一种方法是手动调节数据布局,以达到最佳性能;第二种方法是使用英特尔® C++ 编译器的 SIMD 数据布局模板 (SDLT)。 在这一过程中,我们分析 SDLT 实施并对两种数据布局优化方法所实现的性能提升进行了对比。 结果显示,SDLT 所实现的性能提升与手动调节的优化版相当。 另外,相比于手动调节版,SDLT 所需的代码修改非常少,从而确保了原始源代码的代码可读性。
QuantLib 概述
Quantlib 是用 C++ 编写的免费版、全功能开源金融库。 它包含许多模板以及相关类别,支持用户使用常用的随机程序(Monte Carlo、Black-Scholes、Hull-White、Libor 等)和期限结构(零曲线、ForwardRate、利率期限等)快速分析金融工具(债券、期权、swap、股票等)。 许多金融机构都使用类似的库,以获取用于应用代码的概念和 API 等价集。1
SDLT 概述
SIMD 数据布局模板是一种 C++11 库,可实施通过用户抽象出的数据布局优化方法,帮助实现矢量化。 例如,英特尔® SDLT 库为用户提供结构阵列 (AOS) 接口,而以阵列结构 (SOA) 的格式将数据保存在内存中。 该库运行时不要求任何特殊的编译器支持,但由于其 SIMD 友好型布局,因此能够更好地利用英特尔® C++ 编译器的性能特性,比如 OpenMP* SIMD 扩展、英特尔® Cilk™ Plus SIMD 扩展和 SIMD/IVDEP 编译指示。2
实施
Quantlib 库提供单元测试套件,包括欧式期权、美式期权和百慕大期权估价。 在本文中,我们将使用 Black-Scholes 模型行使欧式期权估价,作为原始 Quantlib 库的测试案例。 Black-Scholes 是一种用于欧式期权定价的闭环式金融衍生工具,只能在到期日当天行使。 Black-Scholes 测试案例为单精度 32 位浮点实施。
Quantlib 库提供单元测试套件,包括欧式期权、美式期权和百慕大期权估价。 在本文中,我们将使用 Black-Scholes 模型行使欧式期权估价,作为原始 Quantlib 库的测试案例。 Black-Scholes 是一种用于欧式期权定价的闭环式金融衍生工具,只能在到期日当天行使。 Black-Scholes 测试案例为单精度 32 位浮点实施。
我们强调最大限度地减少对原始开源 Quantlib 的更改, 但仍然进行了以下修改:
- 删除 Observable 和 Observed 模式,因为这种延续不适用测试案例。
- 使用 Quantlib 中的原始类别,以便输入参数为不同类别的实例。 删除原始类别的少量数据成员,因为它们不属于本次测试案例的一部分。
- 实施过程经过修改,包含多个期货估价,而不是模仿真实场景的一个期货。
为获取优化型手动调节版,我们首先从 C++ 优化版本开始。 输入类别的所有方法均支持内联。 为引入矢量化,我们删除了开关语句,将其替换为 if-else。 通过 #pragma omp simd 对示例循环进行矢量化处理。 它是一种矢量化版本,但并不包含任何面向数据布局的优化。 我们发现,没有数据布局优化,C++ 矢量化版本会生成收集行为,从而降低 SIMD 代码的效率(下文将予以介绍)。
为通过数据布局优化实现最佳性能并消除 C++ 开销,我们使用扁平化 C 版 Black-Scholes 模型来进行欧式期权估价4。 扁平化 C 版最初摘录于文章“基于 GPU 加速金融应用”4。 英特尔进一步优化了面向英特尔® 架构的 OpenMP 版本(前文已经予以介绍,并发表了新文章)。 在扁平化 C 版本中,类别映射至结构。
图 1 展示了 C 版本中输入数据结构的 AOS 布局,其中 C++ 版本的不同输入类别以单一结构的形式呈现,数据成员以结构成员的形式呈现。 为实现手动调节的数据布局优化,结构阵列转化为阵列结构,如图 2 所示。 这种优化可为关键矢量循环提供出色的缓存本地性、对齐,以及单位步长访问。 使用英特尔® 线程构建模块(英特尔® TBB)内存分配程序分配内存。 英特尔 TBB 具有双重优势:内存在缓存对齐边界上分配,以及每个线程都有自己的内存池,因而能够消除对全局堆的依赖性并提高 NUMA 本地性。 有关优化版 C 应用的详细信息以及源代码,请参阅文章“基于英特尔® 架构加速金融应用”3。
typedef struct { int type; float strike; float spot; float q; float r; float t; float vol; float tol; } optionInputStruct; optionInputStruct* values = new optionInputStruct[num_options];
图 1:原始 C 版本的结构阵列布局。
using namespace tbb; typedef struct { int* type; float* strike; float* spot; float* q; float* r; float* t; float* vol; float* tol; } /**Memory Allocation using TBB**/ optionInputStruct* values =(optionInputStruct*)scalable_aligned_malloc(sizeof(optionInputStruct),L1LINE_SIZE); values->type= (int*)scalable_aligned_malloc(sizeof(int)*memsize,L1LINE_SIZE); values->strike= (float*)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->spot=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->q=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->r=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->t=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->adj_t=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE); values->vol=( float *)scalable_aligned_malloc(sizeof(float)*memsize,L1LINE_SIZE);
图 2:优化型 C 版本的阵列结构版。
为使用 SDLT 进行优化,我们以优化型 C++ 版本为基准。 我们使用 sdlt::soa1d_container,它是面向一维基元容器的“阵列结构”内存布局的模板类,具备以下功能:
- 采用类似 std::vector 的接口动态调整大小
- 包含适用于在 SIMD 循环内部对结构进行高效数据访问的访问器对象。
图 3 所示为 soa1d 容器实施。/**declaring sdlt SOA 1D container for engine class**/ typedef sdlt::soa1d_container<AnalyticEuropeanEngine> OptionContainer; /**Initializing SDLT Container**/ OptionContainer europeanOption(numVals); #pragma omp simd for (int i=0; i < numVals; ++i) { AnalyticEuropeanEngine localeuropeanOption = europeanOption[i]; output[i] = localeuropeanOption.calculate(); }
图 3:soa1d 容器实施。
我们需要声明 SDLT 基元,以描述我们希望放置在 SDLT 容器中的结构的数据布局。 该数据结构必须遵守一定的规则,才能用作基元。 创建之前的优化型 C++ 版本时,我们遵守了这些规则,因此可支持编译器在循环内部生成面向局部变量基元实例的高效 SIMD。
为声明基元,可以使用 helper 宏指令 SDLT_PRIMITIVE,从而接受结构类型后附带数据成员列表(逗号隔开),如图 4 所示。
SDLT_PRIMITIVE( AnalyticEuropeanEngine, //engineclass process_, //Data member payoff_, //Data member exercise_); //Data member
图 4:SDLT_PRIMITIVE 声明。
我们将数据成员声明为 SDLT_PRIMITIVE_FRIEND,以便 SDLT 访问该类的数据成员。 另外一种方法是将其声明为公开数据成员。
class AnalyticEuropeanEngine { . . . SDLT_PRIMITIVE_FRIEND; . . }
图 5:将 SDLT_PRIMITVE_FRIEND 声明为友元类。
图 4 和图 5 中为引入 SDLT 所做的更改适用于所有支持数据在矢量化代码中访问的类。 与扁平化 C 版本不同,类方法或算法无需作出更改。 高级算法仍然具备可读性和可维护性。 关于 SDLT 版本的源代码,请访问此处。5
结果
本节介绍了不同示例组合和迭代测试的结果。 为确保其完整性,我们将介绍未经任何手动数据布局更改的 C++ 未优化型版本和矢量化 C++ 版本的结果,以及 SDLT 和最佳优化型 C 版本的结果。 表 1 所示为性能对比结果。
以秒为单位的全部用时(越低越好)
测试 | 样本 | 迭代 | C++ 未优化型版本 | C++ 矢量化版本 | C++ SDLT 版本 | C 扁平化优化型版本 |
1 | 100,000 | 2048 | 20.7 | 2.1 | 1.7 | 1.7 |
2 | 100,000 | 4096 | 41.3 | 4.1 | 3.5 | 3.5 |
3 | 1,000,000 | 2048 | 217.1 | 33.3 | 17.3 | 17.4 |
4 | 1,000,000 | 4096 | 451.3 | 66.6 | 34.7 | 34.8 |
表 1: 单精度性能对比结果。
图 6:不同数据优化方法所实现的性能提升对比。
C++ 未优化型版本表示未实现任何矢量化性能提升的基准版本。 引入之前介绍的不同优化方法可将性能提升约 10 倍。 处理本测试案例中数量较大的输入样本时,数据布局优化可将性能进一步提升约 90%。
如表 1 所示,SDLT 的性能与扁平化优化型版本大体相同。 矢量报告显示 C++ 矢量化版本中生成了“收集”行为。 代码执行非单位步长内存访问时,编译器在矢量代码中生成了“收集”指令。 通过收集指令,数据从内存加载至矢量寄存器。 一般来说,收集指令可用于提取分散在内存中的数据元素。 因此执行收集时产生了延迟,进而降低了应用的整体性能。
为实现 C++ 版本的最佳性能,必须消除收集行为, 这一目标可通过更改数据布局来实现。 在 C++ 版本中,输入数据分散在不同的类别中。 因此实施数据布局优化要求显著更改原始源代码。 我们从 C++ 迁移至 C 版本以实现最佳性能时可验证这点。 另外,SDLT 使用原始 C++ 代码中引入的模板更改数据布局,如图 3、4、5 所示。
总结
针对算法提升矢量化性能与高效数据布局息息相关。 本文介绍了不同数据布局优化方法对性能所产生的影响。 手动调节的数据布局优化型版本的性能与英特尔® SDLT 版本的性能进行了比较。 SDLT 方法的性能与优化型版本的性能大体相同,从基本上来说是通过手动调节对数据布局作出了更改的扁平化 C 版本。 请注意,为引入 SDLT 所做的更改要求几乎不更改原始源代码。 另一方面,手动数据布局更改要求大幅修改代码,以实现最佳性能。 除 SOA 数据布局外,SDLT 在用户不知道的情况下实施对齐内存分配并为编译器提供对齐提示,以进一步优化性能。 因此用户只需引入上述 SDLT 模板即可,这点与手动调节版本不同,该版本要求用户明确实施从 AOS 到 SOA 的转化,并对齐数据,以提升性能。
关于作者
Nimisha Raut 拥有克莱姆森大学计算机工程系硕士学位,以及印度孟买大学电子工程专业的学士学位。 目前在英特尔公司软件及服务事业部的金融服务设计团队担任软件工程师。 她的主要兴趣是基于英特尔® 处理器及协处理器和 Nvidia* GPGPU 的并行编程与性能分析。
Alex Well 目前在英特尔技术计算设计团队担任高级应用工程师。 Alex 带领支持动画电影工具(尤其是动画角色)实现数据并行化。 他设计了英特尔 SIMD 数据布局模板以及开源项目“XForm 构建模块”。 加入英特尔公司之前,Alex 开发了游戏并共同设计了用 C++ 编写的交叉平台 3D 引擎。Alex 毕业于圣巴巴拉市的加州大学。
参考资料
- Quantlib 网站:
http://quantlib.org/index.shtml - 英特尔® SIMD 数据布局模板:
https://software.intel.com/zh-cn/code-samples/intel-compiler/intel-compiler-features/intel-sdlt - 基于英特尔架构加速金融应用:
https://software.intel.com/sites/default/files/managed/0f/32/Accelerating-Financial-Applications-on-Intel-architecture.pdf - 基于 GPU 加速金融应用:
https://cavazos-lab.github.io/FinanceBench/resources/AcceleratingFinancialApplicationsOnTheGPU-paper.pdf - Quantlib-SDLT 源代码:
https://software.intel.com/sites/default/files/managed/7e/63/Quantlib-SDLT-Whitepaper.tar.gz