去年年底的压轴工作就是Easy Framework,和基于它之上的通用配置系统。一直对其中运用的诸多反射机制和魔法函数的性能问题抱有疑问,这几天开始花时间看看细节,然后找找对策方法来解决它。

1. Config

通用的配置结构我自认为是Easy Framework里比较好用的一个东西。其好用就是建立在配置通用的基础上的,这就对配置系统本身提出了要求,首先一点是配置文件放置的位置必须是有规律的,其次配置文件的命名也必须是有规律的。在这些基础上,框架可以实现一套非常自律的配置系统,所有的配置读取需求可以在统一的接口下进行操作。

当然,方便的使用通常也意味着某些代价的付出。在这里就是规律的解析。试想下,如果是一个纯手工编写的脚本,所有的配置文件在代码里写死,那么脚本的性能就能被发挥到机制,没有任何的“浪费”。而在配置系统下,规律的解析是要时间的,配置目录的定位,配置文件的定位,都是写在规律之下的。这个时候在配置系统初始化的时候显得特别明显,系统会有一系列的动作来建立配置实例,包括配置系统自身的class load,然后是配置文件的读取,配置文件数组的初始化。在你想通之后,你会发现,其实手工写死配置和使用配置系统之间最大的性能差就在这里,当然,在每次进行配置定位到某个配置数组block之前还是会有性能差异,不过这就很小了,基本可以忽略不计。而好处则是非常明显的,配置不再混乱,不再需要根据项目反复实现其自身的配置读取流程。

测试脚本选用了很简单的一个逻辑脚本,在脚本实质逻辑部分几乎没有内容,纯粹为了显示框架本身的耗时而编写的。测试的结果框架本身占用的时长耗去1.5毫秒左右,基本可以忽略了。性能还是很好的。如果打开APC模块的话,性能应该还能进一步提升。

2. Controller

Controller和前述的Config其实是一个情况,controller的制定是为了解决多脚本入口造成的比较混乱的代码流程问题。而它的开销也在于一开始的初始化过程,需要读取ctrl配置信息,然后初始化配置数组,再导向代码进入逻辑层。

性能上的结论和Config处描述的一致,对于一个框架来说是必须的,且不是很大的性能开销。

3. Bo

遵从开发效率的考虑方向,bo封装层是完全抽象到EfBo这个父类上的。EfBo的职责就是从ORM配置中读取到当前对象的属性构造,然后自动组装成能get和set的bo对象。性能瓶颈存在于两点,其一是在进行初始化的时候,父类EfBo必须知道当前实现类的构造,就需要读取ORM配置,这是开销。其二,所有的get和set方法都会使用魔法函数__call来进行动态执行,这个也是开销。此外,在使用APC的服务器上,静态的PHP脚本性能和使用魔法函数的动态脚本之间的差距将会进一步拉开。这里我们使用xdebug进行PHP脚本的profile,并使用QCacheGrind软件进行图形界面显示,以下皆同。

测试对象构造很简单,属性有id、name、class、rank、score这5个。ReflectionYBo对象是读取ORM的动态PHP脚本,而ReflectionNBo则是不读取ORM、预定义的PHP脚本。测试脚本中分别会对所有属性get和set一次,脚本I1001代表动态脚本,脚本I1002代表静态脚本。(下面profile图中的脚本名字都是一开始跑测试时随便起的,是规范之前的,所以名字与本文中的说明有出入,请无视之)

第一次测试:分别都跑一轮,我们看下性能差距:

[codesyntax lang="bash"]

Jan 18 12:34:52 Jonathans-MacBook-Pro php-cgi[8144]: EfsAction::I1001() START.
Jan 18 12:34:52 Jonathans-MacBook-Pro php-cgi[8144]: EfsAction::I1001() consumed: [4]
Jan 18 12:28:19 Jonathans-MacBook-Pro php-cgi[8143]: EfsAction::I1002() START.
Jan 18 12:28:19 Jonathans-MacBook-Pro php-cgi[8143]: EfsAction::I1002() consumed: [2]

[/codesyntax]

因为循环都为一轮性能差距不是很明显,一个4毫秒,一个2毫秒,差一倍。接下来我们看下profile出来的结果:

I1001,动态脚本,profile出来的结果有点意思。魔法函数__call也有占运算时间,而且时长还不小:

789是执行总时长,等于0.789毫秒。10是执行总次数,正好等于get和set的总和。

可见所有的get或set函数都执行了一次,耗时0.1毫秒左右。

I1002,静态脚本,也很奇怪,我居然没找到get函数的执行,只有set函数。 应该是软件的原因,在MacCallGrind里是有的。

可以看到执行速度很快,基本都无法计时了,0.001毫秒。

上面图中列出的还只是直接调用get和set函数和使用魔法函数之间的差距,其实还有ORM配置读取,配置初始化等等一系列的差距。虽然在执行次数只有一次的前提下时长看起来差距不大,但是在执行次数放大之后的差距就很离谱了,接下来就是第二次测试。

第二次测试:我们把循环数拉到500轮,然后再看结果:

[codesyntax lang="bash"]

Jan 18 12:38:30 Jonathans-MacBook-Pro php-cgi[8145]: EfsAction::I1001() START.
Jan 18 12:38:31 Jonathans-MacBook-Pro php-cgi[8145]: EfsAction::I1001() consumed: [807]
Jan 18 12:38:34 Jonathans-MacBook-Pro php-cgi[8146]: EfsAction::I1002() START.
Jan 18 12:38:34 Jonathans-MacBook-Pro php-cgi[8146]: EfsAction::I1002() consumed: [76]

[/codesyntax]

现在的性能差距就很明显了,动态脚本花了800毫秒,而静态脚本则只有76毫秒。profile结果:

I1001,魔法函数__call的耗时占了大部分时间,且每个get或set函数的时长都上去了:

魔法函数总耗时373.780毫秒。

所有的get和set函数每500次执行都接近45毫秒。

I1002,耗时都上去了,但是还在可接受范围之内,set的时长比get要大:

结论很明显了,在一个逻辑比较复杂的脚本中,基本的bo对象的初始化和get以及set次数都不会少,这种性能的差距是不能被接受的。
解决方法也很简单,在AutoBuilder脚本执行的时候把所有的bo对象自动化创建完成就可以了。

4. Model

Model也是一个性能上比较有疑虑的地方,它和bo比较类似而不同于config和controller,后两者的性能开销比较集中在初始化的时候,因此一般是能被接受的,而model和bo则初始化和使用比较频繁,如果性能开销比较大的话,很容易被放大到不能接受的程度。所以需要做性能测试。

在profile之前我们可以整理下使用框架和不适用框架之间的逻辑流程差别。其实两者之间的差别不是很大。
在未使用框架的情况下,逻辑处理一条SQL操作,需要遍历以下几步:

  • 取到数据库操作类的实例
  • 取到SqlBuilder类的实例
  • 进行数据库连接
  • 进行数据库查询
  • 将返回结果转换
  • 缓存取出的结果

而使用框架则多了几步中间步骤。

  • 在Model对象初始化的时候需要读取ORM配置信息:一般不会需要读取多个Model,也就不会造成太多的初始化开销,且ORM配置读取过一次后,在全局config里是有缓存的
  • 在获得数据库操作类实例和获取SqlBuilder类实例的时候,先要访问context,然后才能拿到对象实例:基本没有开销
  • 在结果转换的时候需要使用到eval:基本没有开销
  • 在获得缓存操作类实例的时候,先要访问context,然后才能拿到对象实例:基本没有开销

从上面的分析中就能看出来,实际上两者的差距真的很小很小,几乎没有什么差别。主要的开销就在于框架的初始化部分和几个context的反射机制和eval动态调用机制。实际的测试结果也与这个分析基本相同。

先简单介绍下测试脚本的逻辑,两份脚本分别使用pvzs项目模式和easy framework模式,两种模式,来进行一次数据库表的select查询操作,完成之后再将array转换成bo对象,最后进行memcache的set操作。memcache的get操作永久保持false返回,保证每次脚本执行都会向数据库查询。脚本I1003是前者,I1004是后者。这里还要说明下,这里测试的主要目的是测试使用和不适用框架的性能差距,所以sql都是查询操作,没有update和delete等,因为只要数据库连接连上之后,后面的耗时都是数据库的事情,和PHP就没关系了。

第一次测试:跑一轮,结果和预测的一样,两者的差距在1毫秒之间。

[codesyntax lang="bash"]

Jan 29 17:02:48 Jonathans-MacBook-Pro php-cgi[6982]: EfsAction::I1003() consumed: [5]
Jan 29 17:02:48 Jonathans-MacBook-Pro php-cgi[6983]: EfsAction::I1003() consumed: [3]
Jan 29 17:03:16 Jonathans-MacBook-Pro php-cgi[6978]: EfsAction::I1004() consumed: [4]
Jan 29 17:03:17 Jonathans-MacBook-Pro php-cgi[6979]: EfsAction::I1004() consumed: [5]

[/codesyntax]

第二次测试:跑100轮,性能的差距开始慢慢显现出来了。还在接受范围之内。

[codesyntax lang="bash"]

Jan 29 17:10:32 Jonathans-MacBook-Pro php-cgi[6978]: EfsAction::I1003() consumed: [40]
Jan 29 17:10:38 Jonathans-MacBook-Pro php-cgi[6979]: EfsAction::I1003() consumed: [42]
Jan 29 17:10:38 Jonathans-MacBook-Pro php-cgi[6980]: EfsAction::I1003() consumed: [40]
Jan 29 17:10:19 Jonathans-MacBook-Pro php-cgi[6973]: EfsAction::I1004() consumed: [76]
Jan 29 17:10:20 Jonathans-MacBook-Pro php-cgi[6974]: EfsAction::I1004() consumed: [74]
Jan 29 17:10:20 Jonathans-MacBook-Pro php-cgi[6975]: EfsAction::I1004() consumed: [71]

[/codesyntax]

这里的性能差距和bo测试时候是不一样的,bo的时候差距是按数量级别缓慢放大的,而这里则是每次sql操作都差1毫秒左右,随着执行次数的增加,耗时差距慢慢就堆叠上去了。使用MacCallGrind工具profile出来的结果也是如此。而一个逻辑运算中的数据库操作一般不会太多,所以这个性能还在接受范围内。

5. 总结

这次的回顾过了几个主要的核心组件,包括了config、controller、bo和model,其中性能基本都在预料范围之内。而bo的性能瓶颈则可以通过项目build时候预创建代码文件来绕过。总体来说问题不大,但之前所写的充其量也只能说是理论上的分析加上比较粗浅的快速测试比较,至于实际使用情况还需要在将来慢慢跟进。Keep thinking.