Skip to content

JIT: Make overflow add/sub cheaper - #23940

Open
ndossche wants to merge 1 commit into
php:masterfrom
ndossche:jit-add-overflow-undo
Open

ndossche wants to merge 1 commit into
php:masterfrom
ndossche:jit-add-overflow-undo

Conversation

@ndossche

Copy link
Copy Markdown
Member

PHP code:

<?php
function sum($n) {
    $s = 0;
    for ($i = 0; $i < $n; $i++) {
        $s += $i;
    }
    return $s;
}

var_dump(sum(10000000));

Old x86-64 code:

TRACE-1$sum$4:
	subq $0x30, %rsp
	movabsq $EG(jit_trace_num), %rax
	movl $1, (%rax)
	cmpb $4, 0x58(%r14)
	jne jit$$trace_exit_0
	cmpb $4, 0x68(%r14)
	jne jit$$trace_exit_1
	cmpb $4, 0x78(%r14)
	jne jit$$trace_exit_2
	movq 0x50(%r14), %rax
	movq 0x60(%r14), %rcx
	movq 0x70(%r14), %rdx
.L1:
	cmpq %rax, %rdx
	jge jit$$trace_exit_3
	movq %rdx, %rsi
	addq %rcx, %rsi
	jo jit$$trace_exit_4
	leaq 1(%rdx), %rdx
	movabsq $EG(vm_interrupt), %rcx
	movb (%rcx), %cl
	testb %cl, %cl
	jne jit$$trace_exit_5
	movq %rsi, %rcx
	jmp .L1

Note that this code uses an extra register and 2 extra moves (for %rcx) to carry the result. This is because the trace exit wants the old result. But we can avoid the extra register and extra moves if we simply reconstruct the old value upon exit.
This patch implements that idea, which results in the following new code:

TRACE-1$sum$4:
	subq $0x30, %rsp
	movabsq $EG(jit_trace_num), %rax
	movl $1, (%rax)
	cmpb $4, 0x58(%r14)
	jne jit$$trace_exit_0
	cmpb $4, 0x68(%r14)
	jne jit$$trace_exit_1
	cmpb $4, 0x78(%r14)
	jne jit$$trace_exit_2
	movq 0x50(%r14), %rax
	movq 0x60(%r14), %rcx
	movq 0x70(%r14), %rdx
	jmp .L2
	; ... nops for alignment
.L1:
	leaq 1(%rdx), %rdx
	movabsq $EG(vm_interrupt), %rsi
	movb (%rsi), %sil
	testb %sil, %sil
	jne jit$$trace_exit_5
.L2:
	cmpq %rax, %rdx
	jge jit$$trace_exit_3
	addq %rdx, %rcx
	jno .L1
	subq %rdx, %rcx
	jmp jit$$trace_exit_4

Notice how the addition now no longer needs extra moves and how the result is kept in %rcx all the time. The cold path under jno restores the old value right before jumping to the trace exit.

PHP code:
```php
<?php
function sum($n) {
    $s = 0;
    for ($i = 0; $i < $n; $i++) {
        $s += $i;
    }
    return $s;
}

var_dump(sum(10000000));
```

Old x86-64 code:
```asm
TRACE-1$sum$4:
	subq $0x30, %rsp
	movabsq $EG(jit_trace_num), %rax
	movl $1, (%rax)
	cmpb $4, 0x58(%r14)
	jne jit$$trace_exit_0
	cmpb $4, 0x68(%r14)
	jne jit$$trace_exit_1
	cmpb $4, 0x78(%r14)
	jne jit$$trace_exit_2
	movq 0x50(%r14), %rax
	movq 0x60(%r14), %rcx
	movq 0x70(%r14), %rdx
.L1:
	cmpq %rax, %rdx
	jge jit$$trace_exit_3
	movq %rdx, %rsi
	addq %rcx, %rsi
	jo jit$$trace_exit_4
	leaq 1(%rdx), %rdx
	movabsq $EG(vm_interrupt), %rcx
	movb (%rcx), %cl
	testb %cl, %cl
	jne jit$$trace_exit_5
	movq %rsi, %rcx
	jmp .L1
```

Note that this code uses an extra register and 2 extra moves (for %rcx)
to carry the result. This is because the trace exit wants the old
result. But we can avoid the extra register and extra moves if we simply
reconstruct the old value upon exit.
This patch implements that idea, which results in the following new code:

```asm
TRACE-1$sum$4:
	subq $0x30, %rsp
	movabsq $EG(jit_trace_num), %rax
	movl $1, (%rax)
	cmpb $4, 0x58(%r14)
	jne jit$$trace_exit_0
	cmpb $4, 0x68(%r14)
	jne jit$$trace_exit_1
	cmpb $4, 0x78(%r14)
	jne jit$$trace_exit_2
	movq 0x50(%r14), %rax
	movq 0x60(%r14), %rcx
	movq 0x70(%r14), %rdx
	jmp .L2
	; ... nops for alignment
.L1:
	leaq 1(%rdx), %rdx
	movabsq $EG(vm_interrupt), %rsi
	movb (%rsi), %sil
	testb %sil, %sil
	jne jit$$trace_exit_5
.L2:
	cmpq %rax, %rdx
	jge jit$$trace_exit_3
	addq %rdx, %rcx
	jno .L1
	subq %rdx, %rcx
	jmp jit$$trace_exit_4
```

Notice how the addition now no longer needs extra moves and how the
result is kept in %rcx all the time. The cold path under `jno` restores
the old value right before jumping to the trace exit.
@github-actions

Copy link
Copy Markdown

AWS x86_64 (c6id.metal)

Attribute Value
Environment aws
Instance type c6id.metal
Architecture x86_64
CPU Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz, 64 cores @ 2900 MHz
CPU settings disabled deeper C-states, disabled turbo boost, disabled hyper-threading
RAM 251 GB
Kernel 6.18.38-76.139.amzn2023.x86_64
OS Amazon Linux 2023.12.20260727
GCC 14.2.1
Binary layout strategy none
Time 2026-09-28 23:05:13 UTC
Job details https://github.com/php/php-src/actions/runs/36491968828 (Artifacts)
Changeset https://github.com/php/php-src/compare/a8caefcafb..5d160796de

Laravel 12.11.0 demo app - 50 iterations, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skewness Z-stat P-value Memory
PHP - baseline@a8caefc 0.37380 0.37762 0.00057 0.15% 0.37442 0.00% 0.37429 0.00% 3.910 0.000 1.000 25.78 MB
PHP - baseline@a8caefc (JIT) 0.35215 0.35330 0.00028 0.08% 0.35258 -5.83% 0.35253 -5.81% 0.976 8.614 0.000 25.82 MB
PHP - jit-add-overflow-undo 0.37488 0.37665 0.00043 0.11% 0.37542 0.27% 0.37527 0.26% 1.367 -7.856 0.000 25.72 MB
PHP - jit-add-overflow-undo (JIT) 0.35212 0.35440 0.00039 0.11% 0.35281 -5.77% 0.35274 -5.76% 1.592 8.614 0.000 25.81 MB

Symfony 2.8.0 demo app - 50 iterations, 50 warmups, 100 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skewness Z-stat P-value Memory
PHP - baseline@a8caefc 0.67258 0.67393 0.00025 0.04% 0.67303 0.00% 0.67299 0.00% 1.067 0.000 1.000 26.24 MB
PHP - baseline@a8caefc (JIT) 0.63718 0.64303 0.00082 0.13% 0.63781 -5.23% 0.63768 -5.25% 5.518 8.614 0.000 26.25 MB
PHP - jit-add-overflow-undo 0.66997 0.67134 0.00036 0.05% 0.67054 -0.37% 0.67053 -0.37% 0.410 8.614 0.000 26.21 MB
PHP - jit-add-overflow-undo (JIT) 0.63766 0.63933 0.00032 0.05% 0.63844 -5.14% 0.63845 -5.13% 0.202 8.614 0.000 26.23 MB

Wordpress 6.9 main page - 50 iterations, 20 warmups, 20 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skewness Z-stat P-value Memory
PHP - baseline@a8caefc 0.59111 0.59769 0.00100 0.17% 0.59243 0.00% 0.59224 0.00% 3.117 0.000 1.000 26.30 MB
PHP - baseline@a8caefc (JIT) 0.51962 0.52440 0.00085 0.16% 0.52068 -12.11% 0.52057 -12.10% 2.733 8.614 0.000 26.24 MB
PHP - jit-add-overflow-undo 0.58966 0.59270 0.00058 0.10% 0.59051 -0.32% 0.59042 -0.31% 1.956 8.069 0.000 26.32 MB
PHP - jit-add-overflow-undo (JIT) 0.51894 0.52271 0.00087 0.17% 0.51975 -12.27% 0.51952 -12.28% 2.335 8.614 0.000 26.22 MB

bench.php - 50 iterations, 20 warmups, 2 requests (sec)

PHP Min Max Std dev Rel std dev % Mean Mean diff % Median Median diff % Skewness Z-stat P-value Memory
PHP - baseline@a8caefc 0.45157 0.45472 0.00078 0.17% 0.45306 0.00% 0.45302 0.00% 0.073 0.000 1.000 26.30 MB
PHP - baseline@a8caefc (JIT) 0.14447 0.14534 0.00019 0.13% 0.14490 -68.02% 0.14489 -68.02% 0.110 8.614 0.000 26.24 MB
PHP - jit-add-overflow-undo 0.45161 0.45499 0.00071 0.16% 0.45315 0.02% 0.45308 0.01% 0.636 -0.421 0.674 26.32 MB
PHP - jit-add-overflow-undo (JIT) 0.14453 0.14914 0.00069 0.47% 0.14510 -67.97% 0.14500 -67.99% 4.930 8.614 0.000 26.23 MB

@dstogov dstogov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see, this makes better code for provided example - increment of induction variable.
However, I'm not sure if this will make improvement in general.
This will emit more cold code, even if register reuse is not desired.
On the other hand this will decries register pressure.

Please check if this will work for "$x = $x + $x". Undoing this might be problematic.
Anyway, I don't care. Merge this if you like.

A better solution would be executing the "roolback" code by de-optimizer.
I know LuaJIT somehow does this (may be deoptimizer interprets the trace) , but I'm not sure if it's possible to do the same with IR.

@ndossche

ndossche commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

Please check if this will work for "$x = $x + $x". Undoing this might be problematic.

This seems to work. Checked with the following code and -d opcache.jit_hot_loop=10:

<?php
function sum($n) {
    $s = 1;
    for ($i = 0; $i < $n; $i++) {
        $s = $s + $s;
    }
    return $s;
}

var_dump(sum(30));

This generates the exact same assembly code as before.
To me it makes sense since everything operates on SSA level.

This will emit more cold code, even if register reuse is not desired.
On the other hand this will decries register pressure.

This might increase cold code generation, but the non-cold code generation may decrease because of the reduction in register usage can decrease the number of moves too.

@ndossche
ndossche requested a review from arnaud-lb October 4, 2026 20:45
@arnaud-lb

Copy link
Copy Markdown
Member

Problem with cold code is that it's emitted immediately after the trace, so it still reduces code locality across traces. I agree that we could avoid the cold code entirely by undoing the operation in the deoptimizer.

PoC with an LLM:

diff --git a/ext/opcache/jit/zend_jit.h b/ext/opcache/jit/zend_jit.h
index c4080d86bc6..b977036ee4c 100644
--- a/ext/opcache/jit/zend_jit.h
+++ b/ext/opcache/jit/zend_jit.h
@@ -191,6 +191,12 @@ void zend_jit_restart(void);
 #define ZREG_ZVAL_ADDREF    (4<<4)
 #define ZREG_THIS           (5<<4)
 
+/* The slot holds the result of the ADD/SUB opline the exit point points to
+ * (in the register, spill slot or VM stack slot described by the other bits).
+ * The deoptimizer reconstructs the old value of op1 by reverting the operation
+ * with op2 of the opline. See zend_jit_math_long_long(). */
+#define ZREG_UNDO_MATH      (1<<7)
+
 #define ZREG_NONE           -1
 
 #endif /* HAVE_JIT_H */
diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
index 2da9978a00c..64ea2d6ce0a 100644
--- a/ext/opcache/jit/zend_jit_ir.c
+++ b/ext/opcache/jit/zend_jit_ir.c
@@ -821,6 +821,7 @@ void *zend_jit_snapshot_handler(ir_ctx *ctx, ir_ref snapshot_ref, ir_insn *snaps
 			ir_ref var = i - 2;
 
 			ZEND_ASSERT(var < t->exit_info[exit_point].stack_size);
+			uint8_t undo = t->stack_map[t->exit_info[exit_point].stack_offset + var].flags & ZREG_UNDO_MATH;
 			if (t->stack_map[t->exit_info[exit_point].stack_offset + var].flags == ZREG_ZVAL_COPY) {
 				ZEND_ASSERT(reg != ZREG_NONE);
 				if ((exit_flags & ZEND_JIT_EXIT_FIXED)
@@ -854,34 +855,34 @@ void *zend_jit_snapshot_handler(ir_ctx *ctx, ir_ref snapshot_ref, ir_insn *snaps
 							/* spill slot on a VM stack */
 							if ((exit_flags & ZEND_JIT_EXIT_FIXED)
 							 && (t->stack_map[t->exit_info[exit_point].stack_offset + var].reg != ZREG_NONE
-							 || t->stack_map[t->exit_info[exit_point].stack_offset + var].flags != ZREG_TYPE_ONLY)) {
+							 || t->stack_map[t->exit_info[exit_point].stack_offset + var].flags != (ZREG_TYPE_ONLY | undo))) {
 								exit_point = zend_jit_duplicate_exit_point(ctx, t, exit_point, snapshot_ref);
 								addr = (void*)zend_jit_trace_get_exit_addr(exit_point);
 								exit_flags &= ~ZEND_JIT_EXIT_FIXED;
 							}
 							t->stack_map[t->exit_info[exit_point].stack_offset + var].reg = ZREG_NONE;
-							t->stack_map[t->exit_info[exit_point].stack_offset + var].flags = ZREG_TYPE_ONLY;
+							t->stack_map[t->exit_info[exit_point].stack_offset + var].flags = ZREG_TYPE_ONLY | undo;
 						} else {
 							if ((exit_flags & ZEND_JIT_EXIT_FIXED)
 							 && (t->stack_map[t->exit_info[exit_point].stack_offset + var].reg != IR_REG_NUM(reg)
-							 || (t->stack_map[t->exit_info[exit_point].stack_offset + var].flags & ~(ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE)))) {
+							 || (t->stack_map[t->exit_info[exit_point].stack_offset + var].flags & ~(ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE|ZREG_UNDO_MATH)))) {
 								exit_point = zend_jit_duplicate_exit_point(ctx, t, exit_point, snapshot_ref);
 								addr = (void*)zend_jit_trace_get_exit_addr(exit_point);
 								exit_flags &= ~ZEND_JIT_EXIT_FIXED;
 							}
 							t->stack_map[t->exit_info[exit_point].stack_offset + var].reg = IR_REG_NUM(reg);
-							t->stack_map[t->exit_info[exit_point].stack_offset + var].flags &= (ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE);
+							t->stack_map[t->exit_info[exit_point].stack_offset + var].flags &= (ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE|ZREG_UNDO_MATH);
 						}
 					} else {
 						if ((exit_flags & ZEND_JIT_EXIT_FIXED)
 						 && (t->stack_map[t->exit_info[exit_point].stack_offset + var].reg != ZREG_NONE
-						 || t->stack_map[t->exit_info[exit_point].stack_offset + var].flags != ZREG_TYPE_ONLY)) {
+						 || t->stack_map[t->exit_info[exit_point].stack_offset + var].flags != (ZREG_TYPE_ONLY | undo))) {
 							exit_point = zend_jit_duplicate_exit_point(ctx, t, exit_point, snapshot_ref);
 							addr = (void*)zend_jit_trace_get_exit_addr(exit_point);
 							exit_flags &= ~ZEND_JIT_EXIT_FIXED;
 						}
 						t->stack_map[t->exit_info[exit_point].stack_offset + var].reg = ZREG_NONE;
-						t->stack_map[t->exit_info[exit_point].stack_offset + var].flags = ZREG_TYPE_ONLY;
+						t->stack_map[t->exit_info[exit_point].stack_offset + var].flags = ZREG_TYPE_ONLY | undo;
 					}
 				} else if (!(exit_flags & ZEND_JIT_EXIT_FIXED)) {
 					int32_t idx = _add_trace_const(t, ctx->ir_base[ref].val.i64);
@@ -4477,6 +4478,85 @@ static int zend_jit_store_reg(zend_jit_ctx *jit, uint32_t info, int var, int8_t
 	return 1;
 }
 
+/* Returns the IS_LONG value of the stack slot 'var' of an exit point */
+static ir_ref zend_jit_deopt_slot_lval(zend_jit_ctx *jit, const zend_jit_trace_stack *stack, int var, const zend_jit_exit_const *constants)
+{
+	int8_t reg = STACK_REG(stack, var);
+	uint8_t flags = STACK_FLAGS(stack, var);
+
+	ZEND_ASSERT(STACK_TYPE(stack, var) == IS_LONG);
+	if (flags == ZREG_CONST) {
+		return ir_CONST_LONG((zend_long)constants[STACK_REF(stack, var)].i);
+	} else if (flags & ZREG_SPILL_SLOT) {
+		ZEND_ASSERT(reg != ZREG_NONE);
+		return ir_LOAD_L(ir_ADD_OFFSET(ir_RLOAD_A(reg), STACK_REF(stack, var)));
+	} else if (reg != ZREG_NONE) {
+		return zend_jit_deopt_rload(jit, IR_LONG, reg);
+	} else {
+		/* The value is in the VM stack slot */
+		ZEND_ASSERT(!(flags & ~(ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE)) || flags == ZREG_TYPE_ONLY);
+		return jit_Z_LVAL(jit, ZEND_ADDR_MEM_ZVAL(ZREG_FP, EX_NUM_TO_VAR(var)));
+	}
+}
+
+/* Reconstructs the old value of op1 of the ADD/SUB opline the exit point
+ * points to. See ZREG_UNDO_MATH. */
+static ir_ref zend_jit_deopt_undo_math(zend_jit_ctx *jit, const zend_op *opline, const zend_jit_trace_stack *stack, int var, const zend_jit_exit_const *constants)
+{
+	uint8_t flags = STACK_FLAGS(stack, var);
+	uint8_t opcode = (opline->opcode == ZEND_ASSIGN_OP) ? opline->extended_value : opline->opcode;
+	ir_ref res, op2;
+
+	ZEND_ASSERT(flags & ZREG_UNDO_MATH);
+	ZEND_ASSERT(opcode == ZEND_ADD || opcode == ZEND_SUB);
+	ZEND_ASSERT(var == EX_VAR_TO_NUM(opline->op1.var));
+	ZEND_ASSERT(STACK_TYPE(stack, var) == IS_LONG);
+
+	/* The slot describes where the result of the operation is */
+	if ((flags & ~ZREG_UNDO_MATH) == ZREG_TYPE_ONLY) {
+		/* The result was spilled to the VM stack slot it is bound to */
+		uint32_t res_var = (opline->opcode == ZEND_ASSIGN_OP) ? opline->op1.var : opline->result.var;
+
+		res = jit_Z_LVAL(jit, ZEND_ADDR_MEM_ZVAL(ZREG_FP, res_var));
+	} else if (flags & ZREG_SPILL_SLOT) {
+		ZEND_ASSERT(STACK_REG(stack, var) != ZREG_NONE);
+		res = ir_LOAD_L(ir_ADD_OFFSET(ir_RLOAD_A(STACK_REG(stack, var)), STACK_REF(stack, var)));
+	} else {
+		ZEND_ASSERT(STACK_REG(stack, var) != ZREG_NONE);
+		res = zend_jit_deopt_rload(jit, IR_LONG, STACK_REG(stack, var));
+	}
+
+	if (opline->op2_type == IS_CONST) {
+		zval *zv = RT_CONSTANT(opline, opline->op2);
+
+		ZEND_ASSERT(Z_TYPE_P(zv) == IS_LONG);
+		op2 = ir_CONST_LONG(Z_LVAL_P(zv));
+	} else {
+		op2 = zend_jit_deopt_slot_lval(jit, stack, EX_VAR_TO_NUM(opline->op2.var), constants);
+	}
+
+	return (opcode == ZEND_ADD) ? ir_SUB_L(res, op2) : ir_ADD_L(res, op2);
+}
+
+static int zend_jit_store_undo_math(zend_jit_ctx *jit, const zend_op *opline, const zend_jit_trace_stack *stack, int var, const zend_jit_exit_const *constants, bool set_type)
+{
+	zend_jit_addr dst = ZEND_ADDR_MEM_ZVAL(ZREG_FP, EX_NUM_TO_VAR(var));
+	ir_ref src = zend_jit_deopt_undo_math(jit, opline, stack, var, constants);
+
+	if (jit->ra && jit->ra[var].ref == IR_NULL) {
+		zend_jit_def_reg(jit, ZEND_ADDR_REG(var), src);
+	} else {
+		jit_set_Z_LVAL(jit, dst, src);
+		if (set_type &&
+		    (Z_REG(dst) != ZREG_FP ||
+		     !JIT_G(current_frame) ||
+		     STACK_MEM_TYPE(JIT_G(current_frame)->stack, EX_VAR_TO_NUM(Z_OFFSET(dst))) != IS_LONG)) {
+			jit_set_Z_TYPE_INFO(jit, dst, IS_LONG);
+		}
+	}
+	return 1;
+}
+
 static int zend_jit_store_spill_slot(zend_jit_ctx *jit, uint32_t info, int var, int8_t reg, int32_t offset, bool set_type)
 {
 	zend_jit_addr src;
@@ -5099,6 +5179,9 @@ static int zend_jit_math_long_long(zend_jit_ctx   *jit,
 				uint32_t old_res_info;
 				int32_t exit_point;
 				const void *exit_addr;
+				uint32_t undo_var = (uint32_t)-1;
+				ir_ref undo_ref = IR_UNUSED;
+				uint8_t undo_flags = 0;
 
 				if (opline->opcode == ZEND_ADD
 				 && Z_MODE(op2_addr) == IS_CONST_ZVAL && Z_LVAL_P(Z_ZV(op2_addr)) == 1) {
@@ -5114,47 +5197,39 @@ static int zend_jit_math_long_long(zend_jit_ctx   *jit,
 					SET_STACK_REF(stack, EX_VAR_TO_NUM(opline->result.var), ir_CONST_DOUBLE((double)ZEND_LONG_MIN - 1.0));
 					exit_point = zend_jit_trace_get_exit_point(opline + 1, 0);
 					SET_STACK_INFO(stack, EX_VAR_TO_NUM(opline->result.var), old_res_info);
-				} else if ((op == IR_ADD_OV || op == IR_SUB_OV) && !same_ops && Z_MODE(op1_addr) == IS_REG) {
-					/* Undo the operation on the cold path for deoptimization,
-					 * instead of keeping op1 alive across it.
-					 * This allows reusing its register for the result. */
-					uint32_t op1_var = EX_VAR_TO_NUM(opline->op1.var);
-					ir_ref old_ref = STACK_REF(stack, op1_var);
-
-					ir_ref if_ov = ir_IF(ir_OVERFLOW(ref));
-					ir_IF_TRUE_cold(if_ov);
-
-					/* undo: opposite operation */
-					ir_ref old_op1;
-					if (op == IR_ADD_OV) {
-						old_op1 = ir_SUB_L(ref, op2);
-					} else {
-						old_op1 = ir_ADD_L(ref, op2);
-					}
-
-					SET_STACK_REF_EX(stack, op1_var, old_op1, STACK_FLAGS(stack, op1_var));
-
-					exit_point = zend_jit_trace_get_exit_point(opline, 0);
-					exit_addr = zend_jit_trace_get_exit_addr(exit_point);
-					if (!exit_addr) {
-						return 0;
-					}
-					jit_SIDE_EXIT(jit, ir_CONST_ADDR(exit_addr));
-
-					/* After the snapshot is built. */
-					SET_STACK_REF_EX(stack, op1_var, old_ref, STACK_FLAGS(stack, op1_var));
-					ir_IF_FALSE(if_ov);
-					exit_point = -1;
 				} else {
+					if ((op == IR_ADD_OV || op == IR_SUB_OV)
+					 && !same_ops
+					 && Z_MODE(op1_addr) == IS_REG
+					 && op1 > 0
+					 && ref > 0) {
+						/* On overflow the VM re-executes this opline and needs the old
+						 * value of op1. Instead of keeping op1 alive across the guard
+						 * (which prevents reusing its register for the result), let the
+						 * deoptimizer reconstruct it from the result and op2. */
+						undo_var = EX_VAR_TO_NUM(opline->op1.var);
+						undo_ref = STACK_REF(stack, undo_var);
+						undo_flags = STACK_FLAGS(stack, undo_var);
+						if (undo_flags & (ZREG_LOAD|ZREG_STORE)) {
+							/* The old value is already in memory. Just don't keep it alive. */
+							SET_STACK_REF_EX(stack, undo_var, IR_NULL, undo_flags);
+						} else if (undo_ref == op1) {
+							SET_STACK_REF_EX(stack, undo_var, ref, undo_flags | ZREG_UNDO_MATH);
+						} else {
+							undo_var = (uint32_t)-1;
+						}
+					}
 					exit_point = zend_jit_trace_get_exit_point(opline, 0);
 				}
 
-				if (exit_point >= 0) {
-					exit_addr = zend_jit_trace_get_exit_addr(exit_point);
-					if (!exit_addr) {
-						return 0;
-					}
-					ir_GUARD_NOT(ir_OVERFLOW(ref), ir_CONST_ADDR(exit_addr));
+				exit_addr = zend_jit_trace_get_exit_addr(exit_point);
+				if (!exit_addr) {
+					return 0;
+				}
+				/* The snapshot is created here */
+				ir_GUARD_NOT(ir_OVERFLOW(ref), ir_CONST_ADDR(exit_addr));
+				if (undo_var != (uint32_t)-1) {
+					SET_STACK_REF_EX(stack, undo_var, undo_ref, undo_flags);
 				}
 				may_overflow = 0;
 			} else if ((res_info & MAY_BE_ANY) == MAY_BE_DOUBLE) {
@@ -17466,6 +17541,22 @@ static void zend_jit_preserve_parent_regs(zend_jit_ctx *jit,
 		zend_jit_trace_stack *parent_stack = parent_vars_count == 0 ? NULL :
 			parent->stack_map +
 			parent->exit_info[exit_num].stack_offset;
+		int undo_op2_var = -1;
+
+		if (ssa) {
+			/* The register of op2 of a ZREG_UNDO_MATH slot is needed even if
+			 * the variable itself is not used by the trace */
+			for (i = 0; i < parent_vars_count; i++) {
+				if ((STACK_FLAGS(parent_stack, i) & ZREG_UNDO_MATH)
+				 && !ssa->vars[i].no_val) {
+					const zend_op *opline = parent->exit_info[exit_num].opline;
+
+					if (opline->op2_type != IS_CONST) {
+						undo_op2_var = EX_VAR_TO_NUM(opline->op2.var);
+					}
+				}
+			}
+		}
 
 		for (i = 0; i < parent_vars_count; i++) {
 			if (STACK_FLAGS(parent_stack, i) != ZREG_CONST
@@ -17482,7 +17573,7 @@ static void zend_jit_preserve_parent_regs(zend_jit_ctx *jit,
 				} else {
 					ZEND_UNREACHABLE();
 				}
-				if (ssa && ssa->vars[i].no_val) {
+				if (ssa && ssa->vars[i].no_val && i != undo_op2_var) {
 					/* pass */
 				} else {
 					ir_ref ref = ir_RLOAD(type, reg);
diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c
index 45936ba01f4..0d04120fee6 100644
--- a/ext/opcache/jit/zend_jit_trace.c
+++ b/ext/opcache/jit/zend_jit_trace.c
@@ -2920,7 +2920,7 @@ static zend_jit_reg_var* zend_jit_trace_allocate_registers(zend_jit_trace_rec *t
 			 && STACK_FLAGS(parent_stack, i) != ZREG_ZVAL_COPY
 			 ) {
 				/* We will try to reuse register from parent trace */
-				RA_IVAL_FLAGS(i) = STACK_FLAGS(parent_stack, i);
+				RA_IVAL_FLAGS(i) = STACK_FLAGS(parent_stack, i) & ~ZREG_UNDO_MATH;
 				count += 2;
 			} else {
 				RA_IVAL_FLAGS(i) = ZREG_LOAD;
@@ -3606,6 +3606,26 @@ static int zend_jit_trace_deoptimization(
 			ZEND_ASSERT(reg != ZREG_NONE);
 			ZEND_ASSERT(check2 == -1);
 			check2 = i;
+		} else if (STACK_FLAGS(parent_stack, i) & ZREG_UNDO_MATH) {
+			if (ssa && ssa->vars[i].no_val) {
+				/* pass */
+			} else {
+				uint8_t type = STACK_TYPE(parent_stack, i);
+
+				ZEND_ASSERT(opline && type == IS_LONG);
+				if (!zend_jit_store_undo_math(jit, opline, parent_stack, i, constants,
+						STACK_MEM_TYPE(parent_stack, i) != type)) {
+					return 0;
+				}
+				if (stack) {
+					if (jit->ra && jit->ra[i].ref) {
+						SET_STACK_TYPE(stack, i, type, 0);
+						SET_STACK_REF(stack, i, jit->ra[i].ref);
+					} else {
+						SET_STACK_TYPE(stack, i, type, 1);
+					}
+				}
+			}
 		} else if (STACK_FLAGS(parent_stack, i) & ZREG_SPILL_SLOT) {
 			if (ssa && ssa->vars[i].no_val) {
 				/* pass */
@@ -8114,6 +8134,20 @@ static void zend_jit_dump_exit_info(zend_jit_trace_info *t)
 					fprintf(stderr, "(zval_try_addref)");
 				} else if (STACK_FLAGS(stack, j) == ZREG_ZVAL_COPY) {
 					fprintf(stderr, "zval_copy(%s)", zend_reg_name(STACK_REG(stack, j)));
+				} else if (STACK_FLAGS(stack, j) & ZREG_UNDO_MATH) {
+					fprintf(stderr, "(undo_math=");
+					if ((STACK_FLAGS(stack, j) & ~ZREG_UNDO_MATH) == ZREG_TYPE_ONLY) {
+						fprintf(stderr, "type_only");
+					} else if (STACK_FLAGS(stack, j) & ZREG_SPILL_SLOT) {
+						if (STACK_REG(stack, j) == ZREG_NONE) {
+							fprintf(stderr, "spill=0x%x", STACK_REF(stack, j));
+						} else {
+							fprintf(stderr, "spill=0x%x(%s)", STACK_REF(stack, j), zend_reg_name(STACK_REG(stack, j)));
+						}
+					} else {
+						fprintf(stderr, "%s", zend_reg_name(STACK_REG(stack, j)));
+					}
+					fprintf(stderr, ")");
 				} else if (STACK_FLAGS(stack, j) & ZREG_SPILL_SLOT) {
 					if (STACK_REG(stack, j) == ZREG_NONE) {
 						fprintf(stderr, "(spill=0x%x", STACK_REF(stack, j));
@@ -8688,6 +8722,62 @@ int ZEND_FASTCALL zend_jit_trace_hot_side(zend_execute_data *execute_data, uint3
 	return ret;
 }
 
+/* Returns the IS_LONG value of the stack slot 'i' of an exit point */
+static zend_long zend_jit_trace_exit_slot_lval(zend_execute_data *execute_data, const zend_jit_trace_info *t, const zend_jit_registers_buf *regs, const zend_jit_trace_stack *stack, uint32_t i)
+{
+	ZEND_ASSERT(STACK_TYPE(stack, i) == IS_LONG);
+	if (STACK_FLAGS(stack, i) == ZREG_CONST) {
+		return (zend_long)t->constants[STACK_REF(stack, i)].i;
+	} else if (STACK_FLAGS(stack, i) & ZREG_SPILL_SLOT) {
+		ZEND_ASSERT(STACK_REG(stack, i) != ZREG_NONE);
+		return *(zend_long*)((uintptr_t)regs->gpr[STACK_REG(stack, i)] + STACK_REF(stack, i));
+	} else if (STACK_REG(stack, i) != ZREG_NONE) {
+		return (zend_long)regs->gpr[STACK_REG(stack, i)];
+	} else {
+		/* The value is in the VM stack slot */
+		return Z_LVAL_P(EX_VAR_NUM(i));
+	}
+}
+
+/* Reconstructs the old value of op1 of the ADD/SUB opline the exit point
+ * points to. See ZREG_UNDO_MATH. */
+static zend_long zend_jit_trace_exit_undo_math(zend_execute_data *execute_data, const zend_jit_trace_info *t, uint32_t exit_num, const zend_jit_registers_buf *regs, const zend_jit_trace_stack *stack, uint32_t i)
+{
+	const zend_op *opline = t->exit_info[exit_num].opline;
+	uint8_t opcode = (opline->opcode == ZEND_ASSIGN_OP) ? opline->extended_value : opline->opcode;
+	zend_ulong res, op2;
+
+	ZEND_ASSERT(opcode == ZEND_ADD || opcode == ZEND_SUB);
+	ZEND_ASSERT(i == EX_VAR_TO_NUM(opline->op1.var));
+	ZEND_ASSERT(STACK_TYPE(stack, i) == IS_LONG);
+
+	/* The slot describes where the result of the operation is */
+	if ((STACK_FLAGS(stack, i) & ~ZREG_UNDO_MATH) == ZREG_TYPE_ONLY) {
+		/* The result was spilled to the VM stack slot it is bound to */
+		uint32_t res_var = (opline->opcode == ZEND_ASSIGN_OP) ? opline->op1.var : opline->result.var;
+
+		res = (zend_ulong)Z_LVAL_P(EX_VAR(res_var));
+	} else if (STACK_FLAGS(stack, i) & ZREG_SPILL_SLOT) {
+		ZEND_ASSERT(STACK_REG(stack, i) != ZREG_NONE);
+		res = *(zend_ulong*)((uintptr_t)regs->gpr[STACK_REG(stack, i)] + STACK_REF(stack, i));
+	} else {
+		ZEND_ASSERT(STACK_REG(stack, i) != ZREG_NONE);
+		res = (zend_ulong)regs->gpr[STACK_REG(stack, i)];
+	}
+
+	if (opline->op2_type == IS_CONST) {
+		zval *zv = RT_CONSTANT(opline, opline->op2);
+
+		ZEND_ASSERT(Z_TYPE_P(zv) == IS_LONG);
+		op2 = (zend_ulong)Z_LVAL_P(zv);
+	} else {
+		op2 = (zend_ulong)zend_jit_trace_exit_slot_lval(execute_data, t, regs, stack, EX_VAR_TO_NUM(opline->op2.var));
+	}
+
+	/* Unsigned arithmetic: the operation overflowed */
+	return (zend_long)((opcode == ZEND_ADD) ? res - op2 : res + op2);
+}
+
 int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf *regs)
 {
 	uint32_t trace_num = EG(jit_trace_num);
@@ -8751,6 +8841,8 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf
 			} else {
 				ZVAL_COPY(EX_VAR_NUM(i), val);
 			}
+		} else if (STACK_FLAGS(stack, i) & ZREG_UNDO_MATH) {
+			ZVAL_LONG(EX_VAR_NUM(i), zend_jit_trace_exit_undo_math(execute_data, t, exit_num, regs, stack, i));
 		} else if (STACK_FLAGS(stack, i) & ZREG_SPILL_SLOT) {
 			ZEND_ASSERT(STACK_REG(stack, i) != ZREG_NONE);
 			uintptr_t ptr = (uintptr_t)regs->gpr[STACK_REG(stack, i)] + STACK_REF(stack, i);

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