Preface
With the rapid growth of the Internet, more and more scripting languages have been used in website development and operations. Because such languages typically provide rich extension libraries, dynamic typing, and strong support for polymorphism (i.e., the same function can accept different kinds of arguments), they greatly accelerate the cycle of development, testing, and deployment, making them a good fit for Internet companies’ “rapid iteration” mindset. However, to support mechanisms like dynamic typing, scripting languages are usually implemented as interpreters, which introduces a very serious performance burden.
For a language like PHP, which is widely used in many scenarios, the cost of this overhead is especially significant. To effectively improve the runtime performance of PHP scripts, Facebook developed the HipHop compiler and HHVM in succession: the former converts PHP code into C++ code, and the latter runs PHP code with JIT to improve execution efficiency. This article will systematically sort out PHP’s main performance bottlenecks and analyze how HipHop and HHVM address these problems, respectively, to improve execution efficiency.
Advantages and drawbacks of dynamic languages
Compared with traditional statically compiled languages such as C and C++, the biggest advantage of scripting languages is that they significantly improve developers’ productivity. This mainly comes from the following points:
- Scripting languages typically have very rich and mature extension libraries and powerful built-in funciton, which can satisfy the needs of most application scenarios;
- Their dynamic typing brings great flexibility and reduces constraints developers must consider when programming;
- Last and most importantly, scripting languages are generally translated and executed dynamically. This means that when the source code changes, you can see the effect directly without going through tedious compilation, linking, and other steps.
But as described above, the largest shared drawback of scripting languages is runtime efficiency, because in most cases they must be implemented with an interpreter to realize their dynamic features. This means that compared with compiled languages implementing the same functionality, scripting languages can be even an order of magnitude slower. Below, using PHP as an example, this article analyzes what dynamic features scripting languages typically need to implement, and why these features bring severe performance overhead.
PHP performance bottlenecks
Dynamic typing
In short, this feature means that at runtime, the same variable can refer to data of different types. As shown in the code below:
<?php
function foo($x) {
echo "foo: " . $x . "\n";
}
foo("Hello"); // prints: "foo: Hello"
foo(10); // prints: "foo: 10"
Why does such a convenient feature affect performance? In fact, in the kernel of PHP’s official implementation (Zend), variables are implemented like this:
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;
struct _zval_struct {
zvalue_value value; /* value of the variable */
zend_uint refcount__gc; /* value of the variable */
zend_uchar type; /* the variable's current data type */
zend_uchar is_ref__gc;
};
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct { /* string value */
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
zend_object_value obj; /* object pointer */
} zvalue_value;
That is, to support the dynamic feature that the same variable can be assigned values of different types at runtime, the interpreter’s implementation effectively stuffs values of various types (integers, floats, strings, objects) into the same container (union _zvalue_value). This design forces runtime to first perform type checks—determining which type an operand is—before it can execute the corresponding operation. But in many cases, the overhead of type checking far exceeds that of the operation itself. For example, multiplying two numbers is just two memory accesses, one multiply instruction, plus one write-back; but performing type checks is much more complex. As shown in the figure below, this introduces huge additional overhead.

In other words, if the compiler can know the type information of variables, it can generate instructions directly for the corresponding type, thereby reducing the extra overhead of type checking and conversion. If it cannot, then you get the kind of performance loss seen in PHP.
Dynamic name binding
In PHP, the concrete implementations of functions and classes are bound to their names only at runtime. For example, in PHP we can write code like this:
<?php
if ($cond) {
function foo($x) { return $x + 1; }
} else {
function foo($x) { return $x - 1; }
}
$y = foo($x);
That is, depending on the branch condition, a different implementation is selected for the foo() function. To support this, the interpreter must generate additional runtime data structures to record the mapping between a function name and its interface/implementation. When a function call occurs, the runtime must dynamically look up the function’s interface, check parameters, then find and execute its implementation. Obviously, this runtime lookup causes noticeable performance loss, especially when the function body itself is very short.
Dynamic name creation/reference/lookup
Dynamic name creation means that functions such as extract() can import variables from an array into the current symbol table, i.e., add variables to the current code context that can be referenced by identifiers. Dynamic name reference means that at runtime you can use a variable’s value (a string value) as an identifier to reference functions, classes, variables, and so on. The last one is self-evident: since dynamic creation is allowed, dynamic lookup of whether a class/function has been defined is also allowed.
<?php
// creation
function f($vars) {
$name = 'some_constant';
// ...
extract($vars);
// ...
other_function($name);
}
// reference
$a = 'f';
$b = 'c';
$c = 'oo';
$func = $a . $$b;
$func();
$obj = new $c;
// lookup
if (function_exists('foo')) {
...
}
if (class_exists($c)) {
...
}
These dynamic features are certainly pleasant to use, but the consequences are also obvious: the program must dynamically search the symbol table for the corresponding function/class/variable at runtime, incurring extra overhead. In fact, for a compiled language like C++, how much stack space is allocated at runtime and how many variables are created are fixed, so the generated machine code is extremely compact and operates on memory locations rather than identifiers, yielding good performance. But in a design like PHP, where new variables can be introduced at any time, variable access becomes more complex and many code optimization techniques become impossible.
Dynamic member variables
In C++, once a class declaration is fixed it cannot be modified; but for a dynamic language like PHP, adding member variables at runtime is supported. For example:
<?php
class C {
public $declProp = 1;
}
$obj = new C;
$obj->dynProp = 2;
echo $obj->declProp . "\n";
echo $obj->dynProp . "\n";
In languages such as C++, the compiler allocates memory for each pre-declared member variable inside an instance object, and its location is a fixed offset relative to the object’s base address—in other words, the member variable positions in the object’s memory layout are fixed. This makes access more efficient: it requires only a few machine instructions and memory operations.
In contrast, accessing dynamic members of an object in PHP requires a hash table lookup, leading to more overhead. Worse still, such dynamic addition features, when implementing overriding or adding class methods, require handling not only the current class but also walking the inheritance chain all the way to the top base class to check whether the operation is legal. The resulting performance burden can be even more severe.
Dynamic code execution
The eval() function is probably a “standard feature” of all interpreted languages. It takes a string representing code and executes it. Such a feature is easy for an interpreter to support, but it completely destroys the determinism of the program’s runtime state—once an external string is passed in, what it does and what side effects it brings are both unpredictable, and then how can you even talk about code optimization? In one sentence: dynamic languages feel great in the moment; type inference is the graveyard.
At the root, dynamic languages are slow. On the surface, it is because their dynamic features prevent type inference and require much information to be determined only at runtime, introducing extra lookup overhead and so on. But essentially, these reasons can all be summarized as: there is less information in the code. In physics terms, it contains more entropy. Because execution and computation are fundamentally a process of reducing entropy, when your code does not contain enough information, it inevitably requires extra computation to compensate for the missing information. So from a physics-minded perspective, the slowness of dynamic languages is hard to avoid: once optimization reaches a certain point, developers must sacrifice productivity and provide more information to make it faster.
Performance issues in the Zend engine
As PHP’s official implementation (the language does not have a “standard” in the usual sense; the official implementation effectively is the standard), the Zend engine follows the interpreter model—more precisely, it is a bytecode interpreter. Its workflow is: each time a PHP file is invoked, it first parses the PHP code into an abstract syntax tree (AST), converts it into binary intermediate instructions (Zend Bytecode), and then executes these instructions one by one.
The problem with the Zend engine is that for the PHP performance bottlenecks described in the previous section, it not only fails to avoid them, but steps into every possible pitfall that could cause performance issues. As analyzed above, Zend’s dynamic typing implementation already causes significant overhead, yet it still adopts a “dynamic loading” strategy: at runtime, only when a PHP file is included does Zend load its components (functions, classes, variables, constants, etc.) into various runtime “lookup tables”. Functions are manageable, but dynamic loading of classes is very expensive, because it forces the interpreter to trace the inheritance tree back to the root node to obtain information about all methods and member variables associated with the class, and to verify legality (e.g., whether an overwrite modifies the interface).
On the other hand, the Zend engine uses a feature called “dynamic lookup” to implement variable access. All identifiers during program execution are added/modified/deleted in runtime-built “lookup tables”, and at runtime it indexes the variable value by its identifier name, which introduces huge overhead. Although at the intermediate-instruction level some of these lookups can be optimized away, the actual performance gain is quite limited.
HipHop Compiler
To solve the performance problems of PHP described above, Facebook developed the HipHop compiler, which converts PHP code into C++ code and then performs static compilation, thereby achieving performance improvements. The core design idea of the HipHop compiler is to convert as much of PHP’s dynamic behavior as possible into something statically determinable through semantic analysis, so that it can generate targeted code to improve performance. Moreover, once PHP code is converted into C++ code, it can fully leverage mature optimization techniques in the C++ ecosystem to further improve performance. Below we only analyze how HipHop reduces the burden introduced by dynamic features to optimize PHP performance; for how HipHop achieves compatibility with PHP syntax, refer to the relevant paper [3] for details.
Dynamic loading
First, because HipHop converts PHP into C++ and compiles/links it, there is naturally no overhead from dynamic loading. For cases where developers must use dynamic loading semantics, HipHop provides additional methods to ensure compatibility—in short, by mimicking the Zend engine’s implementation. Of course, this inevitably introduces extra overhead, but even in the worst case it should not be slower than Zend.
Type inference
Second, as a static ahead-of-time compiler, HipHop can perform semantic analysis that is far more complex than what is possible at runtime, thus gaining strong type inference capabilities. For variables whose primitive types can be inferred and remain unchanged, HipHop can generate efficient code that skips type checks and directly performs operations; such code will naturally be optimized by the C++ compiler into efficient machine code.
It is worth mentioning the type hierarchy designed by HipHop, illustrated below:

Consider the following PHP code:
<?php
define("confName", "OOPSLA");
define("firstYear", 1986);
function year($edition) {
return firstYear - 1 + $edition;
}
echo "Hello " . confName . "’" . year(27);
After parsing, the corresponding AST is as follows:

Based on the type hierarchy above, HipHop performs type-agnostic optimizations such as constant inlining and folding, logical expression simplification, dead-code elimination, and function inlining; then it performs type inference, and finally obtains the following syntax tree:

From the syntax tree above we can see that the constant expression on the far right has been fully merged into a single string, the intermediate expression evaluation has been partially simplified, and all expressions are annotated with type information. It is also worth noting that although HipHop does not know the type of the input variable $edition at compile time, since the left-hand side of the arithmetic operator is of type Integer, it can also infer the value after addition—i.e., the function return value—as type Numeric.
Dynamic lookup
Finally, regarding PHP’s dynamic lookup feature: because HipHop infers types as much as possible, in most cases it can refer to variables by memory address rather than variable name, generating targeted code and saving the overhead of looking up identifiers in the symbol table. Likewise, for the minority of cases where HipHop cannot statically resolve an identifier, the generated C++ code will construct a runtime symbol table to implement Zend-like dynamic lookup, at the cost of some performance loss.
In summary, HipHop mainly relies on relatively powerful semantic analysis (type inference). If the code contains sufficient type information and as few dynamic features as possible, it can generate highly efficient C++ code, which is then further optimized by GCC into efficient machine code.
HipHop Virtual Machine
Overview
From the analysis of the HipHop compiler, we know that optimization based on static compilation only speeds up the parts of PHP code that can be statically analyzed (type-inferred). But if developers insist on using PHP’s dynamic features, they must endure additional performance loss. On the other hand, compiling complex C++ code is often very time-consuming, and small changes to local code can lead to lengthy compilation and linking. This creates differences between the development and production environments: to run code quickly during development, developers use an interpreter to run PHP; after development is complete, they compile and deploy. While this may seem workable, in real production environments, interpreters and compilers inevitably differ in details, wasting developers’ time on tedious compatibility issues. Therefore, to balance PHP development efficiency and execution efficiency, Facebook introduced HHVM (HipHop Virtual Machine).
HHVM core idea
As mentioned above, HipHop’s speedups come largely from effective type inference, allowing many dynamically invoked parts to be statically bound to the corresponding functions. However, due to PHP’s dynamic nature, the branches whose types can be inferred are limited, which constrains HipHop’s performance gains. But there is an important observation: although the types of some variables in PHP code cannot be inferred, the possibilities are finite—a property that can be called the “type consistency hypothesis”. Consider a simple example: PHP’s built-in function strlen() returns an integer when given a normal string, but returns the special value null when given an array. Under normal circumstances, developers will not mistakenly pass an array as the function argument. So although from a static-analysis perspective the function’s return type is variable, in real execution it almost always returns an integer. That is, if we simply guess that strlen() returns an integer and continue type inference, the guess succeeds in the vast majority of cases. Such types, which cannot be statically analyzed but have limited overall possibilities, are defined by HHVM as “latent types”. If we can design an accurate strategy to guess these types and generate targeted code, we can speed up PHP code that cannot be optimized through static analysis.
As a JIT VM with access to runtime type information, HHVM’s advantage is that it can fully utilize these type signals to speculatively generate targeted code even without knowing types ahead of time. To guess these latent types and accelerate execution, HHVM introduces the concept of a “Tracelet” as the basic unit of JIT optimization. A Tracelet is a highly abstract representation of program control flow; it is essentially a single-entry, multi-exit code block, and all variable type information flowing into this block is explicitly annotated. Clearly, this annotation cannot be completed during static analysis, because the code does not contain the necessary information. In practice, HHVM first performs “symbolic execution”—an advanced static analysis technique—and infers as many input/output types as possible. Then, for variables that truly cannot be inferred, HHVM observes runtime types, generates corresponding code, and makes an assumption: when this code executes in the future, the type states of variables are highly likely to match what was observed in the first execution. In other words, based on the program’s type consistency hypothesis, HHVM uses previously observed runtime type information to predict the most likely type state in later executions.
Tracelet execution flow
Next, we analyze how HHVM performs JIT optimization. First, we know that hot regions of PHP code are automatically split into multiple Tracelets, and machine code is generated for each Tracelet. When processing each Tracelet, the JIT first checks whether the type information of all current input variables satisfies the constraints determined during symbolic execution. If the constraints are satisfied, the JIT compiler can use these known type signals to optimize the code inside the Tracelet. Since types are now determined, HHVM can generate targeted and efficient code. Typically, after the current Tracelet finishes execution, control flow transfers to the next Tracelet and continues.
If the type constraints are not satisfied when entering a Tracelet, HHVM calls an error recovery function and regenerates machine code for the current Tracelet based on the new type information, then transfers control flow to the newly generated machine code. Therefore, for code with some degree of polymorphism, this process can be understood as a linear search through a series of machine code blocks to find one that matches the input value types. In most cases, only one or two attempts are needed. In extreme cases, if 12 consecutive transfers still fail to match types, HHVM transfers control to the interpreter to execute this Tracelet in interpreted mode.
Tracelet example
The abstract concept of Tracelet can be somewhat hard to understand. Below is a concrete example for a brief explanation. Consider the following PHP code:
<?php
function max2($a, $b) {
return $a > $b ? $a : $b;
}
echo max2(2, 1) . "\n";
echo max2("wxy", "abc") . "\n";
The HHBC code generated by the HHVM frontend is as follows:
.function("max2")
a: CGetL $b
CGetL $a
Gt
JmpZ c
b: CGetL $a
RetC
c: CGetL $b
RetC
The code above can be split into three Tracelets; the positions labeled a, b, c are the entry points of each Tracelet. When max2() is first called with arguments (2, 1), HHVM generates the code for Tracelet a_1 and caches it. Control flow transfers to its entry point, and the code cache looks like the figure below:

At runtime, it first checks whether the types of input variables satisfy the constraints. Since this is the first call, the code itself was generated assuming two integer inputs, so the constraints are naturally satisfied. Then it can directly use the hardware integer comparison instruction to implement the Gt operator—i.e., targeted optimization. If the constraints are not satisfied, HHVM will recompile this Tracelet.
Next, control flow splits into two branches based on the comparison result, executing either Tracelet b or c. Here, since 2 > 1, execution will fall through and control flow transfers to the Tracelet whose entry is b. But because this Tracelet has not yet been compiled, HHVM calls the error recovery function to compile and cache it, producing the following code cache:

In Tracelet b_1, because the first call to max2() used (2, 1), both local variables are integers, so HHVM guesses that when leaving this Tracelet, $a and $b also remain integers. If this constraint is not satisfied, the current Tracelet will be recompiled; if it is satisfied, the return value can be placed in a register and returned directly. In addition, because the Tracelet contains a return instruction, even though this Tracelet only uses the value of $a, it still must check both local variables. This is because when the function ends and returns, local variables need to be destroyed; depending on variable types, reference counting operations may need to be performed to ensure correct garbage collection.
When max2() is called a second time with two strings, the type check for Tracelet a_1 in the code cache obviously fails, causing the Tracelet at a to be recompiled as Tracelet a_2. Then, when comparing variables, it must call an underlying string comparison function (perhaps from glibc or similar? not sure...). Since the input arguments satisfy "wxy" > "abc", there is no jump at JmpZ, and execution continues at the Tracelet labeled b. In summary, after the function runs, the code cache is as shown below:

What may feel counterintuitive here is: why does the end of Tracelet a_2 not directly point to Tracelet b_2, but instead has two extra jumps? In fact, according to the Tracelet execution logic, when executing the Tracelet at label b, HHVM finds that the code for the current Tracelet already exists in the code cache, so it uses a jump instruction to transfer control flow directly to Tracelet b_1 in the cache. Given that the two input variables are strings, the constraints are not satisfied, so it calls the error recovery function again and recompiles Tracelet b_1 into Tracelet b_2 based on the types of $a and $b. Finally, after type checking, the JIT automatically decrements $b’s reference count and returns $a.
Summary
To summarize, HHVM’s biggest improvement is addressing the cases where HipHop previously could not infer types during static analysis. Without knowing types ahead of time, HHVM can collect runtime type information and, using the principle of type consistency, guess likely types and generate targeted high-performance code. If a guess misses, it keeps the old code and generates new code, forming the complex chain structure shown above. In practice, the hit rate of this guessing strategy is indeed very high: there is a 91% chance of a first-try hit, and the probability of hitting within 4 tries exceeds 99%.
Optimization strategies for PHP code
From the analysis above of the main optimization strategies of HipHop Compiler and HipHop Virtual Machine, we can see that improvements in PHP program efficiency mainly come from these strategies successfully reducing uncertainty in the code. Therefore, to improve the runtime efficiency of PHP code submitted by developers, we can extract HHVM’s parsing frontend to check submitted PHP code, score it based on how fully type inference can be completed, and reject commits with scores that are too low—thus restricting developers’ use of dynamic features. Moreover, for parts where types truly cannot be inferred, we can require the number of possible type branches to be as small as possible, improving the hit rate of the Tracelet mechanism. In this way, within the scope of PHP, we can basically consider that high-scoring code has reached the limit of performance optimization, because it already contains enough information. At that point, the remaining factors affecting runtime efficiency are overhead from external function calls and system calls—areas that no JIT can optimize away—and optimization must be done in combination with the specific business logic.
Appendix
Definitions in the type system
When reading this article, it is necessary to understand some basic definitions in type systems to follow the discussion. In fact, type-system terminology is used inconsistently and can be somewhat messy. Some concepts are not easy to define strictly. Below is a relatively “rigorous” set of definitions from academia.
First, define some basic concepts:
Program Errors
trapped errors: cause the program to terminate, such as division by zero, array out-of-bounds access in Javauntrapped errors: the program may continue after the error, but arbitrary behavior may occur, such as buffer overflow in C, jumping to an invalid address, etc.
Forbidden Behaviours
When designing a programming language, one can define a set of forbidden behaviors. They must include all untrapped errors, but may also include trapped errors.
Well behaved、ill behaved
well behaved: a program is well behaved if forbidden behaviors cannot occur during executionill behaved: otherwise
Below are definitions related to types:
Strong/weak typing
strongly typed: a language is strongly typed if the behavior of all programs is well behaved, i.e., forbidden behaviors cannot occurweakly typed: otherwise. For example, buffer overflow in C is a trapped error and thus a forbidden behavior, so C is weakly typed
In short, weakly typed languages have less strict type checking and tend to tolerate implicit type conversions. For example, in C, an int can be automatically promoted to a double; this makes it easier to produce forbidden behaviours, so it is weakly typed.
Dynamic/static typing
dynamiclly typed: if ill behaviors are rejected at runtime, it is dynamically typedstatically typed: if ill behaved programs are rejected at compile time, it is statically typed
The definitions above may be overly academic. In a more common but less rigorous phrasing: if a variable’s type can be uniquely determined during compilation, it is static typing; if a variable’s type can only be determined at runtime, it is dynamic typing.
Some examples
- Untyped: assembly
- Weakly typed, statically typed: C/C++
- Weakly typed, dynamic type checking: Perl/PHP
- Strongly typed, static type checking: Java/C#
- Strongly typed, dynamic type checking: Python, Scheme
- Statically, explicitly typed: Java/C
- Statically, implicitly typed: Ocaml, Haskell
References
[1] http://www.zhihu.com/question/19918532
[2] Deep understanding of the PHP kernel. Thinking In PHP Internals[J].
[3] Zhao H, Proctor I, Yang M, et al. The HipHop compiler for PHP[C]//ACM SIGPLAN Notices. ACM, 2012, 47(10): 575-586.
[4] Adams K, Evans J, Maher B, et al. The hiphop virtual machine[C]//Proceedings of the 2014 ACM International Conference on Object Oriented Programming Systems Languages & Applications. ACM, 2014: 777-790.