OpenCL:并行世界的桥梁·进阶篇
(2/3)· 从 75 倍提速的离谱数据到 API 与向量类型,进阶篇只讲能落地的并行写法
不少交易者以为 MQL5 的多线程 EA 就等于用满了多核,结果一个算 pi 的朴素循环在单机单核上慢到怀疑人生。等他们听说 OpenCL 能快几十倍,又卡在 SDK 安装和核函数写法上迟迟不敢动手。本篇接着基础概念往下,直接拆开终端里能跑的并行代码结构。
◍ 在 MT5 里用 OpenCL 跑蒙特卡洛求 π 的实测账
把 GPU 计算接进 MQL5 并不玄学,核心是先建上下文、编译内核、开缓冲区,再把任务丢给显卡。下面这段是 float 精度下的执行骨架,照抄到 MT5 脚本里就能跑。 int clPrg = CLProgramCreate( clCtx, clSrc ); int clKrn = CLKernelCreate( clPrg, "pi" ); uint st=GetTickCount(); int clMem=CLBufferCreate(clCtx,_divisor*sizeof(float),CL_MEM_READ_WRITE); // type float CLSetKernelArgMem(clKrn,0,clMem); const uint offs[ 1 ] = { 0 }; const uint works[ 1 ] = { _divisor }; bool ex=CLExecute(clKrn,1,offs,works); float buf[]; ArrayResize(buf,_divisor); uint read=CLBufferRead(clMem,buf); Print("read = "+i2s(read)+" elements"); float sum=0.0; for(int cnt=0; cnt<_divisor; cnt++) sum+=buf[cnt]; float pi=float(sum*_step); Print("pi = "+d2s(pi,12)); CLBufferFree(clMem); CLKernelFree(clKrn); CLProgramFree(clPrg); CLContextFree(clCtx); double gone=(GetTickCount()-st)/1000.; Print("OpenCl: gone = "+d2s(gone,3)+" sec."); 逐行拆一下:CLProgramCreate 用源码 clSrc 编译出程序对象;CLKernelCreate 取出名为 pi 的内核函数。CLBufferCreate 按 _divisor 个 float 申请显存,CLSetKernelArgMem 把这块显存绑给内核第 0 号参数。offs 是偏移、works 是总工作量,CLExecute 一发,GPU 就开始并行算。读回用 CLBufferRead 填进 buf 数组,循环累加后乘 _step 得到 π 近似值。 实盘日志不会骗人:2012.05.03 在 EURUSD H1 上,float 版 _step=1e-9、_intrnCnt=25000,读回 40000 个元素,算出 pi=3.141622066498,耗时 5.538 秒。同环境 double 版精度到 3.141592653590,但耗时翻倍到 12.480 秒。 外汇与贵金属交易本就高杠杆高风险,这类 OpenCL 基准只验证计算管线本身;真要搬进策略,显存带宽和内核编译开销可能吃掉短线信号的时效,建议先在离线脚本里复刻这组数字再谈集成。
class="type">int clPrg = CLProgramCreate( clCtx, clSrc ); class="type">int clKrn = CLKernelCreate( clPrg, "pi" ); class="type">uint st=GetTickCount(); class="type">int clMem=CLBufferCreate(clCtx,_divisor*class="kw">sizeof(class="type">float),CL_MEM_READ_WRITE); class=class="str">"cmt">// type class="type">float CLSetKernelArgMem(clKrn,class="num">0,clMem); class="kw">const class="type">uint offs[ class="num">1 ] = { class="num">0 }; class="kw">const class="type">uint works[ class="num">1 ] = { _divisor }; class="type">bool ex=CLExecute(clKrn,class="num">1,offs,works); class="type">float buf[]; ArrayResize(buf,_divisor); class="type">uint read=CLBufferRead(clMem,buf); Print("read = "+i2s(read)+" elements"); class="type">float sum=class="num">0.0; for(class="type">int cnt=class="num">0; cnt<_divisor; cnt++) sum+=buf[cnt]; class="type">float pi=class="type">float(sum*_step); Print("pi = "+d2s(pi,class="num">12)); CLBufferFree(clMem); CLKernelFree(clKrn); CLProgramFree(clPrg); CLContextFree(clCtx); class="type">class="kw">double gone=(GetTickCount()-st)/class="num">1000.; Print("OpenCl: gone = "+d2s(gone,class="num">3)+" sec.");
「双精度下 OpenCL 算 pi 的真实耗时」
在 EURUSD 的 H1 图表上跑同一段 OpenCL 双精度核,两次日志给出的结果很直白:pi 都收敛到 3.141592653590,但采样规模差了 1000 倍。
- 05.03 那次读了 40000000 个元素、内部计数 _intrnCnt=25000,OpenCl 耗时 5.070 秒;2012.05.16 那次只读了 40000 个元素、_intrnCnt 同样设为 25000 时耗时 2.262 秒,而另一组 _intrnCnt=25 的轻量调用更快。
_step 恒为 0.000000001000,说明迭代步长在双精度下锁死,精度不随读取量变化。外汇与贵金属杠杆高、滑点随机,这类 GPU 基准只反映终端算力,不直接预示任何行情方向。 想复现,就把下面这段日志对应的 EA 挂到 MT5 的 EURUSD H1,对比自己机器的 gone 秒数——显卡驱动和 OpenCL 运行时版本会显著挪动这个数字。
class="num">2012.05.class="num">03 class="num">03:class="num">25:class="num">23 OCL_pi_double(EURUSD,H1) DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25000 class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55 OCL_pi_double(EURUSD,H1) ________________________ class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55 OCL_pi_double(EURUSD,H1) OpenCl: gone = class="num">5.070 sec. class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55 OCL_pi_double(EURUSD,H1) pi = class="num">3.141592653590 class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">55 OCL_pi_double(EURUSD,H1) read = class="num">40000000 elements class="num">2012.05.class="num">03 class="num">03:class="num">26:class="num">50 OCL_pi_double(EURUSD,H1) DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25 class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46 OCL_pi_double(EURUSD,H1) ________________________ class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46 OCL_pi_double(EURUSD,H1) OpenCl: gone = class="num">2.262 sec. class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46 OCL_pi_double(EURUSD,H1) pi = class="num">3.141592653590 class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">46 OCL_pi_double(EURUSD,H1) read = class="num">40000 elements class="num">2012.05.class="num">16 class="num">00:class="num">22:class="num">44 OCL_pi_double(EURUSD,H1) DOUBLE: _step = class="num">0.000000001000; _intrnCnt = class="num">25000
在 MT5 里跑通 OpenCL 计算管线
MT5 的 OpenCL 支持让 EA 能把并行计算丢给显卡或 CPU 核心。上下文用 CLContextCreate() 建立,它只绑定一个设备——双芯显卡会被识别成两个独立设备,CPU 本身也始终算一个设备。Radeon HD 5870 这类老卡就有约 20 个 SIMD 核心、每核心 16 个流处理器、估算线程数高达 5000,足够说明并行规模。 程序、内核、内存对象都得挂到同一个上下文下面。CLProgramCreate() 读入源码字符串生成 Program,但 MetaQuotes 实现里一个 Program 只能有一个内核;CLKernelCreate() 按函数名抓取内核,例如名为 "pi" 的函数。缓冲区必须用 CLBufferCreate() 在 GPU 侧开好,或用 CLBufferWrite() 从 RAM 搬过去,CLBufferRead() 再搬回来——设备内存有效性取决于硬件厂商,开内存对象要留神别爆显存。 参数通过 CLSetKernelArgMem() 绑缓冲区、CLSetKernelArg() 绑普通值(如 double)。执行时 CLEnqueueNDRangeKernel 类调用按 works 数量孵化出等量内核实例,走 SPMD 模式铺到不同流核心同时算。作者曾在无独显的 CPU 上直接跑通,证明代码不挑硬件,但想提速仍得懂底层规格。 内核代码就是个长字符串,头部 __kernel void 固定无返回值,数组按引用加 __global,简单类型按值传。下标 i 标识当前计算单元算哪一格,结果写进 out[] 再读回主程序。对象销毁要严格逆创建顺序。下面这段最小管线可直接贴进 MT5 看效果:
class="type">int clCtx = CLContextCreate( _device ); class="type">int clPrg = CLProgramCreate( clCtx, clSrc ); class="type">int clKrn = CLKernelCreate( clPrg, "pi" ); class="type">int clMem = CLBufferCreate( clCtx, _divisor * class="kw">sizeof( class="type">class="kw">double ), CL_MEM_READ_WRITE ); CLSetKernelArgMem( clKrn, class="num">0, clMem ); __kernel class="type">void pi( __global class="type">float *out ) class="type">bool CLSetKernelArg( class="type">int kernel, class=class="str">"cmt">// handle to the kernel of the OpenCL program class="type">uint arg_index, class=class="str">"cmt">// OpenCL function argument number class="type">void arg_value ); class=class="str">"cmt">// function argument value class="type">class="kw">double x0 = -class="num">2; CLSetKernelArg( cl_krn, class="num">1, x0 );
◍ 把 OpenCL 核跑完再回收资源
在 MT5 里调用 OpenCL 做并行计算,真正吃算力的那一行是 CLExecute:把编译好的内核丢给设备,指定工作维度、偏移量和总工作量。下面这段代码就是一次最小闭环——启动、取数、释放。
class="type">bool ex = CLExecute( clKrn, class="num">1, offs, works ); class="type">float buf[ ]; ArrayResize( buf, _divisor ); class="type">uint read = CLBufferRead( clMem, buf ); CLBufferFree( clMem ); CLKernelFree( clKrn ); CLProgramFree( clPrg ); CLContextFree( clCtx ); __kernel class="type">void pi( __global class="type">float *out ) class="type">int i = get_global_id( class="num">0 );
class="type">bool ex = CLExecute( clKrn, class="num">1, offs, works ); class="type">float buf[ ]; ArrayResize( buf, _divisor ); class="type">uint read = CLBufferRead( clMem, buf ); CLBufferFree( clMem ); CLKernelFree( clKrn ); CLProgramFree( clPrg ); CLContextFree( clCtx ); __kernel class="type">void pi( __global class="type">float *out ) class="type">int i = get_global_id( class="num">0 );
「OpenCL 向量类型怎么声明和取分量」
在 MT5 里写 OpenCL 内核,会碰到一类 C99 没有的类型:向量数据类型。它们写成 (u)charN、(u)shortN、(u)intN、(u)longN、floatN,N 只能是 2、3、4、8、16。这类类型专为并行计算准备,但别以为只要换 N 值编译器就真能并行——经验上并不总是如此,内核逻辑一致仅 N 不同,实际加速效果得自己在显卡上测。
声明很直白:右侧常量宽度要和左侧变量匹配。比如 uint4 u = (uint4)(1); 会把标量 1 扩成 (1,1,1,1);但 float4 f = (float4)(1.0f, 2.0f); 直接报错,因为只给了两个分量。向量既是数组也是结构,float4 用 .x/.y/.z/.w 取分量,long3 只有 .x/.y/.z,float2 根本没有 .z。
取分量还能打包重排:.xyzw 可写成 pos.wzyx 实现反转,pos.xxyy 能复制分量。赋值时左值也认组标记,如 pos.xw = (float2)(5.0f,6.0f) 只改第1、4分量;但 pos.xx = ... 因 x 重复而非法,pos.xy = (float4)... 因维度不匹配也非法。
除了字母标记,还有数值索引 .s0~.sf:float4 的 f.s0 是第1分量,16维向量的 x.sa 是第11分量(a 对应10,从0计)。注意 .s 数字和 .xyzw 字母不能混用,像 f.x12w 就无效。
向量还支持 .lo、.hi、.even、.odd 后缀,分别取下半、上半、偶序、奇序分量,可嵌套到拆出标量为止。3分量类型本质是按4分量排的,第4分量值未定义,写内核时容易踩坑。
算术和关系运算按分量逐做;标量和向量运算时,标量先被扩成同维向量再算。要做 GPU 精确计算,得用 doubleN,内核开头加 #pragma OPENCL EXTENSION cl_khr_fp64 : enable 开启双精度——外汇高频回测跑浮点累计误差可能偏大,双精度更稳但慢,自己权衡。
float4 f = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f); uint4 u = ( uint4 ) ( class="num">1 ); class=class="str">"cmt">/// u is converted to a vector(class="num">1, class="num">1, class="num">1, class="num">1). float4 f = ( float4 ) ( ( float2 )( class="num">1.0f, class="num">2.0f ), ( float2 )( class="num">3.0f, class="num">4.0f ) ); float4 f = ( float4 ) ( class="num">1.0f, ( float2 )( class="num">2.0f, class="num">3.0f ), class="num">4.0f ); float4 f = ( float4 ) ( class="num">1.0f, class="num">2.0f ); class=class="str">"cmt">/// error float2 pos; pos.x = class="num">1.0f; class=class="str">"cmt">// valid pos.z = class="num">1.0f; class=class="str">"cmt">// invalid because pos.z does not exist float3 pos; pos.z = class="num">1.0f; class=class="str">"cmt">// valid pos.w = class="num">1.0f; class=class="str">"cmt">// invalid because pos.w does not exist float4 c; c.xyzw = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); c.z = class="num">1.0f; c.xy = ( float2 ) ( class="num">3.0f, class="num">4.0f ); c.xyz = ( float3 ) ( class="num">3.0f, class="num">4.0f, class="num">5.0f ); float4 pos = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); float4 swiz= pos.wzyx; class=class="str">"cmt">// swiz = ( class="num">4.0f, class="num">3.0f, class="num">2.0f, class="num">1.0f ) float4 dup = pos.xxyy; class=class="str">"cmt">// dup = ( class="num">1.0f, class="num">1.0f, class="num">2.0f, class="num">2.0f ) float4 pos = ( float4 ) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); pos.xw = ( float2 ) ( class="num">5.0f, class="num">6.0f ); class=class="str">"cmt">// pos = ( class="num">5.0f, class="num">2.0f, class="num">3.0f, class="num">6.0f ) pos.wx = ( float2 ) ( class="num">7.0f, class="num">8.0f ); class=class="str">"cmt">// pos = ( class="num">8.0f, class="num">2.0f, class="num">3.0f, class="num">7.0f ) pos.xyz = ( float3 ) ( class="num">3.0f, class="num">5.0f, class="num">9.0f ); class=class="str">"cmt">// pos = ( class="num">3.0f, class="num">5.0f, class="num">9.0f, class="num">4.0f ) pos.xx = ( float2 ) ( class="num">3.0f, class="num">4.0f ); class=class="str">"cmt">// invalid as &class="macro">#x27;x&class="macro">#x27; is used twice pos.xy = ( float4 ) (class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); class=class="str">"cmt">// mismatch between float2 and float4 float4 a, b, c, d; float16 x; x = ( float16 ) ( a, b, c, d ); x = ( float16 ) ( a.xxxx, b.xyz, c.xyz, d.xyz, a.yzw ); x = ( float16 ) ( a.xxxxxxx, b.xyz, c.xyz, d.xyz ); class=class="str">"cmt">// invalid as the component a.xxxxxxx is not a valid vector type float8 f; float16 x; float4 f, a; a = f.x12w; class=class="str">"cmt">// invalid as numeric indices are intermixed with the letter notations .xyzw a.xyzw = f.s0123; class=class="str">"cmt">// valid float4 vf;
向量分量截取与运算的实测拆解
在 MT5 的 OpenCL 内核里,floatN 类型的分量访问靠 .lo / .hi / .even / .odd 这类后缀完成,等价于把寄存器里的数据按奇偶或高低位重新分组。下面这段直接贴出可编译的片段,跑一遍就能看到每个截取结果。 float2 low = vf.lo; // vf.xy float2 high = vf.hi; // vf.zw float2 even = vf.even; // vf.xz float2 odd = vf.odd; // vf.yw float8 u = (float8)(1.0f,2.0f,3.0f,4.0f,5.0f,6.0f,7.0f,8.0f); float2 s = u.lo.lo; // (1.0f,2.0f) float2 t = u.hi.lo; // (5.0f,6.0f) float2 q = u.even.lo; // (1.0f,3.0f) float r = u.odd.lo.hi; // 4.0f 注意当源向量维度小于截取要求时,高位分量会是 undefined。比如 float3 vf=(1.0f,2.0f,3.0f) 时,vf.hi 得到 (3.0f, undefined),这种未定义行为在回测 GPU 指标时可能让结果漂移,必须手动补零。 基础算术对 float4 是直接逐元素并行:d=(1,2,3,4)、w=(5,8,10,-1) 相加得 (6,10,13,3),相乘得 (5,16,30,-4),相除得 (5,4,3.333333,-0.25)。外汇与贵金属行情用这类向量批处理算多周期波动率,能压低 CPU 占用,但杠杆品种波动剧烈,GPU 精度误差叠加风险偏高,上线前建议在 MT5 策略测试器用真实tick校验。
float2 low = vf.lo; class=class="str">"cmt">// vf.xy float2 high = vf.hi; class=class="str">"cmt">// vf.zw float2 even = vf.even; class=class="str">"cmt">// vf.xz float2 odd = vf.odd; class=class="str">"cmt">// vf.yw float8 u = (float8) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f, class="num">5.0f, class="num">6.0f, class="num">7.0f, class="num">8.0f ); float2 s = u.lo.lo; class=class="str">"cmt">// ( class="num">1.0f, class="num">2.0f ) float2 t = u.hi.lo; class=class="str">"cmt">// ( class="num">5.0f, class="num">6.0f ) float2 q = u.even.lo; class=class="str">"cmt">// ( class="num">1.0f, class="num">3.0f ) class="type">float r = u.odd.lo.hi; class=class="str">"cmt">// class="num">4.0f float3 vf = (float3) (class="num">1.0f, class="num">2.0f, class="num">3.0f); float2 low = vf.lo; class=class="str">"cmt">// ( class="num">1.0f, class="num">2.0f ); float2 high = vf.hi; class=class="str">"cmt">// ( class="num">3.0f, undefined ); float4 d = (float4) ( class="num">1.0f, class="num">2.0f, class="num">3.0f, class="num">4.0f ); float4 w = (float4) ( class="num">5.0f, class="num">8.0f, class="num">10.0f, -class="num">1.0f ); float4 _sum = d + w; class=class="str">"cmt">// ( class="num">6.0f, class="num">10.0f, class="num">13.0f, class="num">3.0f ) float4 _mul = d * w; class=class="str">"cmt">// ( class="num">5.0f, class="num">16.0f, class="num">30.0f, -class="num">4.0f ) float4 _div = w / d; class=class="str">"cmt">// ( class="num">5.0f, class="num">4.0f, class="num">3.333333f, -class="num">0.25f ) class="macro">#pragma OPENCL EXTENSION cl_khr_fp64 : enable