Open dcharkes opened 3 years ago
When the test fails, a nested struct accessed on a larger struct which has a typed data as backing store, is returned as a struct with a pointer as backing store, rather than a typed data view. This happens because _addressOf
is inferred to have the type Pointer
(which it has not in this case, it's a TypedData
). This might be speculative optimization.
==== file:///usr/local/google/home/dacoharkes/dart-sdk/sdk/tests/ffi_2/function_structs_by_value_generated_test.dart_Struct8BytesNestedInt_get_a0 (GetterFunction)
B0[graph]:0 {
v0 <- Constant(#null)
v1 <- Constant(#<optimized out>)
v7 <- Constant(#Type: Pointer*) T{_Type}
v10 <- Constant(#true) T{bool}
v19 <- Constant(#_ImmutableList len:3) T{_ImmutableList}
v26 <- Constant(#_ImmutableList len:3) T{_ImmutableList}
v35 <- Constant(#TypeArguments: (H1d7e78bd) [Type: Struct4BytesHomogeneousInt16*]) T{TypeArguments}
}
B1[function entry]:2 {
v2 <- Parameter(0) T{Struct8BytesNestedInt}
}
CheckStackOverflow:8(stack=0, loop=0)
v3 <- AllocateObject(Struct4BytesHomogeneousInt16) T{Struct4BytesHomogeneousInt16}
v5 <- InstanceCall:10( get:_addressOf@8050071<0>, v2 IC[0: ]) T{Object?}
v8 <- InstanceCall:12( _simpleInstanceOf@0150898<0>, v5, v7 IC[0: ]) T{*?}
[...]
After ApplyClassIds
==== file:///usr/local/google/home/dacoharkes/dart-sdk/sdk/tests/ffi_2/function_structs_by_value_generated_test.dart_Struct8BytesNestedInt_get_a0 (GetterFunction)
B0[graph]:0 {
v0 <- Constant(#null)
v1 <- Constant(#<optimized out>)
v7 <- Constant(#Type: Pointer*) T{_Type}
v10 <- Constant(#true) T{bool}
v19 <- Constant(#_ImmutableList len:3) T{_ImmutableList}
v26 <- Constant(#_ImmutableList len:3) T{_ImmutableList}
v35 <- Constant(#TypeArguments: (H1d7e78bd) [Type: Struct4BytesHomogeneousInt16*]) T{TypeArguments}
}
B1[function entry]:2 {
v2 <- Parameter(0) T{Struct8BytesNestedInt}
}
CheckStackOverflow:8(stack=0, loop=0)
v3 <- AllocateObject(Struct4BytesHomogeneousInt16) T{Struct4BytesHomogeneousInt16}
CheckClass:10(v2 Cids[1: Struct8BytesNestedInt etc. cid 1132])
v5 <- LoadField(v2 . _addressOf@8050071 {final}) T{Pointer}
On failing runs with --trace-field-guards --optimization-counter-threshold=5
Store Field <Struct._addressOf@8050071>: final <?> <- Pointer<Struct20BytesHomogeneousInt32>: address=0x55b86aabf9c0
=> <not-nullable Pointer>
On succeeding runs with --trace-field-guards --optimization-counter-threshold=20
Store Field <Struct._addressOf@8050071>: final <?> <- Pointer<Struct20BytesHomogeneousInt32>: address=0x562806815980
=> <not-nullable Pointer>
Store Field <Struct._addressOf@8050071>: final <not-nullable Pointer> <- TypedDataView(cid: 104)
=> <*>
This looks like speculative optimization failing to deoptimize when assumptions no longer hold.
Working theory:
Fragment FlowGraphBuilder::WrapTypedDataBaseInStruct(
const AbstractType& struct_type) {
const auto& struct_sub_class = Class::ZoneHandle(Z, struct_type.type_class());
struct_sub_class.EnsureIsFinalized(thread_);
const auto& lib_ffi = Library::Handle(Z, Library::FfiLibrary());
const auto& struct_class =
Class::Handle(Z, lib_ffi.LookupClass(Symbols::Struct()));
const auto& struct_addressof = Field::ZoneHandle(
Z, struct_class.LookupInstanceFieldAllowPrivate(Symbols::_addressOf()));
ASSERT(!struct_addressof.IsNull());
Fragment body;
LocalVariable* typed_data = MakeTemporary("typed_data_base");
body += AllocateObject(TokenPosition::kNoSource, struct_sub_class, 0);
body += LoadLocal(MakeTemporary("struct")); // Duplicate Struct.
body += LoadLocal(typed_data);
body += StoreInstanceField(struct_addressof,
StoreInstanceFieldInstr::Kind::kInitializing);
body += DropTempsPreserveTop(1); // Drop TypedData.
return body;
}
runtime/vm/compiler/frontend/kernel_to_il.cc
This allocates an object, stores a TypedDataView
into it, without telling the JIT that that now Struct._addressOf
can contain a TypedDataView
. Which makes the JIT rely on that it has only seen Pointers
in Struct._addressOf
, which makes this test fail.
(Trying to add the guard fails, because FFI trampolines are force optimized.)
body += StoreInstanceFieldGuarded(
struct_addressof, StoreInstanceFieldInstr::Kind::kInitializing);
I'm unable to construct a case of this on master (without the CL):
TypedData
or Pointer
.addressOf
always does an assert assignable due to the type argument having to be checked, even if the type feedback only contains Pointer
.It fails in the nested struct CL because we branch on Pointer<T extends NativeType>
instead of on Pointer<T>
in various places where the code branches on whether something is a Pointer
or TypedData
.
Because we have a workaround, this is not crashing.
As discussed in today's meeting we should separate allocation from sizeof calculation (similar to C
code) for code size / tree shake reasons and performance reasons. We could use an API such as
// In "dart:ffi"
abstract class Allocator {
Pointer<Void> allocate(int numBytes);
void free(Pointer<Void> pointer);
}
// In "dart:ffi"
extern Pointer<T> allocate<T extends Struct>(Allocator allocator);
// In "package:ffi/ffi.dart"
class MallocAllocator extends Allocator { ... }
class ZoneAllocator extends Allocator { ... }
final malloc = MallocAllocator();
// User code:
void main() {
Pointer<Foo> foop = allocate<Foo>(malloc);
...
free<Foo>(foop, malloc);
}
// User code (lowered to kernel):
void main() {
Pointer<Foo> foop = malloc.allocate(sizeOf<Foo>(malloc) /* <-- or rather it's lowered form */).cast<Foo>();
...
malloc.free(foop.cast<Void>());
}
So we'll
fd2e9b9f1af9e2ced5d263956f82e4ef3b583c8a Made trampolines explicit in the CFE.
If we make the FFI trampoline actually return TypedData
instead of T extends Struct
and wrap the result in the struct constructor, both the TFA and JIT would know about the struct constructor invocations. We'd need to do this for both the closures and the @Native
external functions.
Update 2021-01-05: The issue is that the JIT trampolines do not report type feedback that
Struct. _addressOf
containsTypedData
on return values and arguments.This could be addressed by making the VM know the layout of Struct and its subtypes. However, that makes the tree shaker no longer understand those types and their constructor calls. So, we might not want to make those types and their constructors opaque to the CFE&TFA.
We have a workaround, so this does not crash.
===============================================================================
In https://dart-review.googlesource.com/c/sdk/+/169221 we add nested structs and copying of nested structs.
Edit: the CQ also caught this: https://logs.chromium.org/logs/dart/buildbucket/cr-buildbucket.appspot.com/8861204090372525392/+/steps/test_results/0/logs/new_test_failures__logs_/0
Test succeeds:
Test fails: