php 项目性能优化
C.2. 类装载
做过Zend Framework 应用性能调优的人都知道,Zend Framework
中类装载的开销是相当大的。从各组件对应的大量类文件,到类名与文件系统非唯一对应的插件的引入,大量include_once和
require_once调用可能导致严重的性能问题。这章将提供一些具体的策略来解决这些问题。
C.2.1. 如何优化include_path?
提高类装载速度的一个优化策略是合理安排include_path。具体而言,你应该做四件事情:使用绝对路径(或绝对路径的相对路径[原文:paths
relative to absolute
paths]);减少包含路径数量;ZF库的路径要尽量靠前;只在include_path最后包含当前路径。
C.2.1.1. 使用绝对路径
尽管这看起来是一个微不足道的优化,然而如果不这么做,PHP的realpath缓存可能就无法发挥作用,结果导致opcode缓存的效果与你的期望大相径庭。
做到这一点有两个简单的途径。第一种是在php.ini、httpd.conf或.htaccess 中硬编码那些包含路径。第二种是在设置include_path时调用PHP的realpath()函数:
$paths = array(
realpath(dirname(__FILE__) . '/../library'),
'.',
);
set_include_path(implode(PATH_SEPARATOR, $paths);
你也可以使用相对路径——如果它是一个绝对路径的相对路径:
define('APPLICATION_PATH', realpath(dirname(__FILE__)));
$paths = array(
APPLICATION_PATH . '/../library'),
'.',
);
set_include_path(implode(PATH_SEPARATOR, $paths);
尽管如此,对一个相对路径调用realpath()也很方便。
C.2.1.2. 减少包含路径的数量
包含路径将按照它们的出现顺序逐个扫描。显而易见,如果文件在第一个路径下的查找速度要远远快于它在最后一个路径。因此,一个明显的改善方法是减少
include_path中的路径数目,只包含必需的路径。仔细检查自己在include_path中定义的每一个路径,确保应用用到了该路径下的代码,
如果没有,则删除。
另外一个优化方法是合并路径。例如,Zend Framework 遵循PEAR 的命名规范,因此,如果应用同时也使用了PEAR
库(或其它遵循PEAR 命名规范的框架或组件库),则可以把它们放在相同的包含路径下。通常,这只需要在一个公共目录下为各个库建立一个符号链接。
C.2.1.3. 尽早定义Zend Framework库的路径
除了上面的建议,另外一个优化方法是尽量把Zend Framework库的路径放在include_path的前面。在大部分情况下,它应该是include_path中的第一个路径。这样可以保证Zend Framework自身的文件可以在第一次扫描中命中。
C.2.1.4. 把当前路径放在最后或去掉
大部分include_path的例子都包含当前路径,或‘.’。这可以方便脚本包含当前目录下的文件。并且在这些例子中,当前路径通常是在
include_path的开头——即首先尝试当前路径。对于Zend
Framework应用,这通常不是开发者所期望的,当前路径放在包含路径的最后或许更适合一些。
Example C.1. 示例: 优化include_path
让我们把上述建议综合在一起。假设我们同时使用Zend Framework 和一些PEAR库——如PHPUnit和Archive_Tar库,并且偶尔也需要从当前目录包含文件。
首先,我们在项目中建立一个库目录。在这个目录中,为Zend Framework库和其它PEAR库建立符号链接:
library
Archive/
PEAR/
PHPUnit/
Zend/
如果需要的话,还可以在此建立项目自身的库目录,这对那些共享库毫无影响。
接下来,我们将在public/index.php 文件中创建包含路径。它会在所有请求中执行生效,不需要在别的地方再次设置。
我们将采用上面的建议:使用通过realpath()获取的绝对路径;把Zend Framework库路径放在前边;合并包含路径;把当前路径放在最后。事实上,示例做得非常漂亮——仅用了两个路径。
$paths = array(
realpath(dirname(__FILE__) . '/../library'),
'.'
);
set_include_path(implode(PATH_SEPARATOR, $paths));
C.2.2. 如何消除非必要的require_once语句
延迟载入是一项仅在需要时载入类的优化技术。——比如实例化一个对象、调用静态类的方法或者引用类的常量或静态属性。PHP通过自动载入机制实现延迟载入,开发者需要定义回调函数,将类名映射到相应的类文件。
然而,如果你在库中仍然使用require_once,那么自动载入的效果就要大打折扣——在Zend Framework中更是如此。因此,现在的问题是:如何消除这些require_once调用,从而发挥自动载入机制的最大性能?
C.2.2.1. 使用find和sed去除require_once调用
去除require_once 的一个简便方法是联合使用unix工具find和sed,把每个require_once 语句转换为注释。可以使用下面的命令(%是shell提示符):
% cd path/to/ZendFramework/library
% find . -name '*.php' -print0 | xargs -0
sed --regexp-extended --in-place 's/(require_once)/// 1/g'
这一行命令(因为阅读方便分为两行)遍历每一个PHP文件,将'require_once' 替换为'// require_once',高效地将每个require_once调用注释掉。
这行命令可以方便地加入到一个自动部署或发布工具中,帮助提升产品的性能。不过需要注意的是,如果你使用了该技术,你就必须使用自动载入功能,这可以在项目的"public/index.php"文件中通过下面代码实现:
require_once 'Zend/Loader.php'; // one require_once is still necessary
Zend_Loader::registerAutoload();
C.2.3. 如何加快插件的载入
许多组件都支持插件,开发者可以自己开发插件和对应组件协同工作,也可以覆写存在于Zend Framework包中的标准插件。插件给框架增加了灵活性,但是为此付出的代价是:插件的载入需要耗费大量资源。
插件载入器允许开发者注册类前缀/路径对,从而为插件指定非标准的类文件搜索路径。每个前缀允许关联多个路径。在内部,插件载入器首先遍历所有类前缀获
取该类前缀关联的路径,然后再遍历这些路径查找文件,然后读入文件,测试查找的类是否已经找到。可以想象,这将产生很多对文件系统的stat调用。
把这个过程乘以使用PluginLoader的组件数,你就可以知道该问题的影响范围了。截止到本文写作时,下面列举的组件使用了PluginLoader: •Zend_Controller_Action_HelperBroker: 助手
•Zend_Dojo: 视图助手,form元素和装饰器
•Zend_File_Transfer: 适配器
•Zend_Filter_Inflector: 过滤器(ViewRenderer动作助手和Zend_Layout使用)
•Zend_Filter_Input: 过滤器和验证器(validators)
•Zend_Form: 元素,验证器,过滤器,装饰器,验证码(captcha),文件转换适配器
•Zend_Paginator: 适配器
•Zend_View: 助手,过滤器
如何减少这些调用的次数呢?
C.2.3.1. 使用插件载入器的包含文件缓存
Zend Framework
1.7.0为PluginLoader增加了一个包含文件缓存。该功能将记录插件的类文件路径,并构造"include_once"语句写入一个文件,从
而可以在启动文件中包含该文件。尽管这么做将使你的代码增加很多include_once调用,但是它可以让插件载入器运行的更快。
PluginLoader的文档:includes a complete example of its use.
C.3. 国际化(i18n)和本地化(l10n)
国际化和本地化站点是扩展用户的好方法,它保证访问者可以获得所需的信息。然而,它也经常会带来一些性能问题。下面这些策略可以用于降低国际化和本地化带来的负担。
C.3.1. 该使用哪个翻译适配器
各种翻译适配器千差万别,有的以功能丰富见长,有的以性能卓越著称。除了因业务限制而只能选择特定适配器之外,该如何选择一个高效率的适配器呢?
C.3.1.1. 使用非-XML翻译适配器获取最快的速度
Zend Framework中包含了许多翻译适配器。然而它们多半使用XML格式,这会导致内存和性能的开销。幸运的是,也有一些适配器是基于能够高效解析的其它格式。按照速度从快到慢的顺序,它们是:
· Array:这是最快的,它将在包含时被直接解析为PHP的原生格式。
· CSV:使用fgetcsv()解析CSV文件,并转化为PHP的原生格式。
· INI:使用parse_ini_file()解析INI文件,并转化为PHP的原生格式。它的性能和CSV差不多。
· Gettext:Zend
Framework中的gettext适配器没有使用gettext扩展,因为该扩展是非线程安全的,并且不能在一个服务器上指定多个locale。因
此,它比直接使用gettext扩展要慢,不过gettext格式是二进制的,比解析XML要快。
如果高性能是你的一个关注点的话,建议使用上述的某个适配器。
C.3.2. 如何使翻译和本地化更快
也许因为商业因素,你只能使用基于XML的翻译适配器。或者想让程序更加高效,或者想让本地化操作更快。有什么办法呢?
C.3.2.1. 使用翻译和本地化缓存
Zend_Translate和Zend_Locale都实现了缓存功能,它可以明显地提升性能。通常情况下,性能瓶颈在于读入文件,而不是实际的查找操作,使用缓存可以避免翻译和本地化文件的重复读取。
你可以从下面链接中进一步了解翻译和本地化缓存的内容:
· Zend_Translate adapter caching
· Zend_Locale caching
C.4. 视图渲染
如果使用Zend
Framework的MVC框架,你可能会使用Zend_View组件。相对于其它视图或模板引擎,Zend_View是相当高效的。因为直接用PHP编
写视图脚本,就没有编译自定义标记的负担,也不需要担心编译后脚本的优化。当然,
Zend_View也存在自己的问题:扩展的代价很大(视图助手),如果通过许多视图助手来完成关键功能,将会导致很大的性能开销。
C.4.1. 如何加快视图助手的解析
Zend_View中的大部分方法实际上都是通过助手系统的重载来实现的。它赋予Zend_View极大的灵活性,不需要扩展Zend_View,提供应
用所需的助手方法就可以,开发者在单独的类中定义助手方法,然后就可以像调用定义在Zend_View中的方法那样使用助手方法。这样既可以保持视图对象
的简单性,也可以保证助手只在需要时被创建。
在内部,Zend_View通过插件载入器查找助手类文件。这意味着每一次调用助手,Zend_View需要把助手名字传给插件载入器,由它决定类名,
如果未载入则载入助手,然后返回实例化的对象。因为Zend_View在内部缓存了已经加载的助手,所以下次再使用这个助手时,速度将会快很多,然而,如
果应用中使用了很多助手,这个过程对系统性能的影响可能会很大。
那接下来的问题是:如何加速助手的解析?
C.4.1.1. 使用PluginLoader的包含文件缓存功能
这是最简单经济的方法,具体见C.2.3.1中所述。曾经有实验表明,这个技术在没有opcode缓存情形下可以提高25-30%的性能,如果有opcode缓存,则可以提高到40-65%。
C.4.1.2. 扩展Zend_View来提供常用助手方法
另外一个提高性能的方法是扩展Zend_View,在子类中手动增加应用常用的助手方法。这些助手方法可以简单地作为一个代理,实例化一个相应助手类完成任务,当然也可以自己实现任务的所有操作。
class My_View extends Zend_View{ /** * @var array Registry of helper
classes used */ protected $_localHelperObjects = array(); /** * Proxy to
url view helper * * @param array $urlOptions Options passed to the
assemble method of the Route object. * @param mixed $name The name of a
Route to use. If null it will use the current Route * @param bool $reset
Whether or not to reset the route defaults with those provided *
@return string Url for the link href attribute. */ public function
url(array $urlOptions = array(), $name = null, $reset = false, $encode =
true ) { if (!array_key_exists('url', $this->_localHelperObjects)) {
$this->_localHelperObjects['url'] = new Zend_View_Helper_Url();
$this->_localHelperObjects['url']->setView($view); } $helper =
$this->_localHelperObjects['url']; return
$helper->url($urlOptions, $name, $reset, $encode); } /** * Echo a
message * * Direct implementation. * * @param string $string * @return
string */ public function message($string) { return "" .
$this->escape($message) . "n"; }}
无论哪种方式,该方法将大量降低助手系统的性能消耗,它完全避免了插件载入器的调用,同时还充分利用了自动加载机制和按需加载的特性。
C.4.2. 如何提高区域视图(view partials)性能
如果开发者在应用中大量使用区域(partials),那么他会经常发现partial() 视图助手会因为克隆视图对象而产生性能瓶颈。有办法提高它的速度吗?
C.4.2.1. 仅在真正需要时使用partial()
partial()视图助手接收三个参数:
· $name: 视图脚本的名字
· $module: 视图脚本所在的模块名,或者当没有第三个参数且是一个对象或数组时,它将作为$model参数。
· $model: 数组或对象,作为干净的数据赋值给视图,用于解析区域视图。
partial()的强大之处在于第二与第三个参数。$module参数允许临时加入指定模块的视图路径,从而解析该模块下的区域视图脚本;$model参数允许你显式地为区域视图指定变量。如果你根本没有用到这两个参数,那么请用render()代替!
基本上,除非你需要为区域视图传入变量并需要一个干净的变量环境,或者渲染另外一个MVC模块的视图,你没有必要使用partial()。相反,你应该使用Zend_View内置的render()方法渲染视图。
C.4.3. 如何提高action()视图助手性能
1.5.0版本引入了action()视图助手,允许开发者分发一个MVC动作并捕获它的输出。它向DRY原则迈近了一大步,促进了代码的复用。但是这也
是一个代价高昂的操作。在action()视图助手内部,它会克隆请求和响应对象,调用分发器,然后调用对应的控制器和动作等等。
如何提高它的速度呢?
C.4.3.1. 尽量使用ActionStack代替
与action()视图助手同时加入ZF的动作堆栈,由一个动作助手和前端控制器插件组成。它允许你将分发周期中需要调用的附加动作压入一个栈中。如果你
在布局视图脚本中使用了action(),它可以用动作堆栈替换,完成视图的渲染,实现响应片段的分离。作为例子,你可以像下面那样实现一个
dispatchLoopStartup() 插件,为每个页面添加一个登录表单输入框:
class LoginPlugin extends Zend_Controller_Plugin_Abstract{ protected
$_stack; public function dispatchLoopStartup(
Zend_Controller_Request_Abstract $request ) { $stack =
$this->getStack(); $loginRequest = new
Zend_Controller_Request_Simple();
$loginRequest->setControllerName('user') ->setActionName('index')
->setParam('responseSegment', 'login');
$stack->pushStack($loginRequest); } public function getStack() { if
(null === $this->_stack) { $front =
Zend_Controller_Front::getInstance(); if
(!$front->hasPlugin('Zend_Controller_Plugin_ActionStack')) { $stack =
new Zend_Controller_Plugin_ActionStack();
$front->registerPlugin($stack); } else { $stack =
$front->getPlugin('ActionStack') } $this->_stack = $stack; }
return $this->_stack; }}
然后UserController::indexAction()方法就可以通过responseSegment参数指定渲染哪个响应片段。在布局脚本中,你可以简单地输出该响应片段:
layout()->login ?>
尽管ActionStack需要一个分发周期,但是它比action()视图助手效率要高,因为它不需要克隆对象及重置它们的状态。此外,它可以确保所有的pre/post分发插件会被调用,如果你是通过前端控制器插件实现ACL,那么这一点就显得特别重要。
C.4.3.2. 查询模型时使用视图助手代替action()
大部分情况下,使用action()是小题大做。如果大部分的业务逻辑都封装在模型中,而你只是简单地查询模型,并将结果传递给视图脚本的话,通过一个视图助手获取模型,查询数据,完成相应的操作,将是一个更高效简洁的方法。
作为一个例子,考虑下面的控制器动作和视图脚本:
class BugController extends Zend_Controller_Action{ public function
listAction() { $model = new Bug(); $this->view->bugs =
$model->fetchActive(); }} // bug/list.phtml:echo "n";foreach
($this->bugs as $bug) { printf("•%s: %sn",
$this->escape($bug->id), $this->escape($bug->summary));
•
}echo "n";
使用action(),将像下面这样调用:
action('list', 'bug') ?>
这可以重构为通过视图助手实现,如下所示:
class My_View_Helper_BugList extends Zend_View_Helper_Abstract{ public
function direct() { $model = new Bug(); $html = "n"; foreach
($model->fetchActive() as $bug) { $html .= sprintf( "•%s: %sn",
•
$this->view->escape($bug->id), $this->view->escape($bug->summary) ); } $html .= "n"; return $html; }}
然后像下面这样调用助手:
bugList() ?>
这么做有两个好处:消除了action()助手的开销,并且该API更易于理解。