Skip to content

Avoid type checks in async completion implementation #48226

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

Open
mkustermann opened this issue Jan 26, 2022 · 3 comments
Open

Avoid type checks in async completion implementation #48226

mkustermann opened this issue Jan 26, 2022 · 3 comments
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-performance Issue relates to performance or code size

Comments

@mkustermann
Copy link
Member

For an async method like this:

Future<String> foo() async {
  return 'hello world';
}

we generate currently this kernel:

  static method foo() → Future<String>  {
    final _Future<String> :async_future = new _Future::•<String>();
    ...
    bool* :is_sync = false;
    FutureOr<String>? :return_value;
    ...
    function :async_op([dynamic :result, dynamic :exception, dynamic :stack_trace]) → dynamic yielding 
      ...
       :return_value = "hello world";
      _completeOnAsyncReturn(:async_future, :return_value, :is_sync);
      return;
    }
    ...
    return :async_future;
  }

The value of the return is fully typed all along:

  • the future is a _Future<String>
  • the resulting value is a FutureOr<String>

Though we loose the correct type when we call _completeOnAsyncReturn, which looks like this:

@pragma("vm:entry-point", "call")
void _completeOnAsyncReturn(_Future _future, Object? value, bool is_sync) {
  if (!is_sync || value is Future) {
    _future._asyncComplete(value);
  } else {
    _future._completeWithValue(value);
  }
}

Notice, the parameter is typed _Future.

The future implementation then looks like this:

class _Future<T>  {
  ...
  void _asyncComplete(FutureOr<T> value) {                                                                          
    if (value is Future<T>) {
      _chainFuture(value);       
      return;     
    }  
    _asyncCompleteWithValue(value as dynamic); // Value promoted to T.   
  }
  ...
}

This function performs

  • a type check for the parameter (call site might've up-casted _Future<X> to _Future<Object> ...)
    => Though actually we know the call site, it was generated by kernel transformer, we know that the check will succeed.
    => This is probably an unnecessary check.
  • is Future<T>
    => We already know value is FutureOr<T>.
    => Would it be sufficient to just check is Future (instead of is Future<T>) - makes the check faster/simpler?
  • the _asyncCompleteWithValue(value as dynamic) will cause an implicit value as T check.
    => We already know that it is T and should not perform any checking here.

/cc @lrhn

(/cc @sstrickl The FutureOr<T> covariant parameter check falls through to STC which overflows)

@mkustermann mkustermann added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. type-performance Issue relates to performance or code size labels Jan 26, 2022
@mkustermann
Copy link
Member Author

Since we'll have a hard time telling the VM to omit the parameter type check for _Future<T>._asyncComplete(FutureOr<T> value) when coming from that particular callsite, I think it's easier to introduce a new method on _Future that the VM can invoke where no check is performed.

=> Will try cl/230261

@lrhn
Copy link
Member

lrhn commented Jan 28, 2022

It's generally not sufficient to check is T or is Future to deduce that a FutureOr<T> is a Future<T>.

The counter example in one direction is a FutureOr<Object> which is actually a Future<dynamic>.
Checking just is Future is insufficient to recognize that it's not actually a Future<Object>.

In the other direction, a FutureOr<Object> which is actually a Future<int> is an Object.

We really need to check against Future<T> to be completely certain that we get the correct (and sound) semantics.
(And this is why I want to disallow returning futures directly from async functions, dart-lang/language#870, because there is no good way to implement it efficiently, and the overhead costs even for code which doesn't need it).

As for adding an _asyncCompleteWithValueUnchecked(Object? value) { T typedValue = unsafeCast<T>(value); ... }, it can definitely work (put it in a VM patch file, it's VM specific).
Just be absolutely effing certain it's only being used correctly, because unsoundness is bad.

@mkustermann
Copy link
Member Author

mkustermann commented Jan 28, 2022

And this is why I want to disallow returning futures directly from async functions, dart-lang/language#870,

Would love to see that land!

there is no good way to implement it efficiently, and the overhead costs even for code which doesn't need it

I have thought of a workaround to make it efficient for the majority of cases: Namely rely on the static types: Recognize that all returns in a function return types that cannot possibly be a Future. For such functions we can then generate code that avoids the is Future<T> check.
-> See cl/230662

copybara-service bot pushed a commit that referenced this issue Feb 4, 2022
…er knows its safe

Currently we have to do a runtime `as FutureOr<T>` check for the
parameter to `_Future<T>.asyncComplete(FutureOr<T> value)`. Although
needed for general cases, for the particular usage in the VM's
async/await transformer, we know that the test is going to suceed.

This CL adds two VM-specific methods on `_Future` that take `dynamic`
and downcast via `unsafeCast<>()` to avoid this rather large runtime
type checking overhead.

Issue #48226

TEST=ci

Change-Id: Ieeffae3ac8e2960f849512cf22c51d41cadb1ecf
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230261
Reviewed-by: Slava Egorov <[email protected]>
Reviewed-by: Lasse Nielsen <[email protected]>
Commit-Queue: Martin Kustermann <[email protected]>
copybara-service bot pushed a commit that referenced this issue Feb 4, 2022
…hen caller knows its safe"

This reverts commit 8e0feb9.

Reason for revert: breaks tests in Google3, see b/217919095

Original change's description:
> [vm] Avoid type parameter check of _Future._asyncComplete() when caller knows its safe
>
> Currently we have to do a runtime `as FutureOr<T>` check for the
> parameter to `_Future<T>.asyncComplete(FutureOr<T> value)`. Although
> needed for general cases, for the particular usage in the VM's
> async/await transformer, we know that the test is going to suceed.
>
> This CL adds two VM-specific methods on `_Future` that take `dynamic`
> and downcast via `unsafeCast<>()` to avoid this rather large runtime
> type checking overhead.
>
> Issue #48226
>
> TEST=ci
>
> Change-Id: Ieeffae3ac8e2960f849512cf22c51d41cadb1ecf
> Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230261
> Reviewed-by: Slava Egorov <[email protected]>
> Reviewed-by: Lasse Nielsen <[email protected]>
> Commit-Queue: Martin Kustermann <[email protected]>

[email protected],[email protected],[email protected]

Change-Id: I37a07cbb6fb37620e5305dfe1b759b0de1c37653
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/231703
Reviewed-by: Ilya Yanok <[email protected]>
Commit-Queue: Ilya Yanok <[email protected]>
copybara-service bot pushed a commit that referenced this issue Feb 4, 2022
…hen caller knows its safe"

Currently we have to do a runtime `as FutureOr<T>` check for the
parameter to `_Future<T>.asyncComplete(FutureOr<T> value)`. Although
needed for general cases, for the particular usage in the VM's
async/await transformer, we know that the test is going to suceed.

This CL adds two VM-specific methods on `_Future` that take `dynamic`
and downcast via `unsafeCast<>()` to avoid this rather large runtime
type checking overhead.

Issue #48226

Change after revert: Replace the
    assert(value is T || value is Future<T>);
with
    assert((value as FutureOr<T>) == value);

The former doesn't allow `null` while the ladder might (depending
on nullability of `T`).

TEST=ci

Change-Id: I2379c625003fea6aa836ac74beb1cd59201b3560
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/231704
Reviewed-by: Alexander Markov <[email protected]>
Commit-Queue: Martin Kustermann <[email protected]>
copybara-service bot pushed a commit that referenced this issue Feb 4, 2022
… returned values

In general any async function can return X or Future<X>. Though the
future implementation has to do different things depending on which case
we're in. It does so by using a `<obj> is Future<T>` test. This test is very
expensive if many different classes flow into `<obj>`.

Though most functions do not return `Future` objects from async
functions. We can determine that statically by looking if any returned
expression could be a future. If not, we can bypass the `is Future<T>`
test entirely.

Issue #48226
Issue #48235

TEST=pkg/front_end/testcases/general/async_function_returns_future_or.dart

Change-Id: If655bdbdddc214dd7b12be9905b3c788252547d0
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/230662
Reviewed-by: Alexander Markov <[email protected]>
Reviewed-by: Lasse Nielsen <[email protected]>
Commit-Queue: Martin Kustermann <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. type-performance Issue relates to performance or code size
Projects
None yet
Development

No branches or pull requests

2 participants