参数优化性能调优

来源:互联网 发布:linux 将文件打包解压 编辑:程序博客网 时间:2024/05/17 12:55

与众多的程序化交易软件类似,我们的产品也有一个强大的功能叫参数优化。然而,最近这个功能却被暴露出来性能问题,优化参数的穷举组合仅在2000个左右,优化线程数为20,每个线程负责执行一个策略,也就是说正常情况下执行100轮就执行完毕,但却在执行了10轮左右也就是执行了200个策略左右时,进度条就变得越来越缓慢。这种情况下最容易让人想到的原因无非有2个:CPU独占和内存泄漏。于是,采用jstack(命令行执行:jstack pid,如果内容太多还可以先输出到文件:jstack pid > tmp.txt)先看一下堆栈情况,果不其然,有2个线程在执行java.util.HashMap.get()时被阻塞(BLOCKED)了,看到这个不禁心中窃喜,这么容易就找到原因了。记得以前遇到过一次,在多线程并发的情况下,java.util.HashMap.get()极有可能导致CPU100%,并且永久阻塞,当然网上也曾经有人遇到过此类问题,参见:http://my.oschina.net/javagg/blog/3358

于是,我很爽快地把HashMap改成了ConcurrentHashMap,并再一次进行测试。很遗憾,事情并没有就此结束,更让人意外的是,这仅仅是恶梦的开始,当然这是后话。第二次执行时,策略没有在10轮左右的时候变慢,而是把这个时间延后了,变成了在50轮左右的时候变慢。这说明刚才的改动明显是起到了一定的功效,于是再次用jstack查看,这次又发现了一个新的现象:

"SyncEventProcessor@strategyId#1840[DEMO名字#1840]" prio=6 tid=0x0c792000 nid=0x4784 waiting for monitor entry [0x0f54f000..0x0f54fae8]
   java.lang.Thread.State: BLOCKED (on object monitor)
 at java.text.DecimalFormat.format(DecimalFormat.java:578)
 - waiting to lock <0x04245330> (a java.text.DigitList)
 at java.text.DecimalFormat.format(DecimalFormat.java:507)
 at java.text.NumberFormat.format(NumberFormat.java:269)
 at com.dfitc.stp.indicator.Indicator.toString(Indicator.java:509)
 at java.lang.String.valueOf(String.java:2827)
 at java.lang.StringBuilder.append(StringBuilder.java:115)
 at com.dfitc.stp.strategy.CustomStrategy._updateIndicators(CustomStrategy.java:248)
 at com.dfitc.stp.strategy.CustomStrategy.preProcessEvent(CustomStrategy.java:789)
 at com.dfitc.stp.strategy.CustomStrategy.processEvent(CustomStrategy.java:898)
 - locked <0x0774b118> (a com.dfitc.strategy.demo.PrinterStrategy$$EnhancerByCGLIB$$9ff2c180)
 at com.dfitc.stp.strategy.CustomStrategy._processImmediately(CustomStrategy.java:219)
 at com.dfitc.trading.hedge.test.SyncEventProcessor.processNormalEvent(SyncEventProcessor.java:342)
 at com.dfitc.trading.hedge.test.SyncEventProcessor.run(SyncEventProcessor.java:395)
 at java.lang.Thread.run(Thread.java:619)

这次阻塞的对象变成了java.text.DecimalFormat.format(),原因是我在Indicator.toString()中会使用DecimalFormat对象去进行数字格式化操作,但为了节约内存,把DecimalFormat对象设成了静态的,这样所有的Indicator在格式化时都会使用同一个DecimalFormat进行格式化,通过查看DecimalFormat的源码得知,这个类在格式化时会对自身的实例变量digitList加锁:

private StringBuffer format(double number, StringBuffer result, FieldDelegate delegate) {  ...  synchronized(digitList) {    ...  }}

这是不是瓶颈还不知道,但是考虑到把Indicator中的DecimalFormat改成非静态的也无伤大雅,于是索性改掉。但第三次进行测试时,问题依旧。没办法,继续在进度条变慢时用jstack进行多次查看对比,仍没看出来问题。但前面的努力也并非徒劳无功,至少让缓慢的进度条出现时间延后了,不仅如此,还可以断定剩下的问题一定是出在内存泄漏上了。经过仔细审阅代码,发现策略执行时,是一组一组顺序执行,策略实例在每一组执行完毕后都会进行销毁,所以可以推断策略本身不会出现内存泄漏。但是策略执行完后,会把执行的结果(即运行后的分析结果,如盈亏、回撤等)存储起来,2000个策略当然最终就会存储2000份。不过这一块并不是由我负责开发的,我只是在策略下单产生成交时调用,然后在策略执行完毕后获取分析结果。据猜测,这个功能很可能在计算完结果后,没有将用于计算结果的原始数据清除。我马上找到该模块的开发人员确认,事实证明我的猜测是对的。于是我让该开发人员马上进行修正,在适当的时候清除掉原始的数据。改完后马上进行第四轮测试,原以为事件可以就此终结,但从执行结果来看,和第一次改动一样,问题仅仅是得到了缓解,并没有根除,策略在执行到60轮时又出现了同样的问题。没办法,只能继续查找原因,我再次对代码进行了详细的审阅,仍没有发现问题,束手无策时我想起了java自带的工具jmap,步骤仅需2步:

1.保存内存快照: jmap -dump:file=c:\dump.dat pid,不过保存后的文件是二进制的,无法用文本编辑器查看

2.查看内存快照,这个需要先安装一个eclipse插件Memory Analyzer(jdk自带的工具也可以查看,不过据说Memory Analyzer更直观),在线安装地址:http://download.eclipse.org/mat/1.2/update-site/,安装后打开对应的Perspective,就可以将刚才保留的dump.dat打开查看了

这真是一个神奇的工具,还没有等我自己去看哪有问题,它已经直接告诉我了:

一目了然,有一个叫做HisStrategyContext的类进入了我的视线,这个类开辟的内存空间占用了所有优化策略的82%。经过查看代码发现,这个类会存储策略执行过程中需要持久化的所有缓存信息,包括委托、成交回报、撤单回报等等,虽然在策略被“删除”时会执行清空缓存的操作,但在参数优化时,所有的策略都没有“删除”这个操作,只有“停止”。

于是,对代码进行了改进,策略停止时也追加清除缓存的处理。我坚信这次找到了性能瓶颈,所以索性把穷举组合增大到10000个,不出所料,再一次执行时,每一组都运行稳定:

group(1):size=20, current=(20/11664), time=42s
group(2):size=20, current=(40/11664), time=39s
group(3):size=20, current=(60/11664), time=37s
...
group(498):size=20, current=(9960/11664), time=34s
group(499):size=20, current=(9980/11664), time=34s
group(500):size=20, current=(10000/11664), time=34s

至少,在执行了167组(3340个) 策略时,一切正常。

通过这次调优经历,总结如下:

1.HashMap是非线程安全的,get()方法在多线程竞争时会导致CPU永久100%的可怕事情发生,所以,如果可能出现多个线程同时访问Map集合,需要使用更安全的ConcurrentHashMap代替

2.数字格式化的类DecimalFormat,其format方法也会有竞争,所以在不影响性能的情况下,尽量不要在多线程中使用DecimalFormat

3.再谨慎的编码习惯也难以确保不会出现内存泄漏,必要时使用工具可以让事情事半功倍