背景:
之所以要使用Dic跨公式分项数据的原因和遇到的问题请看下面两个帖子:
一顿敲敲打打终于把分析行情结构的策略公式写好了,测试了下通过Dic分享给其它独立的策略公式,然后遇到了意想不到的性能问题。原以为只是用非持久化Dic存储一些Numeric数据,过过内存分发下,overheads应该不会很高,结果性能大幅下降,尤其是内存使用从百兆级别直接冲到10G+。
问题重现:
因为实际的策略比较复杂,跑起来需要多个用户函数,所以就写了一个简单的策略文件来复现问题,在运行时请使用ag888这个合约,策略单元选择1分钟,范围从最近10天开始,具体代码和测试和测试对照组说明都在下面的代码里:
//------------------------------------------------------------------------
// 简称: sbx_perf_datawrite
// 名称:
// 类别: 策略应用
// 类型: 用户应用
// 输出: Void
//------------------------------------------------------------------------
Params
//此处添加参数
Vars
Global Array<Array<Numeric>> dataArray;
Dic<Array<Array<Numeric>>> Dic_TestPerfData("Test_Perf", False, "Data");
Global Numeric dic_write_time(19250127.110211); //用于确保每个dic内只有一条记录的系统时间
Defs
//此处添加策略函数
Events
//此处实现事件函数
//初始化事件函数,策略运行期间,首先运行且只有一次
OnInit()
{
PrintClear();
}
OnBar(ArrayRef<Integer> indexes)
{
Array<Numeric> newData;
Integer i;
// 生成一个数组newData,然后存放在二维数组dataArray里
for i = 0 to 23
newData[i] = Rand(100, 10000);
ArrayPushBack(dataArray,newData);
If(BarStatus == 2)
Print(Text(GetArraySize(dataArray)));
/*
Dic性能三个测试组(每次测试都是关闭TB3,在任务管理器里看不到TB3进程后重新启动再运行):
1. 不更新Dic变量:将第48,49行都注释掉 (100个交易日的5w根1分钟K线, TB3内存使用300M左右, CPU使用最高6%)
2. 确保Dic变量里只保存一份记录: 注释掉48行,保留49行 (10个交易日的5500根1分钟K线, 运行后TB3内存在恢复到300M前使用峰值可达15G以上, CPU使用可达25%)
3. 用独立时间戳更新Dic变量: 注释掉49行,只保留48行 (10个交易日的5500根1分钟K线, 运行后TB3内存在恢复到300M前使用峰值可达15G以上, CPU使用可达25%)
*/
//Dic_TestPerfData = dataArray;
//SetDicValue("Test_Perf", "Data", dic_write_time, dataArray, False);
/*
使用event分享数据,内存使用量比Dic大幅下降,但是CPU使用很高,策略运行很慢
*/
//Map<String, String> msg;
//msg["newData"] = TextArray(newData);
//PublishEvent("data-change", msg);
}
//------------------------------------------------------------------------
// 编译版本 2025/2/4 85714
// 版权所有 tb274345
// 更改声明 TradeBlazer Software保留对TradeBlazer平台
// 每一版本的TradeBlazer策略修改和重写的权利
//------------------------------------------------------------------------
问题:
目前看因为这个性能瓶颈,想按照预期行情分析和策略代码分开编写是不太现实了,只能把代码都堆到一个策略公式里或者想办法降低对Dic更新的频率,只分享尽可能少量的数据。 我的问题是:
#1 实盘交易时策略会一直跑下去,以上面的代码为例,不使用Dic的前提下在代码里定期删除dataArray中的一些老数据,应该会降低内存使用量吧?
#2 实盘运行时还有什么办法降低内存使用量?
“测试了下通过Dic分享给其它独立的策略公式,然后遇到了意想不到的性能问题。原以为只是用非持久化Dic存储一些Numeric数据,过过内存分发下,overheads应该不会很高,结果性能大幅下降,尤其是内存使用从百兆级别直接冲到10G+。”?
怎样的测试环境可以到10G+?一个策略单元写,多少策略单元收?
就用我贴的这个代码,只要写就可以了,不需要用单元去收,可以设置到一分钟级就用ag888, 然后你把时间设为过去200天(从小一点的开始设起),这样大概就会有两万根k线,会看到内存消耗巨大。
(1)如果你的【行情分析策略】与【交易决策策略】在同一个【策略单元】里面,那么在【行情分析策略】里面使用【SetGlobalVar】按数组序号发送【行情分析结果数据】,然后在【交易决策策略】里面使用【GetGlobalVar】按序号接收【行情分析结果数据】存放到接收数组中。这个操作不会消耗太多CPU性能资源,实现也比较容易。
(2)如果你的【行情分析策略】与【交易决策策略】在不同的【策略单元】里面。那么在【行情分析策略】里面使用【PublishEvent】来发送数组数据,但是在发送之前,先把数值型数据转成字符串加分隔符的形式,也就是进行数据序列化操作,才能发送【行情分析结果数据】。然后在【交易决策策略】初始化时要先【SubscribeEvent】订阅【发送的数据】,然后在【OnEvent】里面,首先接收【序列化的数据】,然后使用【StringSplit】分割字符串,然后转成【数值型】数据类型,最后存放到【接收数组】中。这个操作会消耗一点点CPU性能资源,实现难度适中。这个解决方案中有可能会存在偶尔几个Tick分析数据发送失败或没有接收到的情况,所以在【交易决策策略】中考虑这种情况的存在,但是好在Tick时间期货500ms、股票好像是3秒,一般数据丢失对策略决策影响的危害不大。
(3)关于Dic数据设计之初,就不是为了实盘Tick级别进行数据读写共享的,所以你最初想法本身就是有问题的。Dic数据是以截面(时间点)形式 保存写入,以截面形式来读取的。大多数情况下,这个截面时间点可能是周期性的(Bar数据的开、高、低、收、成交量、成交额)、也可能是非周期性的(基本面数据、除权换月数据),如果你费用使用Dic来发送共享数据,那至少应该将【SetDicValue】放到【OnBarOpen】或【OnBarClose】里面,而不是在【OnBar】里面。
(4)以上谈的是 CPU执行【时间】的性能消耗问题。下面谈谈内存【空间】的消耗问题。根据你自己的描述,只需要当下最新的数据,那么你的数组在定义的时候【Global Array<Array<Numeric>> dataArray;】可以把【Global】删除,因为全局类型,在整个【策略公式】运行期间是常驻内存,【OnBar】执行完后,不会释放内存空间,到下一次【OnBar】执行时,是基于前一次【OnBar】的基础上继续执行的。所以将【Global】删除后,数组从【全局数组】变成【普通数组】后,每次执行完【OnBar】后,操作系统会自动释放【数组占用的内存空间】。另外还有一种更加笨一点但是更加保险的方法,就是每次在ArrayPushBack之前都执行一次ArrayClear,这样即使是全局数组,每次也只保留最新的数据,删除历史数据。
ArrayClear(dataArray);
ArrayPushBack(dataArray,newData);
老师您好,非常感谢老师的详细回复,我今天仔细思考了下,也拜读了您列出的几条建议,我觉得问题的关键是我是在历史Bar上每次做完分析就把分析的结果进行分发,这个会造成短时间内大量的数据要传递,合理的做法应该是等所有的历史Bar处理完,发一次,以后在每个OnBarClose里分发一次,即使我用的是多个周期的数据源,每次分发的压力都会小很多,无论是用Dic还是自定义事件应该都可以,另外可以定期删除一些用不上的历史数据,这样每次传递数据的时候数据量也可以小一些。以下是关于您4条建议的几点想法,交流学习一下:
(1)SetGlobalVar和SetGlobalVar2 这两个都只能存放Numeric数据,我要传递的数据是一维和二维数组
(2) 这个是我最初尝试的方式,用PublishEvent我发送的是增量数据,接收方接收后自己更新本地的数据写了个PoC验证了下确实可行,后来了解到Dic可以直接保存数组,就决定用Dic,想着可以省点事情。
(3) 您的看法我非常认同,确实不适合用在Tick级别。事实上实际的代码是放在OnBarClose里的。分析结果只需要在对应级别的Bar Close时更新一次即可。帖子里这个是用于复现问题,所以写的时候就用了OnBar。目前问题主要是出在加载历史Bar的时候,售后我会修正下代码只在最后一个历史Bar同步一次数据,而不是在每个历史bar上都做,然后对于实时数据就在每个Bar Close时更新。
(4) 关于内存这个,您提醒了我一点就是可以定期删除一些历史数据。让我觉得最吃惊的是因为保存的数据都是Numeric, 不更新到Dic的时候几万个K线多个周期随便跑都存储消耗都很小(整个进程也就300M,增量很少),但是一旦更新Dic且在不持久化的情况下, 哪怕只是10日的1分钟数据(白银这种夜盘时间长的,也就5500条)进程的内存就会增长到15G,着实远超我的预期。估计是Dic的设计本身就不是简单的存储,我的用法不太适合。
再次感谢老师的耐心回复,谢谢!
>(1)SetGlobalVar和SetGlobalVar2 这两个都只能存放Numeric数据,我要传递的数据是一维和二维数组
这不是容易处理的数据结构,以下代码:我还专门留心告诉你使用【SetGlobalVar】和【GetGlobalVar】没说使用【SetGlobalVar2】和【GetGlobalVar2】,就是为了这里处理数组下标特别方便。
//【发送端策略】使用以下代码:
For i = 0 To GetArraySize(newData) - 1{
SetGlobalVar(i, newData[i]);
}
//【接收端策略】使用以下代码:
ArrayClear(Received1WArr);
For i = 0 To 23{
ArrayPushBack(Received1WArr, GetGlobalVar(i));
}
ArrayPushBack(Received2WArr, Received1WArr);
>(4) 关于内存这个,您提醒了我一点就是可以定期删除一些历史数据。让我觉得最吃惊的是因为保存的数据都是Numeric, 不更新到Dic的时候几万个K线多个周期随便跑都存储消耗都很小
那是因为,你不了解《微机原理》,更深入一步讲,当你对dic类型数据进行读写操作的时候,编译以后的机器码其实是对【硬盘】进行物理读写操作,而你使用【SetGlobalVar】和【GetGlobalVar】是对【内存】进行物理读写操作。前者还有一个叫法对数据库进行读写操作,后者才是真正地对内存变量进行读写操作。两者读写速度,对CPU的中断等待读写控制难度不是n个级别的差距,以及对于CPU资源的消耗也是差距很大的。所以在代码里面,虽然形式上一样都是定义了 给代码进行读写操作的【变量名】:【Dic_TestPerfData】和【VarArr】,但是到了汇编层面,是对2个完全不同的物理存储器在进行读写操作。变量名只是一个符号,背后映射的存储器是不同的。
//定义Dic类型的变量 Dic_TestPerfData
Dic<Array<Array<Numeric>>> Dic_TestPerfData("Test_Perf", False, "Data");
//定义数组变量 VarArr
Array<Numeric> VarArr;
顺便说一句,所以你只要跑过一次策略程序,结果数据一旦保存成功后,即使关机,再开机, 你不用跑第二遍程序,只要打开数据中心,Dic_TestPerfData变量的结果依然可以看见,因为这些数据保存在物理的【硬盘】里面,业务名叫【写入数据库】。
谢谢老师的进一步说明
(1)【SetGlobalVar】我用的少,若不是老师提醒我都忽视了用这个就可以在同一个策略单元里跨策略传递数值信息,借这个机会向老师学习下【SetGlobalVar】, 我需要传递的数据是个二维数组dataArray,参看贴子里的代码,摘录关键部分如下:
Global Array<Array<Numeric>> dataArray;
//Dic_TestPerfData = dataArray;
//SetDicValue("Test_Perf", "Data", dic_write_time, dataArray, False);
代码里把这两行注释掉了,是因为有三种情况可以复现对比(具体看代码里的注释),如果是想要传递的是二维数组,请问如何使用【SetGlobalVar】来实现呢?
(4)我用Dic的时候设置的是非持久化(定义Dic的时候和写入数据时专门设置了False),运行后数据中心里是看不到的(这个之前独立测试过),我的这个场景不需要持久化,避免开了持久化写入Sqlite数据库使用硬盘IO会很影响性能,我只需要保存在内存里。为了避免Dic按照时间存入历史版本,也在代码里做了处理,只保存一份最新的数据。(可以参看本贴最上面关联帖子)
顺便说下:调整了下代码,改成经过若干个Bar保存下行情信息二维数据到Dic,性能大幅提升,应该是能满足我目前的需求了。
之前看文档和这个帖子 https://bbs.tbquant.net/thread/20250124112334529398
老师的回复我以为使用Dic如果不设置持久化,那么数据就只会在内存里,而不会经过硬盘,所以数据中心里看不见,但是今天看您给我解释了微机原理,刚才试着把持久化设为True,似乎运行性能差不多(尤其是磁盘IO使用),只是能在数据中心里看到值了。联想到这相似的性能损耗,难道不持久化的Dic其实也是要过数据库,只是不是数据中心的那个数据库?
>借这个机会向老师学习下【SetGlobalVar】, 我需要传递的数据是个二维数组dataArray
二维数组不就是降维的一维数组:
//【发送端策略】使用以下代码:
Global Array<Array<Numeric>> SendDataArray;
Integer i;
Integer j;
For i = 0 To GetArraySize(SendDataArray) - 1{
For j = 0 To GetArraySize(SendDataArray[i]) - 1{
SetGlobalVar(i * GetArraySize(SendDataArray[i]) + j, SendDataArray[i][j]);
}
}
SetGlobalVar(i * GetArraySize(SendDataArray[i]) + j + 1, InvalidNumeric);
//【接收端策略】使用以下代码:
Global Array<Array<Numeric>> ReceivedDataArray;
Integer i;
Integer j = 0;
Integer Arr1WSize = 24;
ArrayClear(ReceivedDataArray);
While(True){
For i = 0 To Arr1WSize - 1{
ReceivedDataArray[j][i] = GetGlobalVar(j * Arr1WSize + i);
}
j = j + 1;
If(GetGlobalVar(j * Arr1WSize) == InvalidNumeric) Break;
}
>(4)我用Dic的时候设置的是非持久化(定义Dic的时候和写入数据时专门设置了False)
感觉是杀鸡用牛刀,为了点醋专门跑去买肉、买葱、买菜、买面、生火、和面、醒面、包饺子。为了挖口井原本挖掘机就搞定了,但是用上了盾构机,搬运、物流、交通管制、仓储、施工、安装、调试、能源配合都用上了,做法上有点背离了工具适用的初衷。
老师的这个方法应该可以,受教了。这样就可以用SetGlobalVar来专门存储最重要的二维数组,然后其它的一些更新不那么频繁的一维数组,数值啥的就用Dic来做。谢谢启发👍👍
还得再请教一下老师,我要实现的策略会用到不同周期的数据,对于每个数据源都会生成一个表示行情结构的二维数组,而且每个数据源的二维数组尺寸都在动态变化,且各个图层的更新变化并不同步
1. 如果用【SetGlobalVar】这个应该是全局的唯一的数值,如果想要保留多个二维数组,估计要搭配着别的方式记录下每个二维数组的大小和存入顺序,然后在接收端逐个解出?
2. 如果想要定期从各个二维数组中移除用不到的一些历史数据,估计要在数据移除后当前的所有二维数组重新通过【SetGlobalVar】写入一次? (如果性能足够好且占用内存不多,也许就不用定期移除了)
测试了以下您建议的GetGlobalVar方法,内存使用方面确实明显要好于Dic, 只是运行速度要慢相当的多, CPU占用也更高,我还在权衡用哪种方法,只是把测试结果跟您说下供参考。
关于Global变量和Dic变量使用的差异,我发了个独立的帖子 ,有兴趣可以看看管理员的回复 https://bbs.tbquant.net/thread/20250205095341728714
再次感谢您的指点
建议你还是不要再权衡了,你的算法最初的方向就是错误的,这种大批量不断累加增长的二维数组,在接近实时周期上,还要跨公式复制级别的搬运,这种算法思路一开始就是错误的。离实际交易场景太远了,倒更像是一位数码测评博主对某个产品性能参数的测评。借用当下火热的DeepSeek的话题打个比方,你是钻到了堆算力、堆资源、只注重评分参数的牛角尖里面了,而完全忽视了的策略算法本身如何改变、如何优化,工程上有没有必要这样干,去解决交易本身真正的问题点。
先试试吧,目前看还行,实在不行就把公用代码复制粘贴到各个公式😅
试试用GetDicValue是不是好一些呢
现在只是写入就已经很慢了,我确实有一个策略在读取(代码没贴出来,因为只是写入就已经能体现出性能了),也测试用了GetDicValue这个函数,性能还是很差。
又试了一下,性能还是不行,读取策略代码如下,先加载帖子里写入的策略,再加入这个读取策略,功能没问题,就是特别消耗内存,执行也慢
//------------------------------------------------------------------------
// 简称: sbx_perf_dataread
// 名称:
// 类别: 策略应用
// 类型: 用户应用
// 输出: Void
//------------------------------------------------------------------------
Params
//此处添加参数
Vars
Dic<Array<Array<Numeric>>> Dic_TestPerfData("Test_Perf", False, "Data");
Defs
//此处添加策略函数
Events
//此处实现事件函数
//初始化事件函数,策略运行期间,首先运行且只有一次
OnInit()
{
}
// 和sbx_perf_datawrite一起加载入同一个策略单元,用于读取其写入到Dic中的数据
OnBar(ArrayRef<Integer> indexes)
{
Array<Array<Numeric>> values;
//获取基础数据
Bool ret = GetDicValue("Test_Perf", "Data", SystemDateTime, values);
Print(Text(GetArraySize(values)));
}