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
Improve performance of ZEND_IS_IDENTICAL #4900
Conversation
Zend/zend_vm_def.h
Outdated
FREE_OP1(); | ||
FREE_OP2(); | ||
ZEND_VM_SMART_BRANCH(result, 1); | ||
ZEND_VM_SMART_BRANCH(result, 0); |
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.
It looks like, you messed the meaning of the second argument of ZEND_VM_SMART_BRANCH().
It should be 1 here (because FREE_OP on IS_OBJECT may call destructor that throws exception) and 0 in other places.
Zend/zend_vm_def.h
Outdated
/* This has to check for undefined variable errors when IS_NULL is possible. */ | ||
FREE_OP1(); | ||
FREE_OP2(); | ||
ZEND_VM_SMART_BRANCH(1, 1); |
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.
I guess this can probably be ((OP1_TYPE & IS_CV) || (OP2_TYPE & IS_CV))
, if only CVs can be undefined variables. http://nikic.github.io/2017/04/14/PHP-7-Virtual-machine.html#variable-types
@@ -451,7 +451,50 @@ ZEND_VM_COLD_CONSTCONST_HANDLER(16, ZEND_IS_IDENTICAL, CONST|TMP|VAR|CV, CONST|T | |||
SAVE_OPLINE(); | |||
op1 = GET_OP1_ZVAL_PTR_DEREF(BP_VAR_R); | |||
op2 = GET_OP2_ZVAL_PTR_DEREF(BP_VAR_R); | |||
result = fast_is_identical_function(op1, op2); | |||
if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) { |
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.
I'd suggest to fetch op1 as UNDEF and handle the REF/UNDEF cases explicitly, so we get a fast path if they do not apply.
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.
I was actually working on that locally - I didn't see much of a performance boost locally (I assume it's a compare and conditional jump, for 2 instructions)
- Maybe moving an
is_identical_function
implementation into the
I was also thinking of skipping the IS_UNDEF check for const/tmp/var, and IS_INDIRECT for everything except var
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.
https://github.com/TysonAndre/php-src/pull/new/ZEND_IS_IDENTICAL-wip-broken - will fix this later today or tomorrow.
I forgot to check for IS_INDIRECT in the case for if (Z_TYPE_P(op1) != Z_TYPE_P(op2)) {
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.
The issue I was having was caused by opcache being enabled in the php.ini that would have been used by the installation. I think IS_INDIRECT still needed to be handled
And for ZEND_IS_NOT_IDENTICAL. - Take advantage of branch prediction in cases that are false/true - Avoid function call and duplicate fetches of Z_TYPE_P in common use cases - Avoid checking for EG(exception) when it's impossible. I think that the zval would always be IS_NULL or lower for an exception, but may be wrong. Aside 1: I see that ZEND_IS_EQUAL does have the `SMART_BRANCH` in the `SPEC`, generating optimized implementations based on what type of smart branch is used. Is excluding it from ZEND_IS_IDENTICAL deliberate, and if so, what's the reasoning? (e.g. is the use in common applications low?) - The SMART_BRANCH specialization was introduced in 27e01cd Aside 2: The function pointer for ZEND_IS_EQUAL_LONG can probably be used directly for ZEND_IS_IDENTICAL_LONG. I'm not sure how to generate that. (and DOUBLE) The amount of time for nestedloop_ni(20) decreased from 0.726s to 0.593s due to this change. This is still worse than `!=` (0.434) and `<` (0.375), which have implementations that take advantage of branch prediction for IS_LONG ```php function nestedloop_ni($n) { $x = 0; for ($a=0; $a!==$n; $a++) for ($b=0; $b!==$n; $b++) for ($c=0; $c!==$n; $c++) for ($d=0; $d!==$n; $d++) for ($e=0; $e!==$n; $e++) for ($f=0; $f!==$n; $f++) $x++; print "$x\n"; } ```
Locally, I didn't see that much of a performance boost in absolute terms compared to inlining zend_is_identical (compare+unlikely jump is probably 2 instructions)
7482b71
to
2f96eba
Compare
macOS fails to build |
Still 2 macOS build error:
|
c47e9b3
to
18a672b
Compare
The PR makes some improvement using aggressive code inlining. The typical use of ===/!== is a comparison with a constant. @TysonAndre you may also think about general specialization on types of constants. This is more serious and questionable task. It may eliminate a lot of useless run-time cheks in VM, but this also may significantly increase the code size. So it's hard to say, if this is going to be accepted before we see a PoC. |
Is that for
(there are now reused handlers for ZEND_IS_IDENTICAL handling both sides being longs and both sides being doubles) It'd make sense that comparison with scalars would be more common in a typical application |
I meant: ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, (op->op2_type == IS_CONST && RT_CONSTANT(op, op->op2) == IS_LONG), ZEND_IS_IDENTICAL_LONG, TMPVARCV, CONST) ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, (op->op2_type == IS_CONST && RT_CONSTANT(op, op->op2) == IS_DOUBLE), ZEND_IS_IDENTICAL_DOUBLE, TMPVARCV, CONST) ZEND_VM_HOT_TYPE_SPEC_HANDLER(ZEND_IS_IDENTICAL, (op->op2_type == IS_CONST && RT_CONSTANT(op, op->op2) == IS_STRING), ZEND_IS_IDENTICAL_STRING, TMPVARCV, CONST) |
This has a large performance benefit for the below snippet (0.64s vs 0.94s), and a small benefit for `$alwaysStr == 'notnumeric'`/`$s = 'other'` (for 1000 runs of test('test')) - If the literal is non-numeric and the variable is a string, `==` behaves identically to `===`, so reuse the opcode. This is similar to what was suggested in php#4900 - Avoiding types with __destruct or notices simplifies the specialized implementation and avoids expensive exception handling. - TMPVARCV doesn't support FREE_OP1() - use TMPVAR|CV instead. ``` function test(string $s) : int { $total = 0; for ($i = 0; $i < 100000; $i++) { if ($s === "first") { $total++; $s = null; } } return $total; } ```
Specialized type handlers seem to be a better approach - my subsequent PRs have better performance gains. |
This has a large performance benefit for the below snippet (0.64s vs 0.94s), and a small benefit for `$alwaysStr == 'notnumeric'`/`$s = 'other'` (for 1000 runs of test('test')) - If the literal is non-numeric and the variable is a string, `==` behaves identically to `===`, so reuse the opcode. This is similar to what was suggested in php#4900 - Avoiding types with __destruct or notices simplifies the specialized implementation and avoids expensive exception handling. - TMPVARCV doesn't support FREE_OP1() - use TMPVAR|CV instead. ``` function test(string $s) : int { $total = 0; for ($i = 0; $i < 100000; $i++) { if ($s === "first") { $total++; $s = null; } } return $total; } ```
This has a large performance benefit for the below snippet (0.64s vs 0.94s), and a small benefit for `$alwaysStr == 'notnumeric'`/`$s = 'other'` (for 1000 runs of test('test')) - If the literal is non-numeric and the variable is a string, `==` behaves identically to `===`, so reuse the opcode. This is similar to what was suggested in php#4900 - Avoiding types with __destruct or notices simplifies the specialized implementation and avoids expensive exception handling. - TMPVARCV doesn't support FREE_OP1() - use TMPVAR|CV instead. ``` function test(string $s) : int { $total = 0; for ($i = 0; $i < 100000; $i++) { if ($s === "first") { $total++; $s = null; } } return $total; } ```
And for ZEND_IS_NOT_IDENTICAL.
Take advantage of branch prediction in cases that are false/true
Avoid function call and duplicate fetches of Z_TYPE_P in common use cases
Avoid checking for EG(exception) when it's impossible.
I think that the zval would always be IS_NULL or lower for an exception,
but may be wrong.
Aside 1: I see that ZEND_IS_EQUAL does have the
SMART_BRANCH
in theSPEC
, generating optimized implementations based on what type of smartbranch is used. Is excluding SMART_BRANCH from ZEND_IS_IDENTICAL deliberate,
and if so, what's the reasoning?
Aside 2: The function pointer for ZEND_IS_EQUAL_LONG can probably be used
directly for ZEND_IS_IDENTICAL_LONG. I'm not sure how to generate that.
(and DOUBLE)
The amount of time for nestedloop_ni(20) decreased from 0.726s to
0.593s due to this change.
This is still worse than
!=
(0.434) and<
(0.375), whichhave implementations that take advantage of branch prediction for
IS_LONG (i.e.
EXPECTED
blocks)