以我自己在 hw_2 的一次时间优化为例
前言#
室友 @Kie-Chi 发现了这个好用的工具,但他忙着性能优化没空写,所以我来写(
Profiler,通常就是指程序的性能分析工具,可以帮助我们分析和优化代码的性能。(废话)
IntelliJ Profiler#
IntelliJ Profiler 是 JetBrains 为 IDEA Ultimate 版提供的性能分析工具,用于监控和优化程序的性能。(废话 x2)
不过要注意,这个工具的使用需要 Ultimate 版本,如果是免费的 Community 版本是不支持的,所以如果你想使用,请先参照网上的教程申请 IDEA Ultimate 版。
接下来,我会以我的一次(时间)性能优化为例,向各位展示这个工具的妙用。
以下案例基于我的真实优化案例,但是略有加工,具体情况我会在结语说明。
前情提要#
xx年x月x日x时x分,tsxb 完成了 hw_2 的基本编写,然而他的程序还是拜倒在水群大佬搓的数据下:
1
f{n}(x)=1*f{n-1}(sin(sin(sin(x))))+0*f{n-2}(x)
f{1}(x)=sin(sin(sin(sin(x))))
f{0}(x)=sin(sin(sin(sin(x))))
f{5}(f{0}(f{0}(f{0}(f{0}(f{0}(f{0}(x)))))))
啊,怎么优化啊。他心里很急切,然而他的在 hw_2 开启后的一个小时内就完成了基本编写并在第二天就完成了绝大部分长度优化并且程序还跑的巨快巨正确没有什么错误的巨佬中的巨佬的室友一脸淡定的回答道:
用 IntelliJ Profiler 分析一下。
于是 tsxb 开始研究这个玩意……
22 分钟可以喝 11 升水#
要想运行 IntelliJ Profiler,在 IDEA 界面下找到右上角运行旁的小三点,点击后在展开的菜单栏下找到“使用 IntelliJ Profiler 分析……”
点击后,IDEA 会自动进入运行界面,同时 IntelliJ Profiler 已就绪。输入数据后(也可以使用重定向输入,见结语)就可以喝杯茶等你的程序跑完~
上帝视角:优化之后我的程序运行就出结果了,现在我签出到优化前的这次提交,跑了n久也没等到结果……
……
……
事实上,在 21 分 33 秒之后 tsxb 洗完澡回到了寝室,然后发现他的程序还没跑完。。。
这不经让他感叹,到底是这个程序跑得更慢,还是他衬衫背后那段代码跑得慢。
没关系的,虽然你程序写的不好,但是你球打的也烂啊,hhhh555。
好吧,虽然界面上显示有“停止并显示结果”的功能,但 tsxb 尝试了之后并没有效果。无奈之下他只好选择另一组数据来测试。
1
f{0}(x) = cos(cos(cos(x)))
f{1}(x) = cos(cos(cos(x)))
f{n}(x) = 1*f{n-1}(cos(cos(x)))+ 1*f{n-2}(cos(cos(x)))
f{5}(cos(cos(cos(cos(cos(cos(cos(cos(x)))))))))
这次他跑的还算比较还行很快,经过 0.13333 分钟后程序就运行出了结果。此时左侧出现了一个通知:“分析器数据已就绪”,tsxb 打开之后得到了以下界面:
真是太壮观了!然而他看不懂。于是他的大牛室友向他介绍了一种简单方法。
简单方法#
点击你的主类,可以在左侧看见每个方法或者语句的执行时间。
单击这个时间,可以选择想要查看的方法,然后层层点击进去,就可以找到影响时间的“缓慢杀手”。
在 tsxb 的 Mono
类和 Poly
类下,他发现了一个问题:他的 toString()
方法运行的耗时最长。
这里展示的是他的 Poly
类的 toString()
方法,可以看到实现得相当冗长。下面那一抹红则是调用了 Mono
的 toString()
方法,而他的 Mono
类的 toString()
方法又调用了 Poly
类的 toString()
方法……
并且在这一过程中,他没有在这方法的内部发现有其他更加耗时的操作。正所谓日积月累,水滴石穿,人一赖床就日上三竿,在 Mono
类和 Poly
类的 toString()
方法的嵌套调用中,tsxb 成功地 CTLE 了。
解决问题#
既然找到了问题,那么对症下药即可。tsxb 发现,他的 Mono
类和 Poly
类的实现都是不可变类——所谓不可变类,tsxb 粗浅的认为就是状态不可更改的类,一旦该类对应的对象被创建,其所有字段的值都被固定下来,无法进行修改。
这就好办了!既然 Mono
类和 Poly
类是不可变的,它们从一开始到结束,toString()
方法生成的字符串也应该是不变的。那么当第一次调用了 toString()
方法,将生成的字符串缓存之后,下次再调用不就可以直接用了吗?
tsxb 觉得:Damn! I’m so fxxking smart! 于是他就开始了重构。完成了修改之后,他再次对这个样例进行了测试:
轻松秒杀~
进阶方法#
实际上 IntelliJ Profiler 还有更多强大的功能。
- 火焰图:可以可视化程序的调用堆栈,并查看它随着时间的变化。栈框架越宽,方法执行时间越长。彩色块显示的是本地代码,而灰色块是 Java 标准库的一些代码。看 tsxb 第一次运行结果的火焰图,不可不谓是众人拾柴火焰高。
- 调用树:可以显示方法使用的 CPU 时间百分比,以及具体的方法执行路径。不断展开选项,你就可以看到一条消耗时间的关键路径(该死的 CO 还在追我)。由于我们是递归调用层次,这一关键路径非常长,而且占用时间百分比是随着层次的展开一点一点减少的。
- 方法列表:可以显示按运行时间排序的方法,这里就可以跳过递归,很快找到耗时最长的方法。
此外还有时间轴、事件等视图,不一一介绍了,进阶嘛,要留着自己探索(
结语#
其实我这次调优的实际情况比这要复杂一些。我先是实现了哈希缓存——是的,不可变对象的哈希当然也不变,然后才想到字符串也可以做缓存。在这期间,我还意外发现我的 powPoly()
方法(就是把一个多项式进行乘方运算)实现非常缓慢,原因竟然是我画蛇添足,给 powPoly()
实现了快速幂算法……
为什么快速幂算法在这里反而会更慢呢?虽然快速幂算法能够优化乘法的次数,但是我忽略了单次乘法的时间——对于多项式来说,一个长的多项式乘以一个短的多项式,明显是要比两个中等的多项式相乘所花的时间要短的。嗯,应该是吧,没有细想,我只是拿等周长的长方形和正方形的面积大小做了个类比。所以快速幂反而变成蜗牛幂了(
此外,其实我的 Poly
类并非一个完全的不可变对象。我的 Poly 类实现了一个 addMono()
的方法,会更新它的 HashMap<MonoKey, Mono> monoMap
这一字段。然而之后我详细查看了我其他调用 addMono()
的方法,都是在其他方法中创建了一个新的 Poly
对象,并在返回前往里面加 Mono
对象,这一过程中并没有用到该 Poly
对象的哈希或者字符串。所以为 Poly
类设置缓存能运行成功,只是侥幸。
不过,如果想要为上面这种可变类设计缓存也不是不行,加上一个脏标记即可(怎么还有线段树的味道)。我的做法是,创建几个布尔变量来表示对缓存的脏标记,如果标记缓存为脏则说明需要重新计算缓存,调用相应方法即可;否则直接调用缓存。在重新计算缓存的方法的结尾,和修改对象的方法的开头,可以加上对脏标记的修改,这样大概就可以了。
关于脏标记我也不太懂,如果有大佬实现了,希望能发出来让大家瞻仰一下)
关于 IDEA 的重定向输入输出的问题,可以在右上角运行附近找到“编辑配置”选项,里面的选项中可以选择重定向输入以及将控制台输出保存到文件,进行相应的配置即可。此外,虚拟机参数也可以如图加上 -Xms768m -Xmx768m
的选项,以将程序运行内存限制在 768M 以内,符合课程组强测的要求。(玩过 mc 的应该都懂吧)
好了,这次分享就到这里,希望你能喜欢,并有所收获!