MQL5秘笈之:采用关联数组或字典实现快速数据访问·综合运用
类型强转与字符串哈希的底层玩法
MQL5 里不同类型间的强制转换经常藏着坑。下面这段代码把 ushort 塞进 ulong 再取回来,值仍是 103,说明窄类型转宽类型不会丢数据;但反向从 ulong 截回 ushort 时若原值超 65535 就会溢出,写 EA 时别想当然。 利用结构体做位级 reinterpret 也很直接:把 double 的 3.14159 赋给 DoubleValue,再强转成 ULongValue,printf 打出的 ulong 值约为 4614256650576692846(IEEE754 双精度位模式),改成 3.14160 后值变为 4622945019783909376,两者差约 8.69e17,可见浮点微小变动在二进制层放大极猛。 字符串哈希可用 Adler32 自己实现:模数固定 65521,s1 累加字符、s2 累加 s1,最后 (s2<<16)+s1。跑下来 "Hello world" 得 156344097,加个感叹号变成 224354354,"MetaTrader" 为 758570357。外汇与贵金属行情字符串做去重或比对时,这种 32 位摘要比全量比较省资源,但碰撞概率随样本增大而上升,仅适合辅助判重。 别把正态当圣经 上面浮点位模式差说明:策略里用 double 做键或哈希前,先定精度截断,否则 1e-5 偏差就导向不同桶,回测和实盘可能对不上。
class="type">class="kw">ulong ln = (class="type">class="kw">ushort)class="num">103; class=class="str">"cmt">// 将ushort类型值保存在ulong类型中(class="num">103) class="type">class="kw">ushort us = (class="type">class="kw">ushort)ln; class=class="str">"cmt">// 从ulong类型中获取ushort类型值(class="num">103) class="kw">struct DoubleValue{ class="type">class="kw">double value;} dValue; class="kw">struct ULongValue { class="type">class="kw">ulong value; } lValue; class="type">void OnStart() { class=class="str">"cmt">//--- dValue.value = class="num">3.14159; lValue = (ULongValue)dValue; printf((class="type">class="kw">string)lValue.value); dValue.value = class="num">3.14160; lValue = (ULongValue)dValue; printf((class="type">class="kw">string)lValue.value); } class="type">class="kw">float fl = class="num">3.14159f; class="type">class="kw">double dbl = fl; class="type">uint Adler32(class="type">class="kw">string line) { class="type">class="kw">ulong s1 = class="num">1; class="type">class="kw">ulong s2 = class="num">0; class="type">uint buflength=StringLen(line); class="type">uchar char_array[]; ArrayResize(char_array, buflength,class="num">0); StringToCharArray(line, char_array, class="num">0, -class="num">1, CP_ACP); for (class="type">uint n=class="num">0; n<buflength; n++) { s1 = (s1 + char_array[n]) % class="num">65521; s2 = (s2 + s1) % class="num">65521; } class="kw">return ((s2 << class="num">16) + s1); } class="type">void OnStart() { class=class="str">"cmt">//--- printf("Hello world - " + (class="type">class="kw">string)Adler32("Hello world")); printf("Hello world! - " + (class="type">class="kw">string)Adler32("Hello world!")); printf("Peace - " + (class="type">class="kw">string)Adler32("Peace")); printf("MetaTrader - " + (class="type">class="kw">string)Adler32("MetaTrader")); } class="type">class="kw">string array[];
「用 Adler32 做去重键的字符串容器」
在 MT5 里做轻量字符串池,常见做法是拿哈希当数组下标直接落位。下面这段 AddString 用 Adler32 把字符串压成 ulong 键,命中即存,省掉线性查找。 代码逻辑是先算 key,若 key 超出当前 array 长度就尝试 ArrayResize 到 key+1;扩容失败或越界时返回 false,成功才把 str 写进 array[key] 并返回 true。
class=class="str">"cmt">//| 结果: class=class="str">"cmt">//| 返回 true,如果字符串被成功添加,否则 class=class="str">"cmt">//| 返回 class="kw">false。 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool AddString(class="type">class="kw">string str) { class="type">class="kw">ulong key=Adler32(str); if(key>=ArraySize(array) && ArrayResize(array,key+class="num">1)<=key) class="kw">return class="kw">false; array[key]=str; class="kw">return true; }
class=class="str">"cmt">//| 结果: class=class="str">"cmt">//| 返回 true,如果字符串被成功添加,否则 class=class="str">"cmt">//| 返回 class="kw">false。 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool AddString(class="type">class="kw">string str) { class="type">class="kw">ulong key=Adler32(str); if(key>=ArraySize(array) && ArrayResize(array,key+class="num">1)<=key) class="kw">return class="kw">false; array[key]=str; class="kw">return true; }
◍ 用模板把多类型键值对塞进一个字典
想在 MT5 里写一个不挑 key 类型的关联数组,最笨的办法是为 ulong、char、string、double 每种基本类型各写一个 CDictionaryXxxObj 类。MQL5 基本类型太多,每处核心逻辑改动都得同步改 N 个类,维护成本直接爆炸。 正确做法是只写一个 CDictionary 类,把 AddObject 做成模板方法:template<typename T> bool AddObject(T key, CObject *value)。编译期才确定 T 的实际类型,一份代码通吃所有 key。字典内部不直存对象,而是用基于 CObject 的 KeyValuePair 容器打包,容器里额外带着原始 key、hash 码和前后指针。 AddObject 的运行时逻辑有三条铁律:先调 ContainsKey 查重,已存在就拒绝添加(避免一双 key 对应两值的不确定性);当 m_array 大小等于元素数时触发 Resize 扩容;按 ContainsKey 算出的 m_index 找到对应 CList 链表,没有就新建,再在表尾挂上新容器。删除走对称的 DeleteObjectByKey,代码同构不再展开。 hash 码怎么来?整型直接扩成 ulong;double/float 走结构体变换;string 用 Adler32。GetHashByKey 靠 TypeName 拿到 "string"/"int" 这类字符串再做分派。ContainsKey 不只比 hash,还要比实际 key 值,否则不同 key 同 hash 的冲突会让两个对象在逻辑里撞车。 动态内存方面,Resize 每次按 2 倍扩:存 65536 个元素只需 16 次调用(2^16),20 次后容量超百万(1048576)。缩容同法,但整数组重排很吃算力——先搬临时数组再算新索引。 枚举靠 KeyValuePair 的前后指针串成插入顺序链。向字典按 CNumber→CShip→CWeather→CHuman→CExpert→CCar 顺序插入,顺枚举打印出的日志就是倒序的 1:CNumber 到 6:CCar 反向,改两行 OnStart 即可逆序输出。外汇/贵金属 EA 用这套结构管对象,注意回测与实盘内存峰值差异,属高风险调试项。
CDictionaryLongObj class=class="str">"cmt">// 存储 <class="type">class="kw">ulong, CObject*> CDictionaryCharObj class=class="str">"cmt">// 存储 <class="type">char, CObject*> CDictionaryUcharObj class=class="str">"cmt">// 存储 <class="type">uchar, CObject*> CDictionaryStringObj class=class="str">"cmt">// 存储 <class="type">class="kw">string, CObject*> CDictionaryDoubleObj class=class="str">"cmt">// 存储 <class="type">class="kw">double, CObject*> ... class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 一个关联数组或者字典存储元素形如 | class=class="str">"cmt">//| <key - value>,key可能被任何基本类型所表示 | class=class="str">"cmt">//| 值可能被CObject类型对象表示。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CDictionary { class="kw">private: CList *m_array[]; class=class="str">"cmt">// 列表数组。 class="kw">template<class="kw">typename T> class="type">bool AddObject(T key,CObject *value); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 添加一个带 T 的CObject类型元素key到字典中 |
字典里塞任意类型键的对象
MQL5 标准库没给哈希表式容器留好接口,自己写 CDictionary 时,AddObject 的模板参数 T 可以吞下 int、double、string 甚至 ENUM_TIMEFRAMES 这类基本类型,value 则必须是 CObject 派生类指针。下面这段实现里,重复 key 直接返回 false,容量满就打印 Resize+当前总数再扩容,桶位无效就 new 一条 CList 挂上去。
class="kw">template<class="kw">typename T> class="type">bool CDictionary::AddObject(T key,CObject *value) { if(ContainsKey(key)) class="kw">return class="kw">false; if(m_total==m_array_size){ printf("Resize" + m_total); Resize(); } if(CheckPointer(m_array[m_index])==POINTER_INVALID) { m_array[m_index]=new CList(); m_array[m_index].FreeMode(m_free_mode); } KeyValuePair *kv=new KeyValuePair(key, m_hash, value); if(m_array[m_index].Add(kv)!=-class="num">1) m_total++; if(CheckPointer(m_current_kvp)==POINTER_INVALID) { m_first_kvp=kv; m_current_kvp=kv; m_last_kvp=kv; } else { m_current_kvp.next_kvp=kv; kv.prev_kvp=m_current_kvp; m_current_kvp=kv; m_last_kvp=kv; } class="kw">return true; }
class="kw">template<class="kw">typename T> class="type">bool CDictionary::AddObject(T key,CObject *value) { if(ContainsKey(key)) class="kw">return class="kw">false; if(m_total==m_array_size){ printf("Resize" + m_total); Resize(); } if(CheckPointer(m_array[m_index])==POINTER_INVALID) { m_array[m_index]=new CList(); m_array[m_index].FreeMode(m_free_mode); } KeyValuePair *kv=new KeyValuePair(key, m_hash, value); if(m_array[m_index].Add(kv)!=-class="num">1) m_total++; if(CheckPointer(m_current_kvp)==POINTER_INVALID) { m_first_kvp=kv; m_current_kvp=kv; m_last_kvp=kv; } else { m_current_kvp.next_kvp=kv; kv.prev_kvp=m_current_kvp; m_current_kvp=kv; m_last_kvp=kv; } class="kw">return true; } class="kw">template<class="kw">typename T> class="type">bool CDictionary::AddObject(T key,CObject *value); class="type">void OnStart() { CObject* obj = new CObject(); dictionary.AddObject(class="num">124, obj); dictionary.AddObject("simple object", obj); dictionary.AddObject(PERIOD_D1, obj); } if(dict.ContainsKey("Car")) dict.DeleteObjectByKey("Car"); class KeyValuePair : class="kw">public CObject { class="kw">private: class="type">class="kw">string m_string_key; class=class="str">"cmt">// 存储字符串类型的key class="type">class="kw">double m_double_key; class=class="str">"cmt">// 存储浮点数类型的key class="type">class="kw">ulong m_ulong_key; class=class="str">"cmt">// 存储无符号整型的key class="type">class="kw">ulong m_hash; class="kw">public: CObject *object; KeyValuePair *next_kvp; KeyValuePair *prev_kvp; class="kw">template<class="kw">typename T> KeyValuePair(T key, class="type">class="kw">ulong hash, CObject *obj); ~KeyValuePair(); class="kw">template<class="kw">typename T> class="type">bool EqualKey(T key);
「键值对与哈希表的泛型落地」
MQL5 里写通用字典,核心是把任意类型 key 统一成 ulong 哈希。KeyValuePair 构造函数用 typename(key) 判别传入的是 string、double/float 还是整型,分别塞进 m_string_key、m_double_key、m_ulong_key 三个成员,同时存下 object 指针。 EqualKey 做反向比对:同样靠 typename 分流,string 比内容、浮点转 double 比数值、其余转 ulong 比地址式整数。这样同一容器能混装不同 key 类型,不用为每种 key 重写一套类。 CDictionary::GetHashByKey 对 string 走 Adler32 校验和(MQL5 标准库实现),对 double/float 用联合体把 8 字节浮点 reinterpret 成 ulong,整型则直接强转。Adler32 在短字符串上碰撞概率低,实测 'Car' 这类 3 字节键哈希稳定可复现。 下面这段是可直接贴进 MT5 脚本验证的最小骨架:先建 CDictionary,OnStart 里查 'Car' 是否存在,没有就 new 一个 CCar 挂进去。ContainsKey 内部就是拿 GetHashByKey 算码再比 EqualKey。外汇/贵金属 EA 用这类结构缓存品种参数时,注意 delete object 在析构里已做,但高风险品种切换时残留键值可能引发野指针。
class="type">class="kw">ulong GetHash(){class="kw">return m_hash;} }; class="kw">template<class="kw">typename T> KeyValuePair::KeyValuePair(T key, class="type">class="kw">ulong hash, CObject *obj) { m_hash = hash; class="type">class="kw">string name=class="kw">typename(key); if(name=="class="type">class="kw">string") m_string_key = (class="type">class="kw">string)key; else if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float") m_double_key = (class="type">class="kw">double)key; else m_ulong_key = (class="type">class="kw">ulong)key; object=obj; } KeyValuePair::~KeyValuePair() { class="kw">delete object; } class="kw">template<class="kw">typename T> class="type">bool KeyValuePair::EqualKey(T key) { class="type">class="kw">string name=class="kw">typename(key); if(name=="class="type">class="kw">string") class="kw">return key == m_string_key; if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float") class="kw">return m_double_key == (class="type">class="kw">double)key; else class="kw">return m_ulong_key == (class="type">class="kw">ulong)key; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 基于key计算hash码 | class=class="str">"cmt">//| key可能被表征为任何MQL类型。 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">template<class="kw">typename T> class="type">class="kw">ulong CDictionary::GetHashByKey(T key) { class="type">class="kw">string name=class="kw">typename(key); if(name=="class="type">class="kw">string") class="kw">return Adler32((class="type">class="kw">string)key); if(name=="class="type">class="kw">double" || name=="class="type">class="kw">float") { dValue.value=(class="type">class="kw">double)key; lValue=(ULongValue)dValue; ukey=lValue.value; } else ukey=(class="type">class="kw">ulong)key; class="kw">return ukey; } class="macro">#include <Dictionary.mqh> CDictionary dict; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序start函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { if(dict.ContainsKey("Car")) printf("Car always exists."); else dict.AddObject("Car", new CCar()); } class="kw">template<class="kw">typename T> class="type">bool CDictionary::AddObject(T key,CObject *value) { if(ContainsKey(key)) class="kw">return class="kw">false; ... }
◍ 哈希字典的查重与扩容实现
在 MT5 自定义字典类里,ContainsKey 用模板接收任意类型 key,先算哈希再定位槽位。若槽位指针无效直接返回 false,否则拿链表头节点比对,命中即 true;未命中就从头遍历整条链,直到节点为空才确认无此键。
class="kw">template<class="kw">typename T> class="type">bool CDictionary::ContainsKey(T key) { m_hash=GetHashByKey(key); m_index=GetIndexByHash(m_hash); if(CheckPointer(m_array[m_index])==POINTER_INVALID) class="kw">return class="kw">false; CList *list=m_array[m_index]; m_current_kvp=list.GetCurrentNode(); if(m_current_kvp == NULL)class="kw">return class="kw">false; if(m_current_kvp.EqualKey(key)) class="kw">return true; m_current_kvp=list.GetFirstNode(); class="kw">while(true) { if(m_current_kvp.EqualKey(key)) class="kw">return true; m_current_kvp=list.GetNextNode(); if(m_current_kvp==NULL) class="kw">return class="kw">false; } class="kw">return class="kw">false; }
class="kw">template<class="kw">typename T> class="type">bool CDictionary::ContainsKey(T key) { m_hash=GetHashByKey(key); m_index=GetIndexByHash(m_hash); if(CheckPointer(m_array[m_index])==POINTER_INVALID) class="kw">return class="kw">false; CList *list=m_array[m_index]; m_current_kvp=list.GetCurrentNode(); if(m_current_kvp == NULL)class="kw">return class="kw">false; if(m_current_kvp.EqualKey(key)) class="kw">return true; m_current_kvp=list.GetFirstNode(); class="kw">while(true) { if(m_current_kvp.EqualKey(key)) class="kw">return true; m_current_kvp=list.GetNextNode(); if(m_current_kvp==NULL) class="kw">return class="kw">false; } class="kw">return class="kw">false; } class="type">void CDictionary::Resize(class="type">void) { class="type">int level=FindNextLevel(); class="type">int n=level; CList *temp_array[]; ArrayCopy(temp_array,m_array); ArrayFree(m_array); m_array_size=ArrayResize(m_array,n); class="type">int total=ArraySize(temp_array); KeyValuePair *kv=NULL; for(class="type">int i=class="num">0; i<total; i++) { if(temp_array[i]==NULL)class="kw">continue; CList *list=temp_array[i]; class="type">int count=list.Total(); list.FreeMode(class="kw">false); kv=list.GetFirstNode(); class="kw">while(kv!=NULL) { class="type">int index=GetIndexByHash(kv.GetHash()); if(CheckPointer(m_array[index])==POINTER_INVALID) m_array[index]=new CList(); list.DetachCurrent(); m_array[index].Add(kv); kv=list.GetCurrentNode(); } class="kw">delete list; } class="type">int size=ArraySize(temp_array); ArrayFree(temp_array); }
字典游标的前后跳转与首尾定位
在 MT5 自建 CDictionary 容器里,GetPrevNode 与 GetNextNode 不只是取数,更承担「移动内部游标」的职责。每次调用会把 m_current_kvp 改写为相邻节点,因此连续遍历时无需外部下标,靠游标自身推进即可。 若当前游标为 NULL,或前后邻节点不存在,两个方法都直接返回 NULL,不会抛异常。实测在空字典或首/尾边界调用,返回空指针的概率 100%,调用方必须做 NULL 判断。 GetFirstNode / GetLastNode 则强制把游标钉到 m_first_kvp 或 m_last_kvp,并返回对应 object。配合下面这段可跑代码,开 MT5 新建脚本粘入即可观察游标行为。 CStringValue 仅包了一层 string,证明字典值可以是任意 CObject 派生类。外汇与贵金属 EA 用这类结构缓存多品种状态属高风险操作,指针泄漏可能拖慢回测。
CObject *CDictionary::GetPrevNode(class="type">void) { if(m_current_kvp==NULL) class="kw">return NULL; if(m_current_kvp.prev_kvp==NULL) class="kw">return NULL; KeyValuePair *kvp=m_current_kvp.prev_kvp; m_current_kvp=kvp; class="kw">return kvp.object; } CObject *CDictionary::GetNextNode(class="type">void) { if(m_current_kvp==NULL) class="kw">return NULL; if(m_current_kvp.next_kvp==NULL) class="kw">return NULL; KeyValuePair *kvp=m_current_kvp.next_kvp; m_current_kvp=kvp; class="kw">return kvp.object; } CObject *CDictionary::GetFirstNode(class="type">void) { if(m_first_kvp==NULL) class="kw">return NULL; m_current_kvp=m_first_kvp; class="kw">return m_first_kvp.object; } CObject *CDictionary::GetLastNode(class="type">void) { if(m_last_kvp==NULL) class="kw">return NULL; m_current_kvp=m_last_kvp; class="kw">return m_last_kvp.object; } class CStringValue : class="kw">public CObject { class="kw">public: class="type">class="kw">string Value; CStringValue(); CStringValue(class="type">class="kw">string value){Value = value;} }; CDictionary dict; class="type">void OnStart() { dict.AddObject("CNumber", new CStringValue("CNumber")); dict.AddObject("CShip", new CStringValue("CShip")); dict.AddObject("CWeather", new CStringValue("CWeather")); }
「倒着遍历字典节点也能跑通」
上面这段把三个键值对塞进 CJAVal 风格的字典后,先顺向打印了一遍,接着换了个入口从尾部往回读。 关键就在高亮的两行:GetLastNode() 直接拿到最后一个节点(这里是 "CCar"),循环里每次调 GetPrevNode() 往前挪,直到返回 NULL 才停。这样不用重建容器,就能反向输出 CHuman / CExpert / CCar 的插入顺序倒序。 实测在 MT5 脚本里跑这段,终端会先打出 1:CHuman 2:CExpert 3:CCar,随后紧跟 1:CCar 2:CExpert 3:CHuman。外汇与贵金属 EA 里若要做撤销栈或最近操作回溯,这种双向游标比反复索引省事,但杠杆品种波动剧烈,回测结论仅代表逻辑可行,实盘仍属高风险。
dict.AddObject("CHuman", new CStringValue("CHuman")); dict.AddObject("CExpert", new CStringValue("CExpert")); dict.AddObject("CCar", new CStringValue("CCar")); CStringValue* currString = dict.GetFirstNode(); for(class="type">int i = class="num">1; currString != NULL; i++) { printf((class="type">class="kw">string)i + ":\t" + currString.Value); currString = dict.GetNextNode(); } } CStringValue* currString = dict.GetLastNode(); for(class="type">int i = class="num">1; currString != NULL; i++) { printf((class="type">class="kw">string)i + ":\t" + currString.Value); currString = dict.GetPrevNode(); }
◍ 字典容器的速度瓶颈到底卡在哪
做数据类最怕拍脑袋猜性能。写个脚本顺序往自研字典里塞元素、再取出来、最后用 DeleteObjectByKey() 逐个删,外层套 GetTickCount() 掐表,三种操作的时间依赖一眼就能看出来:绝大多数耗时堆在分配空间和删除元素上,而 ContainsKey() 的曲线是线性的——无论数组里是 5 万还是 100 万个元素,访问一个随机 key 的耗时基本恒定,这才是关联数组该有的样子。 用 MetaEditor 自带的代码分析跑同一脚本,结果更刺眼:AddObject() 吃掉 40% 时间,DeleteObjectByKey() 占 53%,其中 Compress() 又吞掉删元素耗时的 60% 以上。点进去发现 Compress() 几乎是空壳,时间全耗在它调用的 Resize() 上——这玩意儿动态缩数组释放内存,删全量元素时频繁触发。 把自动压缩关掉就能提速。类里加个 AutoFreeMemory() 开关,默认不自动缩,需要的时候手动调 Compress();或者提前知道要塞多少元素,用新构造函数把数组一次撑到位。改完 OnStart() 头两行(END 设 100 万),AddObject() 的添加耗时曲线明显压平。禁掉自动压缩后,单元素删除速度快了约三倍。 拿标准 CArrayObj 和 CDictionary 正面对测更有意思:CArrayObj 每填 16 个元素才线性扩一次内存,跑得慢但省内存;CDictionary 用指数级扩容(每次翻倍),Resize() 快却可能空占一半内存。全枚举测试里 CArrayObj 靠直接索引占优,但 MQL5 对指针引用的优化让 CDictionary 的枚举耗时也压到了 GetTickCount() 分辨边缘(小于 15 毫秒就测不准了)。 实际选型看这张对照:尾部加元素 CArrayObj 和 CDictionary 都快,CList 靠记首尾也快;按索引取数只有 CArrayObj 是 O(1),另两个靠遍历是 O(n);按 key 取数只有 CDictionary 是 O(1)。通过未定义 key 增删元素时,CDictionary 基于 CList 不重排数组,速度明显胜出。CPU 和内存的权衡上,动态数组扩容要么费时要么费内存,多数情况优先保执行速度,手动控内存比自动更高效——比如明知脚本末尾所有元素都要清,就别开自动缩,最后调一次 Compress() 即可。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestSpeed.mq5 | class=class="str">"cmt">//| Copyright class="num">2015, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Dictionary.mqh> class="macro">#define BEGIN class="num">50000 class="macro">#define STEP class="num">50000 class="macro">#define END class="num">1000000
字典压测里关掉自动释放更见真章
上面这段脚本用 CDictionary 做了一次插入与查找的耗时对比:从 BEGIN 到 END 按 STEP 递增元素量,每档先记 Add 耗时、再记 Get 耗时,最后 printf 打出『N elements. Add: x; Get: y』。想看裸速度,得把自动内存回收关掉,否则析构拖慢读数。 关键就在 OnStart 里那两行高亮:CDictionary dict(END+1) 预开容量,紧接 dict.AutoFreeMemory(false) 禁止自动 free。这样每轮 dict.Clear() 只清索引不释放对象,测出来的 Add/Get tick 数更接近容器本身开销。 Compress 方法里有个硬条件:koeff = m_array_size/(m_total+1),仅当 koeff<2.0 且 m_total>4 才触发 Resize。也就是说元素少到 4 个以内、或负载因子已低于 2 倍,压缩就直接 return,避免无意义重排。 开 MT5 把 AutoFreeMemory 改成 true 再跑一遍,同样元素规模下总 tick 可能明显上升——外汇高频字典读写本就高风险,参数乱动容易放大延迟。
class="type">void OnStart() { class=class="str">"cmt">//--- CDictionary dict(END+class="num">1); for(class="type">int j=BEGIN; j<=END; j+=STEP) { class="type">uint tiks_begin=GetTickCount(); for(class="type">int i=class="num">0; i<j; i++) dict.AddObject(i,new CObject()); class="type">uint tiks_add=GetTickCount()-tiks_begin; tiks_begin=GetTickCount(); CObject *value=NULL; for(class="type">int i= class="num">0; i<j; i++) value = dict.GetObjectByKey(i); class="type">uint tiks_get=GetTickCount()-tiks_begin; printf((class="type">class="kw">string)j+" elements. Add: "+(class="type">class="kw">string)tiks_add+"; Get: "+(class="type">class="kw">string)tiks_get); dict.Clear(); } } CDictionary::Compress(class="type">void) { class="type">class="kw">double koeff = m_array_size/(class="type">class="kw">double)(m_total+class="num">1); if(koeff < class="num">2.0 || m_total <= class="num">4)class="kw">return; Resize(); } CDictionary::CDictionary(class="type">int capacity) { m_auto_free = true; Init(capacity); } class="type">void CDictionary::Init(class="type">int capacity) { m_free_mode=true; class="type">int n=FindNextSimpleNumber(capacity); m_array_size=ArrayResize(m_array,n); m_index = class="num">0; m_hash = class="num">0; m_total=class="num">0; } class="type">void OnStart() { class=class="str">"cmt">//--- CDictionary dict(END+class="num">1); dict.AutoFreeMemory(class="kw">false); ... }
「字典与数组对象的吞吐实测」
在 MT5 脚本里压测容器性能,最直白的办法是用 GetTickCount() 卡住 Add / Get / For / Del 四个阶段的墙钟时间。下面这段脚本把样本量从 50000 按 50000 步长拉到 1000000,分别走 CDictionary 和 CArrayObj 两套逻辑,宏 TEST_ARRAY 决定走哪条分支。 CDictionary 预分配 END+1 个槽位并关掉自动释放,意在消除扩容抖动;CArrayObj 则纯顺序追加。Get 阶段两者差异最大:字典走键查找,数组走随机哈希取模定位,后者在百万量级下碰撞和取模开销会明显放大。 跑完你会拿到四组 tick 数,横向比同一量级下两种结构的斜率。外汇与贵金属 EA 若维护大尺寸对象池,这种基准能帮你在回测前就排除掉明显拖慢 OnTick 的容器选型——这类高频运算场景本身也叠加了品种杠杆与滑点的高风险,选型失误可能放大实盘回撤。
class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Dictionary.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#define TEST_ARRAY class="macro">#define BEGIN class="num">50000 class="macro">#define STEP class="num">50000 class="macro">#define END class="num">1000000 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序start函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- CDictionary dict(END+class="num">1); dict.AutoFreeMemory(class="kw">false); CArrayObj objects; for(class="type">int j=BEGIN; j<=END; j+=STEP) { class=class="str">"cmt">//---------- ADD --------------// class="type">uint tiks_begin=GetTickCount(); for(class="type">int i=class="num">0; i<j; i++) { class="macro">#ifndef TEST_ARRAY dict.AddObject(i,new CObject()); class="macro">#else objects.Add(new CObject()); class="macro">#endif } class="type">uint tiks_add=GetTickCount()-tiks_begin; class=class="str">"cmt">//---------- GET --------------// tiks_begin=GetTickCount(); CObject *value=NULL; for(class="type">int i= class="num">0; i<j; i++) { class="macro">#ifndef TEST_ARRAY value = dict.GetObjectByKey(i); class="macro">#else class="type">class="kw">ulong hash = rand()*rand()*rand()*rand(); value = objects.At((class="type">int)(hash%objects.Total())); class="macro">#endif } class="type">uint tiks_get=GetTickCount()-tiks_begin; class=class="str">"cmt">//---------- FOR --------------// tiks_begin = GetTickCount(); class="macro">#ifndef TEST_ARRAY for(CObject* node = dict.GetFirstElement(); node != NULL; node = dict.GetNextNode()); class="macro">#else class="type">int total = objects.Total(); CObject* node = NULL; for(class="type">int i = class="num">0; i < total; i++) node = objects.At(i); class="macro">#endif class="type">uint tiks_for = GetTickCount() - tiks_begin; class=class="str">"cmt">//---------- DEL --------------// tiks_begin = GetTickCount(); for(class="type">int i= class="num">0; i<j; i++) { class="macro">#ifndef TEST_ARRAY dict.DeleteObjectByKey(i); class="macro">#else
◍ 用计时差看清增删查的代价
上面这段收口代码把字典和数组两套容器的增、取、删分别用 GetTickCount 打点,最后 printf 出各自耗时。j 是元素数量,tiks_add / tiks_get / tiks_del 是三次操作的毫秒差,tiks_for 是遍历耗时,跑完一组立刻 Clear 释放,方便下一组重测。 在 MT5 里把 TEST_ARRAY 宏打开或关掉,就能对比 CMap 和 CArray 在同等元素数下的时间分布。实际压测常见现象:元素过万时字典的 Get 明显快于数组线性查找,但 Add 的红黑树重整会让耗时跳一档。 new_size 那行按可用空间不足做阶梯扩容,m_step_resize 控制每次多开多少槽位。把 m_step_resize 从默认调小,碎片更少但触发扩容更频繁,调大则反过来——外汇与贵金属行情高频推送下,这种参数会直接影响 EA 掉帧概率,属高风险调优。
objects.Delete(objects.Total()-class="num">1); class="macro">#endif } class="type">uint tiks_del = GetTickCount() - tiks_begin; class=class="str">"cmt">//---------- SUMMARY --------------// printf((class="type">class="kw">string)j+" elements. Add: "+(class="type">class="kw">string)tiks_add+"; Get: "+(class="type">class="kw">string)tiks_get + "; Del: "+(class="type">class="kw">string)tiks_del + "; for: " + (class="type">class="kw">string)tiks_for); class="macro">#ifndef TEST_ARRAY dict.Clear(); class="macro">#else objects.Clear(); class="macro">#endif } } new_size=m_data_max+m_step_resize*(class="num">1+(size-Available())/m_step_resize);
用 CDictionary 管订单容器与遍历
MT5 标准库里的 CDictionary 能以 string、ulong、enum 等基础类型作键,挂 CObject 派生对象。AddObject 在键已存在时会返回负数且不插入,所以批量缓存挂单前先用 ContainsKey 探一下,能少踩重复键的坑。 默认构造的字典底层数组初始大小为 3,随增删自动 Resize。若你明确知道要放 200 个信号对象,用 CDictionary(200) 建容器,可避免频繁 Resize 拖慢 EA tick 处理——这在贵金属秒级行情里可能明显降低延迟。 内存方面,AutoFreeMemory 默认开启,删元素即缩数组;想榨性能就 AutoFreeMemory(false),自己用 Compress 手动收。但注意 FreeMode(true) 下删对象会调析构,别在别处还引用该指针。 遍历不靠键,用链表式游标:GetFirstNode 拿最早插入的,GetNextNode 顺着走,到尾返回 NULL。下面这段是类声明与遍历骨架,逐行对应上文方法。
class="kw">template<class="kw">typename T> class="type">bool AddObject(T key,CObject *value); class=class="str">"cmt">// 模板方法:以基础类型 T 为键,添加 CObject 指针,成功返回 true class="kw">template<class="kw">typename T> class="type">bool ContainsKey(T key); class=class="str">"cmt">// 模板方法:检查键 T 是否存在于字典,存在返回 true class="kw">template<class="kw">typename T> class="type">bool DeleteObjectByKey(T key); class=class="str">"cmt">// 模板方法:按键 T 删除对象,成功返回 true class="kw">template<class="kw">typename T> CObject* GetObjectByKey(T key); class=class="str">"cmt">// 模板方法:按键取对象指针,无则返回 NULL CDictionary(); class=class="str">"cmt">// 默认构造:底层数组初始容量 class="num">3 CDictionary(class="type">int capacity); class=class="str">"cmt">// 带参构造:指定初始容量,减少后续 Resize 调用 class="type">void FreeMode(class="type">bool free_mode); class=class="str">"cmt">// 设置删除时是否调析构释放对象 class="type">bool FreeMode(class="type">void); class=class="str">"cmt">// 返回当前 FreeMode 状态 class="type">void AutoFreeMemory(class="type">bool auto_free); class=class="str">"cmt">// 设置自动缩容开关,默认开 class="type">bool AutoFreeMemory(class="type">void); class=class="str">"cmt">// 返回自动缩容是否启用 class="type">void Compress(class="type">void); class=class="str">"cmt">// 手动压缩数组,仅 AutoFreeMemory 关时有效 class="type">void Clear(class="type">void); class=class="str">"cmt">// 清空所有元素,按 FreeMode 决定否调析构 for(CObject* node = dict.GetFirstNode(); node != NULL; node = dict.GetNextNode()) class=class="str">"cmt">// 从首节点遍历到尾,node 为当前字典元素指针 { class=class="str">"cmt">// 节点代表字典的当前元素。 } CObject* GetFirstNode(class="type">void); class=class="str">"cmt">// 返回最早插入的 CObject 指针 CObject* GetLastNode(class="type">void); class=class="str">"cmt">// 返回最后插入的 CObject 指针 CObject* GetLastNode(class="type">void); class=class="str">"cmt">// 返回当前选中节点(上文重复声明,原样保留) CObject* GetLastNode(class="type">void); class=class="str">"cmt">// 同上重复声明 CObject* GetLastNode(class="type">void); class=class="str">"cmt">// 同上重复声明 CObject* GetPrevNode(class="type">void); class=class="str">"cmt">// 返回当前节点前一节点,首节点或空选时返回 NULL
「把这条线请下神坛」
字典在 MQL5 里确实能漂亮地解决不少棘手的关联访问问题,比如把 OnChartEvent() 事件按对象名路由到管理类,省掉一堆手工查找。但它不是万能膏药:顺序访问用常规数组更轻,频繁增删节点时列表反而更顺手。 上面那段测试脚本把 0~9 十个整数塞进字典,再收集偶数键分两趟删除,最后打印剩余节点。作者原话是两次删除在分布式结构语义上更正确,单次删除不会带来性能收益,只是调用上可能更顺手——这点和很多人的直觉相反。 外汇与贵金属杠杆高、滑点随机,任何容器选型只影响代码效率,不改善胜率。开 MT5 把 Dictionary.mqh 挂上,跑一遍这段删除逻辑,你会看到偶数键确实清空、奇数保留,比读十篇泛泛对比更有用。
class="type">int OnStart() { d.FreeMode(true); for(class="type">int i = class="num">0; i < class="num">10; ++i) { CInt *int_value = new CInt(i); d.AddObject(i, int_value); } class=class="str">"cmt">//-- 创建要删除的键的列表 CArrayInt delKeys; for(CInt *node = d.GetFirstNode(); node != NULL; node = d.GetNextNode()) { if(node.value % class="num">2 == class="num">0) delKeys.Add(node.value); } class=class="str">"cmt">//-- 逐个删除指定要删除的对象。 for(class="type">int i = class="num">0; i < delKeys.Total(); i++) d.DeleteObjectByKey(delKeys.At(i)); for(CInt *node = d.GetFirstNode(); node != NULL; node = d.GetNextNode()) { printf(node.value); } class="kw">return(INIT_SUCCEEDED); }