PHP(四)——性能优化

之前基于PHP开发的过程中,一直没有涉及到PHP性能优化的问题,但是一般来说PHP性能问题占整个项目性能问题一般占30%-50%部分,所以说,这部分内容是非常重要的。下面是最近自己PHP性能优化学习的资料整理。

引言[1]

  • PHP存在性能问题的情景

    • PHP语法使用不恰当
    • 使用了PHP语言他不擅长做的事情
    • 用PHP语言连接的服务不给力
    • PHP自身的短板
    • 未知的问题
  • PHP性能问题解决方向

    • PHP语言级的性能优化:日常语法方法的优化。特点:简单高效很快见到效果。
    • PHP周边问题的性能优化 :webserver,mysql。
    • PHP语言自身的分析和优化 :PHP底层C语言逻辑的优化。

从1~5的顺序,按照操作简单,见效快的指标进行的解决方案的排序。

  • Apache压力测试软件

    • Apache Benchmark,简称ab,是由Apache提供的压力测试软件,安装apache服务器时会自带压测软件。

    • 使用./ab -n100 -c10 http://www.baidu.com
      其中-n请求数,-c并发数,url目标。

    • ab 返回结果的参数

      • Requests per second 每秒请求数 (优化目标 每秒的请求数尽可能多)
      • Time per request 响应一个请求耗时 (优化目标 响应一个请求尽可能少)

PHP语言级性能优化

  • 基本原则:少写php代码,多使用PHP内置的变量、常量、函数。

  • 性能问题:自写代码冗余较多,可读性不佳,并且性能低。php代码写的越长长执行效果就会越差,多用php自身的函数等

    因为PHP代码需要编译为C语言,C语言又会编译成汇编语言(机器语言),这里每一个过程都会请求一遍,开销很大。尤其是访问量大的时候,每次都会编译一遍。所以要尽量减少代码。

  • PHP代码如何再linux解析的流程

    • 逐行扫描。*.php 通过zend引擎逐行扫描分析(Scanner)
    • 转码。保存成zend引擎自己能识别的语法(Exprs)
    • 解析为Opcodes。这些zend引擎能识别的语法,再解析(Parser)成Opcodes(最终要拿去执行的机器代码)。
    • 输出。执行Opcodes,然后输出。

      缓存服务都是缓存的Opcodes,不需要扫描和解析,

      PHP_process

PHP语言级性能优化建议

  • PHP内置函数的性能优劣
    • 尽量使用更快内置函数,不同函数依然存在快慢差异。
    • 尽量少用魔法函数,魔法函数性能低。为了给程序员省事,php语言为你做了很多linux time函数 可以直接测试程序的耗时情况 魔法函数举例:__get();可以不用尽量不用,如果必须要用的时候再用。
    • 不使用@错误抑制符,改用try throw
  • 合理使用内存
    • 情况描述:php有内存回收机制保底,但也要小心使用内存
    • 建议:利用unset()及时释放不使用的内存(注:unset出现注销不掉的情况,自己查资料)
  • 尽量少的使用正则表达式
    • 情况描述:正则表达式性能低,因为正则表达式回溯开销较大
    • 好的建议:利用字符串处理函数,实现相同的逻辑
  • 避免循环内做运算
    • 情况描述:循环内的计算式会被重复计算
    • 例 for($i=0;$i<strlen($str);$i++) 每一次for循环都会进行计算strlen
  • 减少计算密集型业务
    • 情况描述:PHP不适合密集型运算场景
    • 原因:
      • 比如不适合大批量日志分析,或者大批量数据处理。
      • php语言特性决定了PHP不适合做大数据运算
      • php所有处理都需要转换成C语言,与C相比,C更好。
      • php还有环境问题,还有语言特性。额外开销比C大很多。变量寄存等。
    • PHP适合处理场景:适合衔接webserver与后端服务、UI呈现。(就是接口,简单数据处理,和套页面)
  • 务必使用带引号字符串做键值:php会将没有引号的键值检查一遍是不是常量,产生查询常量的开销

PHP周边问题的性能优化

  • PHP周边

    • linux环境
    • 硬盘,文件存储,php读写
    • 数据库
    • 缓存:缓存是基于内存的
    • 网络
  • PHP部署环境优化

    单台服务器常用apache+phpnginx+php-fpm方式部署,据说现在用nginx+php-fpm部署方式性能比apache+php性能好,可考虑一试。另外如nginx+swoole等,也是可选项。

    集群是在此基础上,使用nginx/lvs/云上lbs等反向代理作为负载均衡前端。PHP集群部署在可靠性的基础上,PHP集群处理性能比单台服务器有N倍提高(但作为服务的整体性能并不一定有N倍提升)。所以简单地可以认为,通过集群扩展服务器,可以使PHP服务性能得到提升。

性能优化推荐

  • 减少文件类操作。常见PHP场景的开销次序:读写内存 < 读写数据库 < 读写磁盘 < 读写网络数据
  • 减少php发起网络请求,优化网络请求

    • 网络请求的坑:1对方接口的不确定因素 2 网络稳定性
    • 优化网络请求方法

      • 设置超时时间(建议值)

        • 连接超时 200ms 这是上限,最多也不能超过这个时间
        • 读超时 800ms 这个看具体情况
        • 写超时 500ms 建议不要超过500ms
      • 将串行请求并行化

        • 使用curl_multi_*() 返回时间是看用时最长的那个请求
        • 使用swoole扩展,通过C来进行并行化。(推荐使用)
  • 压缩PHP接口输出:如果用php做接口可通过使用Gzip压缩实现更高效的输出。压缩输出的利弊:
    • 利:利于数据输出,client能更快获取数据
    • 弊:额外的CPU开销。如果请求大,可能会有问题

      gzip如果数据量小于几十K的时候效果并不理想。如果大于100k,压缩就有效果。

  • 缓存重复计算内容:固定重复请求的数据做缓存。
  • 重叠时间窗口思想:串行变并行。如果后一个请求不强依赖于前一个返回值。就可以变成并行,降低总体时间消耗

PHP语言自身的分析和优化

  • PHP扩展使用[2],PHP扩展除了使用方便,还是提升性能的亲密伙伴。主要应用有三点:

    • 开启opcode的缓存,来避免重复的编译。可以使用APC,eAccelerator,XCache等PHP扩展,我们使用xcache。这种只要安装即可。

    • 使用扩展提供的方法(或PHP标准库的方法)。通过PHP扩展代替原PHP代码中高频的逻辑,扩展实现的效率比PHP代码中的高。但实际上满足我们项目的扩展方法有限,很多基础方法需要时一步封装,除非有能力自己开发扩展。可考虑使用扩展实现的PHP框架,如phalcon、yaf。

    • 本地缓存,也常用扩展来支持,比如xcache。本地可使用缓存扩展,缓存一些配置数据、元数据或主数据,不用每次都从数据库或文件中读取。

另外,PHP版本上,可以考虑升到PHP7,PHP7在性能上有很大的提升。

  • Runtime优化:HHVM[3](phpng也许更优于HHVM)

具体的PHP语言级的优化建议[4]

  1. 用单引号替代双引号引用字符串,这样做会更快一些。因为PHP会在双引号包围的字符串中搜寻变量,单引号则不会。

  2. 如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍。

  3. $row[‘id’] 的速度是$row[id]的7倍。

  4. echo 比 print 快,并且使用echo的多重参数(译注:指用‘,’号而不是‘.’)代替字符串连接,比如echo $str1,$str2。区别用‘.’,先拼接,在整个输出;用‘,’,是挨个把三个变量输出。

  5. 在执行for循环之前先确定最大循环数,不要每循环一次都计算最大值。

    1
    2
    // 不合理,每次循环都要计算
    for($i=0;$i<strlen($str);$i++)
  6. foreach效率更高,尽量用foreach代替while和for循环,如果考虑到foreach($array as $var)每次拷贝的消耗,可以使用foreach($array as &$var)这样的引用。

  7. 尽量避免使用魔术变量,如__get__set__autoload

  8. require_once()代价昂贵。require_onceinclude_once需要判重,因此效率上要低,但是5.2版本后效率问题已经基本解决。

  9. include文件时尽量使用绝对路径,因为它避免了PHP去include_path里查找文件的速度,解析操作系统路径所需的时间会更少。尽量少用iniset()来设置include_path

  10. 返回脚本开始执行(即服务器端收到客户端请求)的时刻,使用$_SERVER['REQUEST_TIME']要好于time()。因为$_SERVER['REQUEST_TIME']保存了发起该请求时刻的时间戳,而time()则返回当前时刻的Unix时间戳。

  11. 函数代替正则表达式完成相同功能,字符串操作比正则替换要快。如strtokstrstrstrposstr_replacesubstrexplodeimplode等等。注意不同的函数快慢也不同,str_replace函数比preg_replace函数快,但strtr函数的效率是str_replace函数的四倍。

  12. 使用选择分支语句(译注:即switch case)好于使用多个if,else if语句。因为php中switch支持数值和字符串变量,比C的switch要好用,建议使用。
  1. 数据库连接当使用完毕时应关掉,不要用长连接。建议在连接之前,最好设置一下相应的超时机制,例如链接超时、读写超时、等待超时等。

  2. 错误消息代价昂贵。所以说在代码测试完成,上线之前删除错误信息报告代码。

  3. 在方法中递增局部变量,速度是最快的。几乎与在函数中调用局部变量的速度相当。递增一个全局变量要比递增一个局部变量慢2倍。递增一个对象属性(如:$this->prop++)要比递增一个局部变量慢3倍。递增一个未预定义的局部变量要比递增一个预定义的局部变量慢9至10倍。仅定义一个局部变量而没在函数中调用它,同样会减慢速度(其程度相当于递增一个局部变量)。

  4. 派生类中的方法运行起来要快于在基类中定义的同样的方法。

  5. Apache解析一个PHP脚本的时间要比解析一个静态HTML页面慢2至10倍。尽量多用静态HTML页面,少用脚本。

  6. 除非脚本可以缓存,否则每次调用时都会重新编译一次。引入一套PHP缓存机制通常可以提升25%至100%的性能,以免除编译开销。

  7. 尽量可使用memcached等做缓存。memcached是一款高性能的内存对象缓存系统,可用来加速动态Web应用程序,减轻数据库负载。对运算码(OPcode)的缓存很有用,使得脚本不必为每个请求做重新编译。

  8. 当执行变量$i的递增或递减时,$i++会比++$i慢一些。这种差异是PHP特有的,并不适用于其他语言。++$i更快是因为它只需要3条指令(opcodes),$i++则需要4条指令。后置递增实际上会产生一个临时变量,这个临时变量随后被递增。而前置递增直接在原值上递增。这是最优化处理的一种,正如Zend的PHP优化器所作的那样。

  9. 面向对象(OOP)是非必要的,因为面向对象往往开销很大,每个方法和对象调用都会消耗很多内存。并非要用类实现所有的数据结构,数组也很有用。

  10. 不要把方法细分得过多,仔细想想你真正打算重用的是哪些代码?不要过分迷恋各种设计模式,如上一条描述,过分的封装会带来性能的下降。需要考虑两者的权衡。Php有自己的特点,切不可东施效颦,过分效仿java的模式。

  11. 分解成方法要适当,行数少使用频率高的方法尽量用直接写代码,可以减少函数堆栈开销;且方法嵌套不宜过深,否则大大影响PHP的运行效率。

  12. 尽量采用大量的PHP内置函数,除去空函数调用的影响,内置函数和同样功能的C函数性能基本差不多。

  13. 如果在代码中存在大量耗时的函数,你可以考虑用C扩展的方式实现它们。

  14. 打开apache的mod_deflate模块,可以提高网页的浏览速度。mod_zip可作为Apache模块,用来即时压缩你的数据,并可让数据传输量降低80%。

  15. 在可以用file_get_contents替代file、fopen、feof、fgets等系列方法的情况下,尽量用file_get_contents,因为他的效率高得多!但是要注意file_get_contents在打开一个URL文件时候的PHP版本问题。这个要记住,尽量使用file_get_contentsfile_put_contents,不需要自己判断文件句柄打开是否成功。

  16. 尽量的少进行文件操作,虽然PHP的文件操作效率也不低的;

  17. 优化Select SQL语句,在可能的情况下尽量少的进行Insert、Update操作(在update上,我被恶批过);

  18. 循环内部不要声明变量,尤其是大变量:对象(这好像不只是PHP里面要注意的问题吧?)。这个必须的,变量过多或者过大时,每次重分配的开销就无法忽略。

  19. 多维数组尽量不要循环嵌套赋值;

  20. 对global变量,应该用完就unset()释放掉;

  21. 当操作字符串并需要检验其长度是否满足某种要求时,使用isset()替代strlen()函数。isset()作为一种语言结构,它的执行不需要函数查找和字母小写化。此函数执行起来相当快,只返回在zval结构(C的内置数据结构,用于存储PHP变量)中存储的已知字符串长度。但是,由于strlen()是函数更慢,因为函数调用会经过诸多步骤,如字母小写化(指函数名小写化,PHP不区分函数名大小写)、哈希查找,会跟随被调用的函数一起执行。在某些情况下,你可以使用isset() 技巧加速执行你的代码。   

    1
    2
    3
    4
    // (举例如下)
    if (strlen($foo) < 5) { echo “Foo is too short”$$ }
    // (与下面的技巧做比较)
    if (!isset($foo{5})) { echo “Foo is too short”$$ }
  22. 评估检验(profile)你的代码。检验器会告诉你,代码的哪些部分消耗了多少时间。Xdebug调试器包含了检验程序,评估检验总体上可以显示出代码的瓶颈。

  23. 函数相关信息保存在一个大的hash_table中,每次调用时通过函数名在hash表中查找,因此函数名长度对性能也有一定影响。

  24. 函数不宜嵌套过深,递归使用要谨慎。

  25. 如不是特殊需要,参数传递都建议使用传值而不是传引用。当然,如果参数是很大的数组且需要修改时可以考虑引用传递。

  26. 使用NoSQL、Memchached或者Redis缓存。这些是高性能的分布式内存对象缓存系统,能提高动态网络应用程序性能,减轻数据库的负担。这对运算码 (OPcode)的缓存也很有用,使得脚本不必为每个请求重新编译。

参考

[1] 慕课网 – 性能优化之PHP优化总结笔记视频地址

[2] PHP 性能优化

[3] 关于 PHP 性能优化

[4] 一些PHP性能的优化

[5] 48条高效率的PHP优化写法