(转)HHVM 是如何提升 PHP 性能的?
原文地址:http://wuduoyi.com/note/hhvm/ 背景 HHVM 是 Facebook 开发的高性能 PHP 虚拟机,宣称比官方的快9倍,我很好奇,于是抽空简单了解了一下,并整理出这篇文章,希望能回答清楚两方面的问题: HHVM 到底靠谱么?是否可以用到产品中? 它为什么比官方
原文地址:http://wuduoyi.com/note/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 的性能。
更快的 PHP
既然要优化 PHP,那如何去优化呢?在我看来可以有以下几种方法:- 方案1,PHP 语言层面的优化。
- 方案2,优化 PHP 的官方实现(也就是 Zend)。
- 方案3,将 PHP 编译成其它语言的 bytecode(字节码),借助其它语言的虚拟机(如 JVM)来运行。
- 方案4,将 PHP 转成 C/C++,然后编译成本地代码。
- 方案5,开发更快的 PHP 虚拟机。
- 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);
更快的虚拟机
HHVM 为什么更快?在各种新闻报道中都提到了 JIT 这个关键技术,但其实远没有那么简单,JIT 不是什么神奇的魔法棒,用它轻轻一挥就能提升性能,而且 JIT 这个操作本身也是会耗时的,对于简单的程序没准还比 interpreter 慢,最极端的例子是 LuaJIT 2 的 Interpreter 就稍微比 V8 的 JIT 快,所以并不存在绝对的事情,更多还是在细节问题的处理上,HHVM 的发展历史就是不断优化的历史,你可以从下图看到它是如何一点点超过 HPHPc 的:- Andrei Alexandrescu,『Modern C++ Design』和『C++ Coding Standards』的作者,C++ 领域无可争议的大神
- Keith Adams,负责过 VMware 核心架构,当年 VMware 就派他一人去和 Intel 进行技术合作,足以证明在 VMM 领域他有多了解了
- Drew Paroski,在微软参与过 .NET 虚拟机开发,改进了其中的 JIT
- Jason Evans,开发了 jemalloc,减少了 Firefox 一半的内存消耗
- Sara Golemon,『Extending and Embedding PHP』的作者,PHP 内核专家,这本书估计所有 PHP 高手都看过吧,或许你不知道其实她是女的
规范是什么?
自己写 PHP 虚拟机要面临的第一个问题就是 PHP 没有语言规范,很多版本间的语法还会不兼容(甚至是小版本号,比如 5.2.1 和 5.2.3),PHP 语言规范究竟如何定义呢?来看一篇来自 IEEE 的说法:The PHP group claim that they have the ?nal say in the speci?cation of (the language) PHP. This groups speci?cation is an implementation, and there is no prose speci?cation or agreed validation suite.所以唯一的途径就是老老实实去看 Zend 的实现,好在 HPHPc 中已经痛苦过一次了,所以 HHVM 能直接利用现成,因此这个问题并不算太大。
语言还是扩展?
实现 PHP 语言不仅仅只是实现一个虚拟机那么简单,PHP 语言本身还包括了各种扩展,这些扩展和语言是一体的,Zend 不辞辛劳地实现了各种你可能会用到的功能。如果分析过 PHP 的代码,就会发现它的 C 代码除去空行注释后居然还有80+万行,而你猜其中 Zend 引擎部分有多少?只有不到10万行。 对于开发者来说这不是什么坏事,但对于引擎实现者来说就很悲剧了,我们可以拿 Java 来进行对比,写个 Java 的虚拟机只需实现字节码解释及一些基础的 JNI 调用,Java 绝大部分内置库都是用 Java 实现的,所以如果不考虑性能优化,单从工作量看,实现 PHP 虚拟机比 JVM 要难得多,比如就有人用8千行的 TypeScript 实现了一个 JVM Doppio。 而对于这个问题,HHVM 的解决办法很简单,那就是只实现 Facebook 中用到的,而且同样可以先用 HPHPc 中之前写过的,所以问题也不大。实现 Interpreter
接下来是 Interpreter 的实现,在解析完 PHP 后会生成 HHVM 自己设计的一种 Bytecode,存储在~/.hhvm.hhbc
(SQLite 文件) 中以便重用,在执行 Bytecode 时和 Zend 类似,也是将不同的字节码放到不同的函数中去实现(这种方式在虚拟机中有个专门的称呼:Subroutine threading)
Interpreter 的主体实现在 bytecode.cpp 中,比如 VMExecutionContext::iopAdd
这样的方法,最终执行会根据不同类型来区分,比如 add 操作的实现是在 tv-arith.cpp 中,下面摘抄其中的一小段
if (c2.m_type == KindOfInt64) return o(c1.m_data.num, c2.m_data.num); if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl);
m_data.num
和m_data.dbl
的方法来间接获取。
对于这样的问题,就得靠 JIT 来优化了。
实现 JIT 及优化
首先值得一提的是 PHP 的 JIT 之前并非没人尝试过:- 2008 年就有人用 LLVM 实验过,结果还比原来慢了 21 倍。。。
- 2010 年 IBM 日本研究院基于他们的 JVM 虚拟机代码开发了 P9,性能是官方 PHP 的 2.5 到 9.5 倍,可以看他们的论文Evaluation of a just-in-time compiler retrofitted for PHP。
- 2011 年 Andrei Homescu 基于 RPython 开发过,还写了篇论文 HappyJIT: a tracing JIT compiler for PHP,但测试结果有好有坏,并不理想。
unsigned char code[] = { 0x48, 0x89, 0xf8, // mov %rdi, %rax 0x48, 0x83, 0xc0, 0x04, // add $4, %rax 0xc3 // ret }; memcpy(m, code, sizeof(code));
- trace:记录循环执行次数,如果超过一定数量就对这段代码进行 JIT
- method:记录函数执行次数,如果超过一定数量就对整个函数进行 JIT,甚至直接 inline
$k
为整数或字符串两种不同情况的,下面的部分是返回值,所以看起来它主要是根据类型的变化情况来划分 JIT 区域的,具体是如何分析和拆解 Tracelet 的细节可以查看Translator.cpp 中的 Translator::analyze
方法,我还没空看,这里就不讨论了。
当然,要实现高性能的 JIT 还需进行各种尝试和优化,比如最初 HHVM 新增的 tracelet 会放到前面,也就是将上图的 A 和 C 调换位置,后来尝试了一下放到后面,结果性能提示了 14%,因为测试发现这样更容易提前命中响应的类型
JIT 的执行过程是首先将 HHBC 转成 SSA (hhbc-translator.cpp),然后对 SSA 上做优化(比如 Copy propagation),再生成本地机器码,比如在 X64 下是由 translator-x64.cpp 实现的。
我们用一个简单的例子来看看 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
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); }
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 的代码,只能期望它比较稳定了。
引用
- 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
原文地址:(转)HHVM 是如何提升 PHP 性能的?, 感谢原作者分享。

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

Video Face Swap
使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

PHP 8.4 帶來了多項新功能、安全性改進和效能改進,同時棄用和刪除了大量功能。 本指南介紹如何在 Ubuntu、Debian 或其衍生版本上安裝 PHP 8.4 或升級到 PHP 8.4

Visual Studio Code,也稱為 VS Code,是一個免費的原始碼編輯器 - 或整合開發環境 (IDE) - 可用於所有主要作業系統。 VS Code 擁有大量針對多種程式語言的擴展,可以輕鬆編寫

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元

本教程演示瞭如何使用PHP有效地處理XML文檔。 XML(可擴展的標記語言)是一種用於人類可讀性和機器解析的多功能文本標記語言。它通常用於數據存儲

靜態綁定(static::)在PHP中實現晚期靜態綁定(LSB),允許在靜態上下文中引用調用類而非定義類。 1)解析過程在運行時進行,2)在繼承關係中向上查找調用類,3)可能帶來性能開銷。

PHP的魔法方法有哪些? PHP的魔法方法包括:1.\_\_construct,用於初始化對象;2.\_\_destruct,用於清理資源;3.\_\_call,處理不存在的方法調用;4.\_\_get,實現動態屬性訪問;5.\_\_set,實現動態屬性設置。這些方法在特定情況下自動調用,提升代碼的靈活性和效率。
