-
Notifications
You must be signed in to change notification settings - Fork 7.8k
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
Fix JIT for overridden properties with added hooks #17395
base: master
Are you sure you want to change the base?
Conversation
Some code in that function breaks that I don't yet fully understand. I'll need to take a closer look tomorrow. |
2f30e3d
to
e1237ca
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I understood this fix adds additional checks for each "known" non-final property read.
At first the fix looks incomplete. It doesn't take into account other property accessors. ASSIGN_PROP, ASSIGN_PROP_OP, PRE/POST_INC/DEC_PROP, ISSET_PROP (may be others).
At second this is a big overhead. 9 assembler instructions instead of 5 (+3 additional loads) for almost every fast-path property access. Tracing JIT with Symfony Demo and this incomplete fix starts to generate 1.5% more code and becomes 0.2% slower. I can't express my attitude in normative language...
For tracing JIT you may try to fix the problem using class guards. At least they are a bit cheaper (2 instructions, 1 load) and may be checked once for SSA variable.
I don't see how the fix may be improved for function JIT.
I'm aware the fix is incomplete, but I wanted to wait for feedback first. For tracing JIT, in case of compile-time unknown property infos, we use the property info discovered during tracing. This avoids the additional check, as we set instanceof to 0. We could simplify this check further by checking for the class instead, but accessing properties from subclasses is common, so this would result in more slow path executions. Can you clarify on your guard suggestion? I don't understand the difference to the check in this PR. |
The most usual case (accessing property of $this) didn't require class guard. The code from class Foo {
public $b = 0;
function read_prop($n) {
for ($i = 0; $i < $n; ++$i) {
$x = $this->b;
}
}
} The fix adds check for property info on each loop iteration. Now take a look into something like
You are right. This solution is not ideal. The class guards failure may lead to recording and compiling identical traces for different subclasses that access property declared in a base class.
I hope, I explained this above. What was the reason in ability to add hooks in subclasses for a regular property declared in the parent? |
I see, thank you for the explanation. Another thing we could do is add a "property hooks" guard, i.e. check that the traced object doesn't have any property hooks (
It was a fundamental design decision to make plain properties completely compatible with hooks. Some languages only allow adding accessors to child classes for |
ext/opcache/jit/zend_jit_ir.c
Outdated
prop_info_ref = ir_LOAD_A(ir_ADD_OFFSET(prop_info_ref, prop_info_offset)); | ||
ir_ref is_same_prop_info = ir_EQ(prop_info_ref, ir_CONST_ADDR(prop_info)); | ||
if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE) { | ||
int32_t exit_point = zend_jit_trace_get_exit_point(opline, ZEND_JIT_EXIT_TO_VM); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ZEND_JIT_EXIT_TO_VM looks too aggressive. Why can't we record another trace with slow-path JIT-ed code.
To be more accurate instead of checking for the same prop_info, we may check prop_info->hooks == NULL
.
I don't see why "unexpectedly appeared" property hooks should lead to exit to VM. They should be handled by side traces. So I don't see how "property hooks" would really help (may be I didn't understand yet).
This is a yet anther shoot in the leg. The languages designers made that restriction on purpose. |
There's no reason other than that I just don't understand it well, I don't know how this would be implemented.
My idea was basically to create a new type of guard that checks if the runtime object declares any new property hooks that the compile time
That's an assumption. Newer languages do not differentiate between properties and fields, e.g. Swift or Kotlin. C# does it, but it's also historic, because fields were a thing before properties. It also has ABI implications there, which do not apply here. |
Are you sure they allow "conversion" of field to property, may be only If this is not true you should take a look into Swift/Kotlin implementations (find related papers or speak to their experts) to understand how they solve the problem - "It's not possible to simple read object filed if some child class may convert it to property". One more idea how this might be implemented in PHP. PHP always performs type check when reads a filed (this is stupid but even typed field may be unset). In case we might set type of filed (that is converted to property) to be IS_UNDEF, it would start to work out of the box. I suspect, this solution won't work out of the box, but may be you'll find something on top of it. |
I will think about more solutions, but I will also need to take some time just understanding traces and guards better. I don't currently understand the difference between a side-exit and falling back to the interpreter, in other contexts I've seen those two used interchangeably. |
Initially any failed guard leads to transferring control to VM interpreter. |
Just to clarify: Yes, they do allow "virtual" properties, they don't just shadow the properties, if that's what you meant. E.g. see Kotlin here https://pl.kotl.in/iXAa7j5ID. I'm certainly not an expert in these languages but I suspect that they can access fields directly when the class hierarchy is known, thus allowing to omit a call to the accessor function. Of course, another great benefit there is that they are
I have also thought of this. We could think about allocating another slot in the object, but this still won't help if the assignment in the JIT happens to the original property slot. So at least this part would have to be avoided in some way. |
I see. A bit better example https://pl.kotl.in/Dbkz5gvge and the generated JS function printProp(a) {
print(a.x_1);
print(a.get_y_1mhr68_k$());
print(a.get_prop_wosl9o_k$());
print(a.foo_26di_k$());
print('\n');
}
Unfortunately in PHP properties are not final by default. |
For context, Swift does not require |
This site doesn't work for me. Try to analyse the generated code. When it reads fields directly? When access through getters? |
@dstogov I used https://godbolt.org/noscript/swift with the Script
import Foundation
class A {
var prop: Int = 1
let prop2: Int
var prop3: Int
init(prop2: Int, prop3: Int) {
self.prop2 = prop2
self.prop3 = prop3
}
}
class B: A {
override var prop: Int {
get {
return super.prop * 2
}
set {
super.prop = newValue
}
}
}
func print1(a: A) {
print(a.prop)
}
func print2(a: A) {
print(a.prop2)
}
func print3(a: A) {
print(a.prop3)
}
print1(a: A(prop2: 42, prop3: 43))
print2(a: B(prop2: 42, prop3: 43))
print3(a: B(prop2: 42, prop3: 43)) |
It seems like swift always reads properties/fields through virtual getters. (I tried your old example with output.printProp(a: output.A) -> ():
push r13
push rbx
push rax
mov r13, rdi ; keep "a" in %r13
lea rdi, [rip + (demangling cache variable for type metadata for Swift._ContiguousArrayStorage<Any>)]
call __swift_instantiateConcreteTypeFromMangledName
mov esi, 64
mov edx, 7
mov rdi, rax
call swift_allocObject@PLT
mov rbx, rax
mov qword ptr [rax + 16], 1
mov qword ptr [rax + 24], 2
mov rax, qword ptr [r13] ; load VMT into %rax
call qword ptr [rax + 64] ; call some virtual property getter
mov rcx, qword ptr [rip + ($sSiN)@GOTPCREL]
mov qword ptr [rbx + 56], rcx
mov qword ptr [rbx + 32], rax
movabs rdx, -2233785415175766016
mov esi, 32
mov ecx, 10
mov rdi, rbx
mov r8, rdx
call ($ss5print_9separator10terminatoryypd_S2StF)@PLT
mov rdi, rbx
add rsp, 8
pop rbx
pop r13
jmp swift_release@PLT |
I don't really understand this assembly, but the |
Oh, and making the properties |
You messed print2 and print3. (print3 has an additional |
Conclusion: Kotlin uses getters for "open" properties, this is not a big problem as the properties are final by default. Swift always use getters for "var", but optimizer may inline them in some cases. PHP with non-final properties has to check for hooks on each access (VM optimizes this caching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dstogov What do you think about this solution for now? Unfortunately, I can still see an instruction increase of 1.6% for Symfony Demo, but I could not measure any difference in performance.
/* Child did not add any new properties, we are done */ | ||
if (ce->default_properties_count == ce->parent->default_properties_count) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should move this to a separate PR, but IMO this should still be fixed.
@@ -14435,7 +14485,7 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit, | |||
SET_STACK_REG(JIT_G(current_frame)->stack, EX_VAR_TO_NUM(opline->op1.var), ZREG_NONE); | |||
} | |||
|
|||
if (JIT_G(trigger) != ZEND_JIT_ON_HOT_TRACE || !prop_info) { | |||
if (slow_inputs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could technically be reverted, but seems more logical.
Fixes GH-17376
The change in
zend_build_properties_info_table()
is needed to keep overridden property infos up-to-date in child classes that only override properties but add no new ones. The behavior is currently inconsistent, which is itself probably a bug.It would be faster to only compare the class itself instead of the property info, but being pessimistic may lead to picking the slow path too often, which would be much more expensive. @dstogov Maybe you have some better suggestion.