产品服务AMH 免费服务器主机面板SSL证书 免费SSL证书申请 担保单 免费安全、零费率交易平台编程助手免费智能写代码、翻译AMYSQL 免费MySQL管理工具

AMH 社区首页

 AMH社区 - 开放自由有价值的社区

[综合话题] HHVM 是如何提升 PHP 性能的?

liudiary
金牌会员
6471.83 价值分

liudiary 发表于 2014-10-03 08:23:55
转发这篇文章的目的在于让各位了解一下HHVM,确实是一个对性能帮助很大的虚拟机。

背景
HHVM 是 Facebook 开发的高性能 PHP 虚拟机,宣称比官方的快9倍,我很好奇,于是抽空简单了解了一下,并整理出这篇文章,希望能回答清楚两方面的问题:
[*]HHVM 到底靠谱么?是否可以用到产品中?
[*]它为什么比官方的 PHP 快很多?到底是如何优化的?
你会怎么做?
在讨论 HHVM 实现原理前,我们先设身处地想想:假设你有个 PHP 写的网站遇到了性能问题,经分析后发现很大一部分资源就耗在 PHP 上,这时你会怎么优化 PHP 性能?
比如可以有以下几种方式:
[*]方案1,迁移到性能更好的语言上,如 Java、C++、Go。
[*]方案2,通过 RPC 将功能分离出来用其它语言实现,让 PHP 做更少的事情,比如 Twitter 就将大量业务逻辑放到了 Scala 中,前端的 Rails 只负责展现。
[*]方案3,写 PHP 扩展,在性能瓶颈地方换 C/C++。
[*]方案4,优化 PHP 的性能。

方案1几乎不可行,十年前 Joel 就拿 Netscape 的例子警告过,你将放弃是多年的经验积累,尤其是像 Facebook 这种业务逻辑复杂的产品,PHP 代码实在太多了,据称有2千万行(引用自 [PHP on the Metal with HHVM]),修改起来的成本恐怕比写个虚拟机还大,而且对于一个上千人的团队,从头开始学习也是不可接受的。
方案2是最保险的方案,可以逐步迁移,事实上 Facebook 也在朝这方面努力了,而且还开发了 Thrift 这样的 RPC 解决方案,Facebook 内部主要使用的另一个语言是 C++,从早期的 Thrift 代码就能看出来,因为其它语言的实现都很简陋,没法在生产环境下使用。
目前在 Facebook 中据称 PHP:C++ 已经从 9:1 增加到 7:3 了,加上有 Andrei Alexandrescu 的存在,C++ 在 Facebook 中越来越流行,但这只能解决部分问题,毕竟 C++ 开发成本比 PHP 高得多,不适合用在经常修改的地方,而且太多 RPC 的调用也会严重影响性能。
方案3看起来美好,实际执行起来却很难,一般来说性能瓶颈并不会很显著,大多是不断累加的结果,加上 PHP 扩展开发成本高,这种方案一般只用在公共且变化不大的基础库上,所以这种方案解决不了多少问题。
可以看到,前面3个方案并不能很好地解决问题,所以 Facebook 其实没有选择的余地,只能去考虑 PHP 本身的优化了。

2014-10-03 08:23:55 1

liudiary
金牌会员
6471.83 价值分

更快的 PHP
既然要优化 PHP,那如何去优化呢?在我看来可以有以下几种方法:
[*]方案1,PHP 语言层面的优化。
[*]方案2,优化 PHP 的官方实现(也就是 Zend)。
[*]方案3,将 PHP 编译成其它语言的 bytecode(字节码),借助其它语言的虚拟机(如 JVM)来运行。
[*]方案4,将 PHP 转成 C/C++,然后编译成本地代码。
[*]方案5,开发更快的 PHP 虚拟机。

PHP 语言层面的优化是最简单可行的,Facebook 当然想到了,而且还开发了 XHProf 这样的性能分析工具,对于定位性能瓶颈是很有帮助的。
不过 XHProf 还是没能很好解决 Facebook 的问题,所以我们继续看,接下来是方案2,简单来看,Zend 的执行过程可以分为两部分:将 PHP 编译为 opcode、执行 opcode,所以优化 Zend 可以从这两方面来考虑。
优化 opcode 是一种常见的做法,可以避免重复解析 PHP,而且还能做一些静态的编译优化,比如 Zend Optimizer Plus,但由于 PHP 语言的动态性,这种优化方法是有局限性的,乐观估计也只能提升20%的性能。另一种考虑是优化 opcode 架构本身,如基于寄存器的方式,但这种做法修改起来工作量太大,性能提升也不会特别明显(可能30%?),所以投入产出比不高。
另一个方法是优化 opcode 的执行,首先简单提一下 Zend 是如何执行的,Zend 的 interpreter(也叫解释器)在读到 opcode 后,会根据不同的 opcode 调用不同函数(其实有些是 switch,不过为了描述方便我简化了),然后在这个函数中执行各种语言相关的操作(感兴趣的话可看看深入理解 PHP 内核这本书),所以 Zend 中并没有什么复杂封装和间接调用,作为一个解释器来说已经做得很好了。
想要提升 Zend 的执行性能,就需要对程序的底层执行有所解,比如函数调用其实是有开销的,所以能通过 Inline threading 来优化掉,它的原理就像 C 语言中的 inline 关键字那样,但它是在运行时将相关的函数展开,然后依次执行(只是打个比方,实际实现不太一样),同时还避免了 CPU 流水线预测失败导致的浪费。
另外还可以像 JavaScriptCoreLuaJIT 那样使用汇编来实现 interpreter,具体细节建议看看 Mike 的解释
但这两种做法修改代价太大,甚至比重写一个还难,尤其是要保证向下兼容,后面提到 PHP 的特点时你就知道了。
开发一个高性能的虚拟机不是件简单的事情,JVM 花了10多年才达到现在的性能,那是否能直接利用这些高性能的虚拟机来优化 PHP 的性能呢?这就是方案3的思路。
其实这种方案早就有人尝试过了,比如 Quercus 和 IBM 的 P8,Quercus 几乎没见有人使用,而 P8 也已经死掉了。Facebook 也曾经调研过这种方式,甚至还出现过不靠谱的传闻 ,但其实 Facebook 在2011年就放弃了。
因为方案3看起来美好,但实际效果却不理想,按照很多大牛的说法(比如 Mike),VM 总是为某个语言优化的,其它语言在上面实现会遇到很多瓶颈,比如动态的方法调用,关于这点在 Dart 的文档中有过介绍,而且据说 Quercus 的性能与 Zend+APC 比差不了太多([来自The HipHop Compiler for PHP]),所以没太大意义。
不过 OpenJDK 这几年也在努力,最近的 Grall 项目看起来还不错,也有语言在上面取得了显著的效果,但我还没空研究 Grall,所以这里无法判断。
接下来是方案4,它正是 HPHPc(HHVM 的前身)的做法,原理是将 PHP 代码转成 C++,然后编译为本地文件,可以认为是一种 AOT(ahead of time)的方式,关于其中代码转换的技术细节可以参考 The HipHop Compiler for PHP 这篇论文,以下是该论文中的一个截图,可以通过它来大概了解:

这种做法的最大优点是实现简单(相对于一个 VM 来说),而且能做很多编译优化(因为是离线的,慢点也没事),比如上面的例子就将- 1优化掉了,但它很难支持 PHP 中的很多动态的方法,如 eval()、create_function(),因为这就得再内嵌一个 interpreter,成本不小,所以 HPHPc 干脆就直接不支持这些语法。
除了 HPHPc,还有两个类似的项目,一个是 Roadsend,另一个是 phc ,phc 的做法是将 PHP 转成了 C 再编译,以下是它将 file_get_contents($f) 转成 C 代码的例子:static php_fcall_info fgc_info;php_fcall_info_init ("file_get_contents", &fgc_info);php_hash_find (LOCAL_ST, "f", 5863275, &fgc_info.params);php_call_function (&fgc_info);

话说 phc 作者曾经在博客上哭诉,说他两年前就去 Facebook 演示过 phc 了,还和那里的工程师交流过,结果人家一发布就火了,而自己忙活了4年却默默无闻,现在前途渺茫。。。
Roadsend 也已经不维护了,对于 PHP 这样的动态语言来说,这种做法有很多的局限性,由于无法动态 include,Facebook 将所有文件都编译到了一起,上线时的文件部署居然达到了 1G,越来越不可接受了。
另外有还有一个叫 PHP QB 的项目,由于时间关系我没有看,感觉可能是类似的东东。
所以就只剩下一条路了,那就是写一个更快的 PHP 虚拟机,将一条黑路走到底,或许你和我一样,一开始听到 Facebook 要做一个虚拟机是觉得太离谱,但如果仔细分析就会发现其实也只有这样了。
  支持 (0分)  反对 (0分)
回复  2014-10-03 08:24:26 2

liudiary
金牌会员
6471.83 价值分



我们用一个简单的例子来看看 HHVM 最终生成的机器码是怎样的,比如下面这个 PHP 函数:

<?php
function a($b){
echo $b + 2;
}
编译后是这个样子:

mov rcx,0x7200000
mov rdi,rbp
mov rsi,rbx
mov rdx,0x20
call 0x2651dfb <HPHP::Transl::traceCallback(HPHP::ActRec*, HPHP::TypedValue*, long, void*)>
cmp BYTE PTR [rbp-0x8],0xa
jne 0xae00306
; 前面是检查参数是否有效

mov rcx,QWORD PTR [rbp-0x10] ; 这里将 %rcx 被赋值为1了
mov edi,0x2 ; 将 %edi(也就是 %rdi 的低32位)赋值为2
add rdi,rcx ; 加上 %rcx
call 0x2131f1b <HPHP::print_int(long)> ; 调用 print_int 函数,这时第一个参数 %rdi 的值已经是3了

; 后面暂不讨论
mov BYTE PTR [rbp+0x28],0x8
lea rbx,[rbp+0x20]
test BYTE PTR [r12],0xff
jne 0xae0032a
push QWORD PTR [rbp+0x8]
mov rbp,QWORD PTR [rbp+0x0]
mov rdi,rbp
mov rsi,rbx
mov rdx,QWORD PTR [rsp]
call 0x236b70e <HPHP::JIT::traceRet(HPHP::ActRec*, HPHP::TypedValue*, void*)>
ret
而 HPHP::print_int 函数的实现是这样的:

void print_int(int64_t i) {
char buf[256];
snprintf(buf, 256, "%" PRId64, i);
echo(buf);
TRACE(1, "t-x64 output(int): %" PRId64 "\n", i);
}
可以看到 HHVM 编译出来的代码直接使用了 int64_t,避免了 interpreter 中需要判断参数和间接取数据的问题,从而明显提升了性能,最终甚至做到了和 C 编译出来的代码区别不大。

需要注意:HHVM 在 server mode 下,只有超过12个请求就才会触发 JIT,启动过 HHVM 时可以通过加上如下参数来让它首次请求就使用 JIT:

-v Eval.JitWarmupRequests=0
所以在测试性能时需要注意,运行一两次就拿来对比是看不出效果的。

类型推导很麻烦,还是逼迫程序员写清楚吧
JIT 的关键是猜测类型,因此某个变量的类型要是老变就很难优化,于是 HHVM 的工程师开始考虑在 PHP 语法上做手脚,加上类型的支持,推出了一个新语言 - Hack(吐槽一下这名字真不利于 SEO),它的样子如下:

<?hh
class Point2 {
public float $x, $y;
function __construct(float $x, float $y) {
$this->x = $x;
$this->y = $y;
}
}
//来自:https://raw.github.com/strangeloop/StrangeLoop2013/master/slides/sessions/Adams-TakingPHPSeriously.pdf
注意到 float 关键字了么?有了静态类型可以让 HHVM 更好地优化性能,但这也意味着和 PHP 语法不兼容,只能使用 HHVM。

其实我个人认为这样做最大的优点是让代码更加易懂,减少无意的犯错,就像 Dart 中的可选类型也是这个初衷,同时还方便了 IDE 识别,据说 Facebook 还在开发一个基于 Web 的 IDE,能协同编辑代码,可以期待一下。

你会使用 HHVM 么?
总的来说,比起之前的 HPHPc,我认为 HHVM 是值得一试的,它是真正的虚拟机,能够更好地支持各种 PHP 的语法,所以改动成本不会更高,而且因为能无缝切换到官方 PHP 版本,所以可以同时启动 FPM 来随时待命,HHVM 还有FastCGI 接口方便调用,只要做好应急备案,风险是可控的,从长远来看是很有希望的。

性能究竟能提升多少我无法确定,需要拿自己的业务代码来进行真实测试,这样才能真正清楚 HHVM 能带来多少收益,尤其是对整体性能提升到底有多少,只有拿到这个数据才能做决策。

最后整理一下可能会遇到的问题,有计划使用的可以参考:

扩展问题:如果用到了 PHP 扩展,肯定是要重写的,不过 HHVM 扩展写起来比 Zend 要简单的多,具体细节可以看 wiki 上的例子。
HHVM Server 的稳定性问题:这种多线程的架构运行一段时间可能会出现内存泄露问题,或者某个没写好的 PHP 直接导致整个进程挂掉,所以需要注意这方面的测试和容灾措施。
问题修复困难:HHVM 在出现问题时将比 Zend 难修复,尤其是 JIT 的代码,只能期望它比较稳定了。
P.S. 其实我只了解基本的虚拟机知识,也没写过几行 PHP 代码,很多东西都是写这篇文章时临时去找资料的,由于时间仓促水平有限,必然会有不正确的地方,欢迎大家评论赐教 :)

2014年1月补充:目前 HHVM 在鄙厂的推广势头很不错,推荐大家在2014年尝试一下,尤其是现在兼容性测试已经达到98.58%了,修改成本进一步减小。

引用
Andrei Alexandrescu on AMA
Keith Adams 在 HN 上的蛛丝马迹
How Three Guys Rebuilt the Foundation of Facebook
PHP on the Metal with HHVM
Making HPHPi Faster
HHVM Optimization Tips
The HipHop Virtual Machine (hhvm) PHP Execution at the Speed of JIT
Julien Verlaguet, Facebook: Analyzing PHP statically
Speeding up PHP-based development with HHVM
Adding an opcode to HHBC
来自http://wuduoyi.com/note/hhvm/
  支持 (0分)  反对 (0分)
回复  2014-10-03 08:24:49 3

72135
金牌会员
5476.50 价值分

什么啊这么长
  支持 (0分)  反对 (0分)
回复  2014-10-03 17:32:38 4

liudiary
金牌会员
6471.83 价值分

引用:
72135 发表于 2014-10-3 17:32
什么啊这么长


科普文。。。
  支持 (0分)  反对 (0分)
回复  2014-10-03 17:33:45 5

molinxx
铜牌会员
643.00 价值分

关于HHVM,还在观望,折腾可以,生产环境就算了,毕竟不是100%兼容PHP的函数,wordpress的原生程序支持很好,但是插件会有不兼容现象,Discuz更不用说了……
  支持 (0分)  反对 (0分)
回复  2014-10-03 17:39:46 6

liudiary
金牌会员
6471.83 价值分

引用:
molinxx 发表于 2014-10-3 17:39
关于HHVM,还在观望,折腾可以,生产环境就算了,毕竟不是100%兼容PHP的函数,wordpress的原生程序支持很好 ...


我还是使用LAMP 能够体会到apache并发问题的网站一般流量都比较可怕
  支持 (0分)  反对 (0分)
回复  2014-10-03 18:52:34 7

molinxx
铜牌会员
643.00 价值分

引用:
liudiary 发表于 2014-10-3 18:52
我还是使用LAMP 能够体会到apache并发问题的网站一般流量都比较可怕


所有生产环境都转移到LNMP了,apache高并发的优化不大好做,基本就是堆硬件,nginx相对好优化一些~
  支持 (0分)  反对 (0分)
回复  2014-10-03 21:30:54 8

liudiary
金牌会员
6471.83 价值分

引用:
molinxx 发表于 2014-10-3 21:30
所有生产环境都转移到LNMP了,apache高并发的优化不大好做,基本就是堆硬件,nginx相对好优化一些~ ...


nginx是编译优化么?
  支持 (0分)  反对 (0分)
回复  2014-10-03 21:44:11 9

7683002
银牌会员
1695.49 价值分

现在跑wordpress博客首选lnmh,做站还是lnmp 支持
  支持 (0分)  反对 (0分)
回复  2014-10-04 00:18:15 10
 1 2 >  (总2页)
AMH社区列表
用户服务中心