Skip to content

Implicit register operand#28

Draft
arnaud-lb wants to merge 2 commits intomasterfrom
reg-op
Draft

Implicit register operand#28
arnaud-lb wants to merge 2 commits intomasterfrom
reg-op

Conversation

@arnaud-lb
Copy link
Copy Markdown
Owner

@arnaud-lb arnaud-lb commented Mar 19, 2026

This experiment implements implicit register operands. The idea comes from v8:

In order to reduce the size of the bytecode stream, Ignition has an accumulator register, which is used by many bytecodes as implicit input and output register. This register is not part of the register file on the stack, but is instead maintained in a machine register by Ignition. This minimize loading and storing repeatedly to memory for register operations. It also reduces the size of bytecode by avoiding the need to specify an input and output register for many operations. E.g., binary op bytecodes only require a single operand to specify one of the inputs, with the other input and the output registers being implicitly the accumulator register, rather than having to explicitly specify all three registers.
https://docs.google.com/document/d/11T2CRex9hXxoJwbYqVQ32yIPMh0uouUZLdyrtmMoL44/edit?tab=t.0

Basic principle:

  • Introduce a new IS_REG operand type
  • Introduce new REG and SPEC(REG) specializations
  • REG specializations receive one operand via a zval reg parameter. Calling convention passes this via %r14-%r15.
  • SPEC(REG) specializations pass their result to the next opcode via this parameter.
  • The DFA pass promotes IS_TMP_VAR operands to IS_REG when possible
  • REG and SPEC(REG) specializations were added to some of the most frequently executed pairs of opcodes
const zend_op *ZEND_ADD_SPEC_CV_REG_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS, zval reg) {
  zval retreg;
  zval *op1 = EX_VAR(opline->op1.var);
  zval *op2 = ®
  if (EXPECTED(Z_TYPE_INFO_P(op1) == IS_LONG)) {
    if (EXPECTED(Z_TYPE_INFO_P(op2) == IS_LONG)) {
      ZVAL_LONG(&retreg, Z_LVAL_P(op1) + Z_LVAL_P(op2));
    } else ...
  } else ...
  opline++;
  return musttail opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU, retreg);
}

If we ignore the Z_TYPE_INFO_P(op1) == IS_LONG checks, the code above compiles to:

        movslq  8(%r13), %rax      # load opline.op1.var
        addq    (%r12,%rax), %r14  # Z_LVAL(retreg) = Z_LVAL(EX_VAR(%rax)) + Z_LVAL(reg)
        movq    32(%r13), %rax     # load (opline+1)->handler
        addq    $32, %r13          # opline++
        movl    $4, %r15d          # Z_TYPE_INFO(retreg) = IS_LONG
        jmpq    *%rax              # tailcall opline->handler
                                                                                                                                                                                                                                

Before optimizer:

0000 CV0($a) = RECV 1
0001 T1 = FETCH_OBJ_R CV0($a) string("x")
0002 RETURN T1

After optimizer:

0000 CV0($a) = RECV 1
0001 REG = FETCH_OBJ_R CV0($a) string("x")
0002 RETURN REG

Results:

Currently the improvements are modest (about 0.75% on symfony) and rely on a patched clang.

This could have more impact in conjunction with variable-size opcodes.

In the symfony benchmark, about 9% of executed handlers where REG or SPEC(REG) specializations (33257 out of 366793).

Calling convention:

The calling convention passes the reg argument via two registers, and the compiler avoids moving it to memory most of the time (as long as we don't pass its address to some external function, and in a few other cases).

This works surprisingly well, but this is fragile as regressions would go unnoticed. In practice we would likely need some linter to prevent that.

Register pressure:

Initial implementation was adding a zval reg argument to all handlers, and passed it to the next handler. This increased register pressure and stack spilling considerably, as this pins most callee-saved registers (r12-r15 for execute_data, opline, and reg). This resulted in a ~1% regression.

In the current implementation we fix this by using two separate variable (reg for input, retreg for output), and adding the zval reg argument only to handlers that use it.

This leads us to the musttail issue:

musttail issue:

musttail requires that the callee and callers have exactly the same signature. This implies that helpers with extra args can not be tail-called, and must return retreg in addition to the next opline, in a large struct {zend_op *opline, zval reg}. Additionally, we must add the zval reg operand on all handlers.

Both of these add considerable overhead.

The current implementation patches clang to disable this requirement, for the sake of the PoC. (See also clang issue 54964).

Other limitations:

We can only promote pairs of successive oplines, because we do not propagate reg/retreg across non-REG handlers. We can not promote oplines whose result is used more than once, for the same reason.

The second opline must not be a branch target. Result of the first opline must not be used by a Phi (unless all nodes can be promoted). These restrictions are not explicitly implemented in the PoC, but none of the supported handlers would fall in these.

We promote only IS_TMP_VAR, so we can assume that IS_REG have the same semantics.

TODO: Try splitting reg/retreg while keeping identical signatures.

TODO: Can DO_FCALL return via retreg too?

Split OP flags and EXT flags in two separate sets, in zend_vm_gen.php to
maintain 32bit support. In Zend/zend_vm_opcodes.c we can unify them again.
arnaud-lb pushed a commit that referenced this pull request Apr 15, 2026
When certificate `cert` exists, but is not added to the store, it causes
memory leaks. The error handling was already existing but the freeing
only happened on the success case.
One could also ponder whether it is necessary to inform the user when
adding a certificate failed or signal this in some way.

Part of the leak report:
```
Direct leak of 384 byte(s) in 1 object(s) allocated from:
    #0 0x7fdbf1f9e9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x7fdbf183a7c4 in CRYPTO_zalloc (/lib/x86_64-linux-gnu/libcrypto.so.3+0x2237c4) (BuildId: 0698e1ff610cb3c6993dccbd82c1281b1b4c5ade)
    #2 0x7fdbf16f9d13  (/lib/x86_64-linux-gnu/libcrypto.so.3+0xe2d13) (BuildId: 0698e1ff610cb3c6993dccbd82c1281b1b4c5ade)
    #3 0x7fdbf16f9e19 in ASN1_item_new_ex (/lib/x86_64-linux-gnu/libcrypto.so.3+0xe2e19) (BuildId: 0698e1ff610cb3c6993dccbd82c1281b1b4c5ade)
    #4 0x7fdbf19a59f9 in X509_new_ex (/lib/x86_64-linux-gnu/libcrypto.so.3+0x38e9f9) (BuildId: 0698e1ff610cb3c6993dccbd82c1281b1b4c5ade)
    #5 0x5575bcd295cb in php_openssl_pem_read_bio_x509 /work/php-src/ext/openssl/openssl_backend_v3.c:876
    #6 0x5575bcd2ef3d in php_openssl_load_stream_cafile /work/php-src/ext/openssl/xp_ssl.c:855
    #7 0x5575bcd2f4da in php_openssl_enable_peer_verification /work/php-src/ext/openssl/xp_ssl.c:912
    #8 0x5575bcd33104 in php_openssl_setup_crypto /work/php-src/ext/openssl/xp_ssl.c:1610
    #9 0x5575bcd39c18 in php_openssl_sockop_set_option /work/php-src/ext/openssl/xp_ssl.c:2512
    #10 0x5575bdb4c610 in _php_stream_set_option /work/php-src/main/streams/streams.c:1466
    #11 0x5575bdb5557d in php_stream_xport_crypto_setup /work/php-src/main/streams/transports.c:367
    #12 0x5575bcd39f11 in php_openssl_sockop_set_option /work/php-src/ext/openssl/xp_ssl.c:2540
    #13 0x5575bdb4c610 in _php_stream_set_option /work/php-src/main/streams/streams.c:1466
    #14 0x5575bdb54655 in php_stream_xport_connect /work/php-src/main/streams/transports.c:248
    #15 0x5575bdb5365d in _php_stream_xport_create /work/php-src/main/streams/transports.c:145
    #16 0x5575bd8d30b1 in php_stream_url_wrap_http_ex /work/php-src/ext/standard/http_fopen_wrapper.c:490
    #17 0x5575bd8d857e in php_stream_url_wrap_http /work/php-src/ext/standard/http_fopen_wrapper.c:1204
    #18 0x5575bdb5073d in _php_stream_open_wrapper_ex /work/php-src/main/streams/streams.c:2270
    #19 0x5575bd878fa6 in zif_file_get_contents /work/php-src/ext/standard/file.c:409
    #20 0x5575bd5bfe39 in zif_phar_file_get_contents /work/php-src/ext/phar/func_interceptors.c:226
    #21 0x5575bdab7ed2 in zend_test_execute_internal /work/php-src/ext/zend_test/observer.c:306
    #22 0x5575bdde024a in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER /work/php-src/Zend/zend_vm_execute.h:2154
    #23 0x5575bdf40995 in execute_ex /work/php-src/Zend/zend_vm_execute.h:116519
    #24 0x5575bdf558b0 in zend_execute /work/php-src/Zend/zend_vm_execute.h:121962
    #25 0x5575be0ba0ab in zend_execute_script /work/php-src/Zend/zend.c:1980
    #26 0x5575bdaec8bb in php_execute_script_ex /work/php-src/main/main.c:2645
    #27 0x5575bdaecccb in php_execute_script /work/php-src/main/main.c:2685
    #28 0x5575be0bfc16 in do_cli /work/php-src/sapi/cli/php_cli.c:951
    #29 0x5575be0c21e3 in main /work/php-src/sapi/cli/php_cli.c:1362

... etc ...
```

Closes phpGH-21030.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant