Skip to content

Commit 73b98a3

Browse files
committed
TAILCALL VM
Introduce the TAILCALL VM, a more efficient variant of the CALL VM: * Each opcode handler tailcalls the next opcode handler directly instead of returning to the interpreter loop. This eliminates call and interpreter loop overhead. * Opcode handlers use the preserve_none calling convention to eliminate register saving overhead. * preserve_none uses non-volatile registers for its first arguments, so execute_data and opline are usually kept in these registers and no code is required to forward them to the next handlers. Generated machine code is similar to a direct-threaded VM with register pinning, like the HYBRID VM. JIT+TAILCALL VM also benefits from this compared to JIT+CALL VM: * JIT uses the registers of the execute_data and opline args as fixed regs, eliminating the need to move them in prologue. * Traces exit by tailcalling the next handler. No code is needed to forward execute_data and opline. * No register saving/restoring in epilogue/prologue. The TAILCALL VM is used when the HYBRID VM is not supported, and the compiler supports the musttail and preserve_none attributes: The HYBRID VM is used when compiling with GCC, the TAILCALL VM when compiling with Clang>=19 on x86_64 or aarch64, and the CALL VM otherwise. This makes binaries built with Clang>=19 as fast as binaries built with GCC. Before, these were considerably slower (by 2.8% to 44% depending on benchmark, and by 5% to 77% before 76d7c61). Closes GH-17849 Closes GH-18720
1 parent c6f02a4 commit 73b98a3

15 files changed

+60270
-1552
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ PHP NEWS
1919
(alexandre-daubois)
2020
. Fixed bug GH-19544 (GC treats ZEND_WEAKREF_TAG_MAP references as WeakMap
2121
references). (Arnaud, timwolla)
22+
. Introduced the TAILCALL VM, enabled by default when compiling with Clang>=19
23+
on x86_64 or aarch64. (Arnaud)
2224

2325
- Filter:
2426
. Added support for configuring the URI parser for FILTER_VALIDATE_URL

UPGRADING

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,11 @@ PHP 8.5 UPGRADE NOTES
883883
. Creating exception objects is now much faster.
884884
. The parts of the code that used SSE2 have been adapted to use SIMD
885885
with ARM NEON as well.
886+
. Introduced the TAILCALL VM, enabled by default when compiling with Clang>=19
887+
on x86_64 or aarch64. The TAILCALL VM is as fast as the HYBRID VM used when
888+
compiling with GCC. This makes PHP binaries built with Clang>=19 as fast as
889+
binaries built with GCC. The performance of the CALL VM, used with other
890+
compilers, has also improved considerably.
886891

887892
- Intl:
888893
. Now avoids creating extra string copies when converting strings

Zend/Zend.m4

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ ZEND_CHECK_STACK_DIRECTION
170170
ZEND_CHECK_FLOAT_PRECISION
171171
ZEND_DLSYM_CHECK
172172
ZEND_CHECK_GLOBAL_REGISTER_VARIABLES
173+
ZEND_CHECK_PRESERVE_NONE
173174
ZEND_CHECK_CPUID_COUNT
174175
175176
AC_MSG_CHECKING([whether to enable thread safety])
@@ -464,3 +465,103 @@ AS_VAR_IF([ZEND_MAX_EXECUTION_TIMERS], [yes],
464465
AC_MSG_CHECKING([whether to enable Zend max execution timers])
465466
AC_MSG_RESULT([$ZEND_MAX_EXECUTION_TIMERS])
466467
])
468+
469+
dnl
470+
dnl ZEND_CHECK_PRESERVE_NONE
471+
dnl
472+
dnl Check if the preserve_none calling convention is supported and matches our
473+
dnl expectations.
474+
dnl
475+
AC_DEFUN([ZEND_CHECK_PRESERVE_NONE], [dnl
476+
AC_CACHE_CHECK([for preserve_none calling convention],
477+
[php_cv_preverve_none],
478+
[AC_RUN_IFELSE([AC_LANG_SOURCE([[
479+
#include <stdio.h>
480+
#include <stdint.h>
481+
482+
const char * const1 = "str1";
483+
const char * const2 = "str2";
484+
const char * const3 = "str3";
485+
uint64_t key = UINT64_C(0x9d7f71d2bd296364);
486+
487+
uintptr_t _a = 0;
488+
uintptr_t _b = 0;
489+
490+
uintptr_t __attribute__((preserve_none)) fun(uintptr_t a, uintptr_t b) {
491+
_a = a;
492+
_b = b;
493+
return (uintptr_t)const3;
494+
}
495+
496+
uintptr_t __attribute__((preserve_none)) test(void) {
497+
uintptr_t ret;
498+
499+
#if defined(__x86_64__)
500+
__asm__ __volatile__(
501+
/* XORing to make it unlikely the value exists in any other register */
502+
"movq %1, %%r12\n"
503+
"xorq %3, %%r12\n"
504+
"movq %2, %%r13\n"
505+
"xorq %3, %%r13\n"
506+
"xorq %%rax, %%rax\n"
507+
"call fun\n"
508+
: "=a" (ret)
509+
: "r" (const1), "r" (const2), "r" (key)
510+
: "r12", "r13"
511+
);
512+
#elif defined(__aarch64__)
513+
__asm__ __volatile__(
514+
/* XORing to make it unlikely the value exists in any other register */
515+
"eor x20, %1, %3\n"
516+
"eor x21, %2, %3\n"
517+
"eor x0, x0, x0\n"
518+
"bl fun\n"
519+
"mov %0, x0\n"
520+
: "=r" (ret)
521+
: "r" (const1), "r" (const2), "r" (key)
522+
: "x0", "x21", "x22", "x30"
523+
);
524+
#else
525+
# error
526+
#endif
527+
528+
return ret;
529+
}
530+
531+
int main(void) {
532+
533+
/* JIT is making the following expectations about preserve_none:
534+
* - The registers used for integer args 1 and 2
535+
* - The register used for a single integer return value
536+
*
537+
* We check these expectations here:
538+
*/
539+
540+
uintptr_t ret = test();
541+
542+
if (_a != ((uintptr_t)const1 ^ key)) {
543+
fprintf(stderr, "arg1 mismatch\n");
544+
return 1;
545+
}
546+
if (_b != ((uintptr_t)const2 ^ key)) {
547+
fprintf(stderr, "arg2 mismatch\n");
548+
return 2;
549+
}
550+
if (ret != (uintptr_t)const3) {
551+
fprintf(stderr, "ret mismatch\n");
552+
return 3;
553+
}
554+
555+
fprintf(stderr, "OK\n");
556+
557+
return 0;
558+
}]])],
559+
[php_cv_preserve_none=yes],
560+
[php_cv_preserve_none=no],
561+
[php_cv_preserve_none=no])
562+
])
563+
AS_VAR_IF([php_cv_preserve_none], [yes], [
564+
AC_DEFINE([HAVE_PRESERVE_NONE], [1],
565+
[Define to 1 if you have preserve_none support.])
566+
])
567+
])

Zend/zend_portability.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ char *alloca();
321321
# define ZEND_FASTCALL
322322
#endif
323323

324+
#ifdef HAVE_PRESERVE_NONE
325+
# define ZEND_PRESERVE_NONE __attribute__((preserve_none))
326+
#endif
327+
328+
#if __has_attribute(musttail)
329+
# define HAVE_MUSTTAIL
330+
# define ZEND_MUSTTAIL __attribute__((musttail))
331+
#endif
332+
324333
#if (defined(__GNUC__) && __GNUC__ >= 3 && !defined(__INTEL_COMPILER) && !defined(__APPLE__) && !defined(__hpux) && !defined(_AIX) && !defined(__osf__)) || __has_attribute(noreturn)
325334
# define HAVE_NORETURN
326335
# define ZEND_NORETURN __attribute__((noreturn))

Zend/zend_vm_def.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2962,7 +2962,10 @@ ZEND_VM_HOT_HELPER(zend_leave_helper, ANY, ANY)
29622962
{
29632963
zend_execute_data *old_execute_data;
29642964
uint32_t call_info = EX_CALL_INFO();
2965+
#if ZEND_VM_KIND != ZEND_VM_KIND_TAILCALL
2966+
/* zend_leave_helper may be called with opline=call_leave_op in TAILCALL VM */
29652967
SAVE_OPLINE();
2968+
#endif
29662969

29672970
if (EXPECTED((call_info & (ZEND_CALL_CODE|ZEND_CALL_TOP|ZEND_CALL_HAS_SYMBOL_TABLE|ZEND_CALL_FREE_EXTRA_ARGS|ZEND_CALL_ALLOCATED|ZEND_CALL_HAS_EXTRA_NAMED_PARAMS)) == 0)) {
29682971
EG(current_execute_data) = EX(prev_execute_data);
@@ -4917,7 +4920,7 @@ ZEND_VM_HOT_SEND_HANDLER(116, ZEND_SEND_VAL_EX, CONST|TMP, CONST|UNUSED|NUM, SPE
49174920
ZEND_VM_C_GOTO(send_val_by_ref);
49184921
}
49194922
} else if (ARG_MUST_BE_SENT_BY_REF(EX(call)->func, arg_num)) {
4920-
ZEND_VM_C_LABEL(send_val_by_ref):
4923+
ZEND_VM_C_LABEL(send_val_by_ref):;
49214924
ZEND_VM_DISPATCH_TO_HELPER(zend_cannot_pass_by_ref_helper, _arg_num, arg_num, _arg, arg);
49224925
}
49234926
value = GET_OP1_ZVAL_PTR(BP_VAR_R);
@@ -6162,7 +6165,7 @@ ZEND_VM_HANDLER(181, ZEND_FETCH_CLASS_CONSTANT, VAR|CONST|UNUSED|CLASS_FETCH, CO
61626165
}
61636166

61646167
bool is_constant_deprecated = ZEND_CLASS_CONST_FLAGS(c) & ZEND_ACC_DEPRECATED;
6165-
if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) {
6168+
if (UNEXPECTED(is_constant_deprecated) && !CONST_IS_RECURSIVE(c)) {
61666169
if (c->ce->type == ZEND_USER_CLASS) {
61676170
/* Recursion protection only applied to user constants, GH-18463 */
61686171
CONST_PROTECT_RECURSION(c);

0 commit comments

Comments
 (0)