Skip to content

JIT: Don't reuse IP register for EX(call) #18392

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

arnaud-lb
Copy link
Member

@arnaud-lb arnaud-lb commented Apr 22, 2025

JIT reuses the IP/opline fixed register to store an EX(call) cache during function calls. This allows a sequence of INIT_FCALL, SEND, SEND, ..., DO_FCALL to access the current call directly via this register instead of repeatedly fetching it from EX(call), and sometimes to avoid saving EX(call).

My understanding is that we reuse IP out of convenience, as registers were assigned manually in the old JIT. Doing otherwise would have required to reserve a 3rd register at least in code generated between INIT_FCALL and DO_FCALL. This also reduces register pressure, especially on x32.

This has a few drawbacks:

  • It may be confusing. For example, zend_jit_trace_exit_stub saves IP to EX(opline), when it may be being reused for EX(call) (e.g. here)
  • This would be a blocker if we hypothetically wanted to stop using a fixed register for IP
  • The EX(call) cache is lost every time we need to save IP
  • This conflicts with the last valid opline tracking (zend_jit_set_last_valid_opline): reusing IP also forgets the last valid opline.

Here I let IR allocate a register for the EX(call) cache, instead of storing it in the IP register.

bench.php shows no regression in executed instructions under valgrind, and a 1% improvement in wall time.

First commit passes zend_jit_ctx to zend_jit_trace_get_exit_point(), which creates a lot of noise. Second commit is smaller.

@@ -1608,6 +1613,18 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op
call_level++;
}

#if ZEND_DEBUG && 0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debugging helper, will remove

Comment on lines +449 to +460
int32_t call_ref;
int8_t call_reg;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deoptimization relied on EX(call) being in IP when ZEND_JIT_EXIT_RESTORE_CALL was set. I now use the SNAPSHOT mechanism so that deopt knows in which register the call actually is.

// JIT: if (UNEXPECTED(used_stack > (size_t)(((char*)EG(vm_stack_end)) - (char*)call))) {
jit_STORE_IP(jit, ir_LOAD_A(jit_EG(vm_stack_top)));
rx = ir_LOAD_A(jit_EG(vm_stack_top));
Copy link
Member Author

@arnaud-lb arnaud-lb Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable rx is named after the RX register of the old JIT, which was an alias for r15 (IP):

|.define RX, r15 // the same as VM IP reused as a general purpose reg
| MEM_LOAD_ZTS RX, aword, executor_globals, vm_stack_top, RX

It could be renamed to call, but this would increase the diff size.

@@ -4388,6 +4394,14 @@ static const void *zend_jit_trace(zend_jit_trace_rec *trace_buffer, uint32_t par
bool gen_handler = false;

opline = p->opline;

#if ZEND_DEBUG
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debugging helper, will remove

Comment on lines +930 to +933
static void jit_STORE_IP(zend_jit_ctx *jit, ir_ref ref)
{
jit_STORE_IP_no_reset(jit, ref);
zend_jit_reset_last_valid_opline(jit);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We must forget the last valid opline here by default, otherwise zend_jit_set_ip() will use a wrong offset. This used to be done in zend_jit_reuse_ip().

Comment on lines -179 to -181
bool ZEND_FASTCALL zend_jit_deprecated_helper(OPLINE_D)
bool ZEND_FASTCALL zend_jit_deprecated_helper(zend_execute_data *call)
{
zend_execute_data *call = (zend_execute_data *) opline;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally don't understand the details of this PR, but this change alone LGTM. It was super confusing to me when I implemented #[\NoDiscard].

Copy link
Member

@nielsdos nielsdos Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A helper is called with the argument %r15 for the VM IP (normally). However, register %r15 is reused in the JIT in some cases to point to the current execute data, such as here. So the opline pointer was never an opline pointer to begin with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants