原文:http://hhvm.com/blog/713/hhvm-optimization-tips
本文进行简单的翻译、总结与分析。
前言
HHVM’s JIT compiler allows it to execute PHP faster than Zend PHP in most cases, unmodified. However, we've gotten some interest from the community in HipHop-specific optimization tips, so I put together a few for this post.
The principle that underlies all of these tips is: write code that HHVM can understand without running. The more HHVM knows before the code runs, the more it can optimize.
为了写出适合被HHVM虚拟机优化的代码,总体的核心思路就是:你的代码必须能够在仅仅静态分析(不运行)的过程中,尽可能被HHVM理解,也就是能够被类型推导。
不要将热点代码写在全局作用域中
Keep Hot Code Out of Global Scope
Code that runs in global scope is never passed to the JIT compiler. If, for example, you have a script that has a tight loop of computation, make sure that the loop isn’t in global scope, by putting it inside a function.
全局作用域中的PHP代码不会被HHVM所优化,如果你的代码在全局作用域中有循环热点之类的,就得保证它不在全局作用域,比如把它放进函数里面去。
The reason HHVM doesn’t JIT-compile code in global scope is that any code, anywhere, can mutate the “local” variables of global scope. This severely hampers the JIT compiler’s ability to track types of local variables. Here’s a little quiz that demonstrates what I’m talking about:
HHVM不对全局代码进行JIT优化的原因在于,任何地方的代码都有可能改变全局作用域中变量的状态,这严重破坏了JIT推导变量类型的能力。下面是个简单的问题:在什么情况下,执行第二句echo $b;的时候,变量$a会被改变呢?
// This is in global scope. What code could we put here that
// will result in $a being mutated by the statement "echo $b;"?
$a = 20;
echo $b;
Here are two possible answers.
function handler() {
$GLOBALS['a'] = 'hi';
}
set_error_handler('handler');
Since $b will be undefined when echo’ed, a notice will be raised and the error handler will run.
由于调用echo的时候$b未被定义,于是PHP解释器会自动调用错误handle函数,从而修改了$a的状态。
class C {
public function __toString() {
$GLOBALS['a'] = 'hi';
}
}
$b = new C;
Passing an object to echo will call its __toString method.
或者是,重载__toString方法,这样当变量被echo的时候,就会调用相应函数来修改$a的状态。
Granted, these are very contrived examples, but contrived or not, we have to run them correctly. In global scope, these issues are so widespread as to be inescapable, so we completely turn off the JIT.
上述情况都是非常罕见的例子,但无论如何,成熟的虚拟机总要正确执行这些奇葩的代码。在全局作用域中,这类问题多到无法回避,所以HHVM选择在这种情况下直接关掉JIT,不进行优化。
保证类、函数、常量的命名独一无二
Make Class, Function, and Constant Names Unique
This only applies to repo authoritative mode, where HHVM analyzes an entire codebase ahead of time. It’s a fatal error to define a class or function with a name that’s already been bound at runtime, but it’s perfectly legal to have multiple functions or classes with the same name at analysis time. However, that will block some optimizations.
这条规则仅仅适用于repo authoritative mode,即HHVM预先编译整个代码库的情况下。如果一个标识符已经在运行时被绑定,那么如果再次定义它,就会引发严重错误。这里需要稍微解释一下,PHP作为一种动态语言,即便代码中存在重名的函数/类,但并不一定真的会执行到这些重复定义函数/类的语句,所以存在这种问题的代码并不一定就会在运行时出错。因此,在静态分析的阶段,JIT是允许重名的函数或者类的,只是在这种情况下,由于标识符的命名冲突干扰了类型推导,有些优化自然就无法进行了。
When a function has only one definition, that gives HHVM a lot of useful information at the function’s callsites, all of which can be used to generate better code.
- It knows if the callee returns by value or reference, so there’s no need to insert a check after callsites. It may be able to infer the type that the callee returns (integer, string, etc.) and use that information below the callsite.
- It knows if the callee takes its arguments by value or reference, so there’s no need to insert runtime checks; we can emit unconditional boxing or unboxing[1] code as needed.
- It may inline the function, if it’s small enough and conforms to one of a few inlineable “shapes”. For example, if the function just returns a constant, it will be inlined, and there will be no function call at runtime. (Additionally, the constant will be available for constant folding and propagation at the callsite.)
如果一个函数只被定义了一次,那么就能够提供给HHVM许多关于函数调用的有用信息,从而生成性能更好的代码。
- HHVM能够知道被调用函数是返回值还是返回引用,这样就不需要在函数调用时插入检查语句。并且HHVM也可能自动推导返回值的类型,并将这些信息用于函数调用结束后的类型推导。
- HHVM能够知道被调用函数所接受的参数是传值还是传引用,这样同样也不需要再进行运行时检查了;我们可以直接按照需要生成无条件的装箱/拆箱代码。
- 如果函数足够短并且符合一些可以被内联(inline)的“模式”,那么HHVM可以直接将函数进行inline处理。例如,如果函数仅仅返回一个常数,那么就会被inline,这样在运行时就免去了函数调用的开销。(此外,这些常数也可以在函数调用的时候被用于常量表达式求值和传参)。
We can make similarly powerful assumptions about uniquely-named classes: we know their (and their ancestors’) methods and declared properties, what interfaces they implement, etc. Their class constants are available for constant propagation. If we know that an object belongs to such a class, we know all of the above bits of information (arguments by reference, etc.) about the object’s methods.
类似地,我们同样可以利用全局唯一的类名来推导出非常有用的信息:我们可以知道它们(及其基类)的方法以及成员变量,它们实现了怎样的接口等等。类的静态变量(即常量)可以被用于变量传递。如果我们知道一个对象属于这样一个类,那么我们就知道了这个类中方法的所有相关信息(如是否按引用传递参数,等等)。
尽量避免读写动态变量
Avoid Dynamic Variable Access
This means any code that reads or writes a local variable, but where it’s impossible to tell which local is being referred to until runtime. This includes:
如果代码对一些只有在运行时才能够被确定的变量进行读写操作,那么HHVM就无法对其进行优化。这包括:
$$variable_name.compact(),extract(), andget_defined_vars().
These constructs make for slower code because they require HHVM to do extra bookkeeping: every time we enter or exit a function containing these constructs, we have to set up and tear down a map of local variable names to memory locations.
上述这些指令导致代码运行效率降低的原因在于,它们需要HHVM在运行时记录额外的信息:每次执行包含这些指令/调用的函数时,我们必须动态地记录一些变量名到内存位置的映射,因为这些变量是在运行时动态创建的。
In scopes where there are none of these constructs, we don’t need to use local variable names. Every local variable read or write maps to a known constant offset from the VM’s frame pointer, so that reading or writing a local can be done in a single machine instruction. We could also (though we don’t currently) optimize away local variables entirely.
但对于不使用这些调用的代码段,我们所生成的代码中根本不需要包含变量名。因为每个本地变量在实际生成的二进制码(HHBC)中,直接被对应到虚拟机帧指针(frame pointer)的固定偏移位置上面,也就是在实际生成的代码中,每个变量是使用帧指针+偏移量来表示的,因此对局部变量的读写可以直接使用一条机器码来完成。并且,我们甚至可以直接优化掉所有的本地变量。
Consider this code:
function f($vars) {
$name = 'some_constant';
// ...
extract($vars);
// ...
other_function($name);
}
If it weren’t for the extract() call, we could constant-propagate $name to the places where it’s used, and eliminate the local variable. But extract() might overwrite $name, so we can’t. In practice, the surrounding application code might guarantee that $vars will never have a 'name' key, but there’s no way for HHVM to know that, so the optimization is blocked.
上述代码中如果没有extract()函数调用,那么我们可以直接将$name作为常量来处理,这样在使用它的地方直接带入这个常量,从而消除了对于本地变量的引用。但是由于extract()函数的执行结果可能会覆盖掉$name变量,因此我们不能进行这项优化。实际上,上下文代码中或许能够保证$vars一定不包含一个name键,但这是在运行时才能够决定的信息,HHVM本身并不能够在静态分析的时候得知这些,所以无法进行优化。
在类中预先声明对象成员变量
Declare Properties
An expression like $obj->prop is faster to evaluate if prop is a declared property in $obj's class. In HHVM, objects of a class with declared properties have memory allocated for each declared property, at a known constant offset from the start of the object. This makes accessing declared properties very fast: it can be done with a handful of machine instructions and memory operations. Accessing dynamic properties, by contrast, requires hashtable lookups.
如果prop是一个已经在类中被声明的成员变量,那么形如$obj->prop这样的访问就会更快一些。HHVM会为类的实例对象中每一个声明的成员变量分配内存,并且内存位置是相对于对象起始地址的固定偏移位置处,也就是说成员变量在对象的内存布局中,位置是固定的。这使得访问已经被声明的属性时更加高效:仅需要使用几条机器码以及内存操作。但与之相反的是,访问动态成员需要进行哈希表查找,因此导致了更多的性能开销。
Declared properties also allow us to inline getter methods in repo-authoritative mode. So the following code:
预先声明的成员变量也使得HHVM能够更好地处理函数的自动inline。因此下面这段代码:
class C {
private $prop;
public function getProp() {
return $this->prop;
}
}
$obj = new C;
echo $obj->getProp();
is just as fast as this code:
class C {
public $prop;
}
$obj = new C;
echo $obj->prop;
HHVM’s ability to inline the getter lets you keep the property private (better encapsulation) without sacrificing speed.
HHVM自动将函数inline的能力使得我们可以在不牺牲性能的情况下隐藏对象的数据成员,也就是将变量变为私有的。
This also means that when you want a struct-like container, you should use objects with declared properties, instead of arrays. (If you just need a dumb container, it’s fine to declare public properties and forgo getters and setters — this won’t block any optimizations.) The trouble with arrays is that we can’t allocate fixed slots for declared properties the way we do with objects, because there’s no way to declare a set of fixed keys that will be in an array. Every read or write in an array is required to go through a hashtable lookup.
这同样表明,如果你需要一个类似struct的容器,那么你可以直接使用预先声明了成员变量的对象,来替代数组。(特别地,如果你仅仅需要一个纯粹通过变量名访问数据的容器,那么你完全可以不写getters/setters,直接将所有变量定义为公共的——这不会影响HHVM的优化。)数组的问题在于,对于其中的变量,我们无法像处理对象那样,为其中的每一个成员变量分配固定位置的内存区域,因为在数组中我们无法声明一组固定的key。因此,数组中所有的读写操作都必须通过哈希表来完成,由此带来了额外的性能开销。
Caveat: declared-property optimizations are only available when we know the class of $obj. In practice, this is usually only the case when either (a) the expression on the left of the arrow is $this, or (b) $obj was constructed via new in the same scope. In Facebook’s PHP codebase, this is overwhelmingly common; accessing properties of objects other than $this is very rare, so this optimization works well for us. Your mileage may vary.
警告:上述优化手段仅仅在HHVM可以推导出$obj的类型时才有效。实际上,这主要仅存在于两种情况下:箭头(->)左侧的表达式是$this;或者在相同作用域内,$obj是使用new操作符构造出的对象。在Facebook的代码库中,这些情况占据绝大多数部分;使用除了$this以外的方式来访问对象成员的情况是十分少见的,因此这项优化至少对Facebook来说是十分实用的。但对于其他用户的使用场景就不一定了。
Take note: declared properties seem to be slower than dynamic properties in Zend PHP versions older than 5.4.
没必要过多纠结
Don't Worry About This Too Much
HHVM’s optimizer gets better and more sophisticated by the day. We’re regularly finding new patterns to optimize by inspecting existing PHP codebases (primarily Facebook, but some third-party projects too). Spending time crafting micro-optimizations in your PHP code is unlikely to be worthwhile.
HHVM的代码优化功能会逐渐发展得更加复杂且强大。我们会定期地检索我们现有的代码库(主要是Facebook的业务代码,但也包含一些著名的第三方项目),寻找能够进行优化的新场景。因此,把时间耗费在优化PHP代码中一些小细节上是不值得的。
Still, if you’re choosing between two ways to write a piece of code and you’re wondering which one HHVM will do a better job with, put yourself in the mindset of a compiler writer and ask: which one gives the compiler more information? That one is likely to be the better choice. We may not optimize that specific pattern right now, but if the code conveys enough information, it’ll be easier for us to optimize it in the future.
但是,如果你在纠结使用何种方式来实现一段代码能够更加适合HHVM优化,那么就请将你自己代入编译器开发者的角色,然后考虑:哪种写法能够在静态分析的时候提供给编译器更多的信息?那么那种写法就将会是更好的选择。尽管HHVM的当前版本不一定会优化你所想到的那种特定场景,但如果你的代码确实包含了足够的信息,那么必然会更加方便日后新版本的HHVM来进行优化。
[1] Boxing and unboxing are HHVM-specific terms meaning, respectively, to convert a variable into a reference, and to read the value out of a reference.
[1] 装箱/拆箱是HHVM的内部专用术语,分别表示将变量转换为引用,以及从引用中读出值。