Sunday, February 06, 2005
Optimization Surprises
优化的意外惊喜
本周末,我在为泛型函数common-case path的执行时间缩减几微秒上又作了一次尝试,并成功地将执行时间从13.2微秒降至仅为9.8微秒左右。(这相对于同一实验函数手工优化的Python版本多出了大概9微秒的负载。)然而,随着研究的深入,我有了一些关于Python性能调整的惊奇发现。
我做的第一件事是将线程锁从关键路径(critical path)中移除转为使用double-checked锁(这是又一个Java不支持而Python支持的机制之一)。然后我去掉了一个闭包定义使其不会出现在关键路径中,最后在dispatch循环中简化了while条件语句,通过交换比较慢的exit case降低了每个语句的执行时间。这三件事为critical path节省了大约2.5微秒的时间:一半用来移除锁,一半做其它的事情。
在这方面我可以肯定的说,通过对Python部分的代码进行优化后其执行时间已经被榨干得差不多了,唯一仍能有收益的应该会来自于C编写的代码。所以,我着手为此做了一些重构方面的准备工作。我的想法是将Dispatcher.__getitem__挪到一个基类中,该基类在C版本不可用时会存于Python代码中,否则会存于C代码中。为了做到这一点,我从锁中移除了一些私有name mangling,使得基类能够访问得到,然后便执行提取内部闭包并将其转换为一个类的操作,这样做是由于Pyrex不支持闭包。
完成这些变化后,为了检查去掉__getitem__方法是否会对执行产生影响我运行了性能测试程序。令我大吃一惊的是,代码运行快了.8微秒之多。
研究速度为何变快着实花了我一些时间。起先,我仅是插入和移除当前补丁(patch),重新运行我的计时器,因为我的第一反应是计时异常原因导致的。但在运行了几次这两种情况下的程序,我意识到事情并非如此,当然就排除了这方面因素。我接着又执行name mangling,仅去掉该方法,计时仍没受影响。如此看来应该是去掉闭包捣的鬼了。
现在真正让人困惑不解的是我已经去掉了关键路径的闭包定义。所以理论上说,在那个块(block)中有哪些代码应该是无关紧要的了,在性能测试中该块是决不会被执行的!(这就是它为何花了我如此长时间来了解其来龙去脉。)
除非.. 在这前后整个函数的区别是,包含一个闭包的函数有“cell变量”,而不含闭包的函数只有“快速局部(fast local)”变量。会是这种不同导致速度上的差异吗?结果显示一个闭包和封闭(enclosing)函数共享四个变量,在关键路径上这些变量被设置了四次读取了两次,与快速局部变量相比,这些动作将使额外负载增至接近一个微秒的9/10ths。(Cell变量包含额外的一个间接步骤来进行读或写。)因此,即使闭包在关键路径上从未被创建或执行,外函数(outer function)在这上面仍将会有开销!
现在,你该摆脱“闭包会拖累速度”这种想法了吧。在我使用闭包的99%的时间里,它都会高效地从一个模版中复制一个函数,产生函数的特定版本,并且不受外函数性能的影响。我唯一一次所发现与之相关的问题就是如下这种情况:在你试图通过外函数以less-common 路径的代价优化一个快速路径时,创建一个对象而非闭包将对之前本应使用闭包的路径增添明显的负载开销。因此,这是执行路径上的净损失。(我希望最后通过将一个stack-allocated对象数组代替对象并用C编写的方式来补偿一下损失,但现在看来距离实现还比较遥远。)
然而,这仅是我的第一“惊”。下一步我将把新的基类和替代闭包的类引入Pyrex中,因为他们不再有不支持的结构。然后便运行性能测试程序,结果是其性能比我去掉闭包后产生的损失一样十分的糟糕!
(原文链接网址:http://dirtsimple.org/2005/02/optimization-surprises.html)