Zeev,

> So the code after the fix would look like this:
> <?php declare(strict_types=1);
> function foo(int $int): int {
>     return $int + 1;
> }
>
> function bar(int $something): int {
>     $x = (int) $something / 2;  // (int) or whatever else makes it clear
> it's an int
>     return foo($x);
> }
> ?>
>
> Let me explain how this could play out with coercive type hints:
> <?php
> function foo(int $int): int {
>     return $int + 1;
> }
>
> function bar(int $something): int {
>     $x = $something / 2;
>     return foo($x);
> }
>
> We can all agree that determining the types of just about anything here is
> ultra-easy, so easy you could do it with a static analyzer, as you
> suggested.  $int and $something are integers, while $x is either an integer
> or a float.  We also know that both foo() and bar() expect integers.
>
> What's the optimal code we could generate here?
> First, on the function body of foo(), we can clearly and easily translate
> the whole into machine code, as we know we'll get a long and need to return
> a long.
> Moving to the caller scope in bar(), given we know $x is either a float or
> an integer, we could either generate code that calls coerce_to_int($x), or
> even some optimize machine code that checks zval.type and either uses the
> lval or converts dval.  This can be done in AOT, no need to wait for
> runtime.  Once we know for a fact we have an integer in our hands - we can
> make the call directly to the optimized foo(), a C level call without the
> overhead of a PHP function call.

Well, yes and no.

In this simple example, you could generate the division as float
division, then checking the mantissa to determine if it's an int.

long bar(long something) {
    double x = something / 2;
    if (x != (double)(long)x) {
        raise_error();
    }
    return foo((long) x);
}

You're still doubling the number of CPU ops and adding at least one
branch at runtime, but not a massive difference.

However in general you'd have to use something like div_function and
use a union type of some sort. You mention this (about checking
zval.type at runtime). My goal would be to avoid using unions at all
(and hence no zval). Because that drastically simplifies both compiler
and code generator design.

Especially for a JIT compiler (local, not tracing) simplified design
generally translates to significantly faster runtime. Compare LLVM to
libjit: 50x difference in compile time.

> If you look at the generated code, it's going to be remarkably similar
> between the two cases.  If the developer chooses to pick the casting route,
> it will look almost identical - except it will be convert_to_long() that is
> called instead of coerce_to_int(), the former being more aggressive than the
> latter.

I wouldn't even bother with that, I'd just use a C cast (well, the ASM
equivalent). Saves function calls, zval representation, etc.

> Can you see anything impossible or otherwise wrong with my description of
> how the AOT compiler would work in this case, with coercive type hints?  If
> not, there are no performance benefits for the Strict typed version after
> the user alters his code to behave similarly to what coercive type hints
> would bring.

It's very much not about impossible. It's about complexity. Strict
code is easier to reason about, it's easier to analyze and it's easier
to code-generate because all of the reduced amount that you need to
support. And we're not talking about making users change their code
drastically. We're talking about -in many cases- minor tweaks.

Minor tweaks that would need to be done with your proposal as well. So
if we're going to require users change their code, why not make it
opt-in and give them the predictability that we can?

>> > Got you.  Is it fair to say that if we got to that case, it no longer
>> > matters what type of type hints we have?
>>
>> Once you get to the end, no. Recki-CT proves that.
>
> Do you mean that the statement is unfair or that it no longer matters?   If
> it's the former, can you elaborate as to why?

No, I meant that Recki proves what you said (once you get to a stable
type analysis of even untyped code it doesn't matter the hints exist
or not).

>> >
>> > So when you say it 'must be an int', what you mean is that you assume
>> > it needs to be an int, and attempt to either prove that or refute
>> > that.  Is that correct?
>> > If you manage to prove it - you can generate optimal code.
>> > If you manage to refute that - the static analyzer will emit an error.
>> > If you can't determine - you defer to runtime.
>> >
>> > Is that correct?
>>
>> Basically yes.
>
> Let me describe here too how it may look with coercive hints.  Instead of
> beginning with the assertion that it must be an int, we make no guess as to
> what it may be(*).  We would use the very same methods you would use to
> prove or refute that it's an int, to determine whether it's an int.  Our
> ability to deduce that it's an int is going to be identical to your ability
> to prove that it's an int.  If we see that it comes from an int type hint,
> from an int typed function, etc. - we'd be able to generate the same ultra
> optimized C-level call.  If we manage to deduce that it may be an int or a
> float, we can still create an ultra-optimized calling code that would deal
> with just these two cases, or call coerce_to_int().  If we deduce that it's
> a type that cannot be converted to an int (e.g. array or resource) - we can
> emit a compile-time error.   And if we have no idea what it is, we emit a
> regular function call.  Going back to that (*) from earlier, even if we're
> unable to deduce what it is, we can actually assume/hope that it'll be an
> integer and if it is - pass it on directly to the C implementation with a C
> level function call;  And if not, go with the regular function call.
>
> The machine code you're left with is pretty much equivalent in case we
> reached the conclusion that the variable is an integer (which would be
> roughly in the same cases you're able to prove it that it is).  The
> difference would be that it allows for the non-integer types to be accepted
> according to the coercion rules, which is a functional difference, not
> performance difference.

Well, the end result is pretty much equivalent. But only pretty much.
In the example above, the few CPU ops and extra branch will very
likely slow down the code significantly (more than a factor of 2).
It's an extremely small difference, the literal definition of a
micro-optimization (strict would be 2 or 3 operations depending on
platform - div and cast(maybe) and push vs coercive's additional
branch). But when talking about code generation and compilers, these
differences may be non-trivial.

Again, not saying this is major enough to be concerned about, but it's
not identical. There are small differences.

The big difference though is the complexity of the analyzer and
compiler (time, resources, programmer skill) to build one for the
coercive case. With strict types, its drastically easier (simply less
cases to be concerned about).

Anthony

-- 
PHP Internals - PHP Runtime Development Mailing List
To unsubscribe, visit: http://www.php.net/unsub.php

Reply via email to