-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[vm/ffi] Support treeshaking of FFI structs #38721
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
Comments
This problem is of a more general kind: The runtime might use some members of the Dart code based on some condition. In our specific case using One way we could do this is to support conditional entry points: class MyStruct extends Struct<MyStruct> {
@prama("vm:entry-point-if", EntryPointCondition.ifUsedAsType(MyStruct))
static final field int #sizeOf = ...;
@prama("vm:entry-point-if", EntryPointCondition.ifUsedAsType(MyStruct))
constructor fromPointer(dynamic )
...
} TFA as well as the VM's AOT compiler have a notion of a class being retained for it's type. We could build a mapping and every time a class is retained for its type we can enqueue conditional entry points. |
Addressing #38648, together with disallowing However, we still need it on We could keep the unoptimized |
@mkustermann I think it would be hard to support entry points which depend on the outcome of tree shaking due to circular nature of such entry points. We need to take entry points into account during type flow analysis, while notion of classes retained for types exists only at later tree shaking step which happens after analysis is finished. For conditional entry points we would need to get back to analysis and probably iterate between analysis and tree shaking until reaching fixed point:
@dcharkes Could you elaborate why we need these static If it's not possible, can we convert these static At last, we can have a custom logic in |
@alexmarkov we use it for being able to call The sizeOf is mostly statically computed. The size depends on the ABI, and kernel is ABI agnostic, so it's
Yes, but sometimes generically, as in It is also quite common that the only way objects are created in code using class Foo extends Struct { ... }
T allocateStruct<T extends Struct>(){
Pointer<T> pointer = allocate<T>();
T t = pointer.ref; // native entry of `.ref` manufactures an object, it calls the hidden constructor.
return t;
}
main() {
Foo f = allocateStruct<Foo>();
print(f);
} We would need to disallow generically calling However, when adding structs by value (#36730) we would introduce the same problem again, because then the return type of a native function could be that struct. Currently, already But this seems to be a general pattern, that the FFI can create objects without the constructors in the user code. |
Can we require type argument of Similarly, if we can require Generally speaking, an ability to create instances of arbitrary classes in native methods is not working well with tree shaking. This is one of the reasons why |
However, it would also properly make reasoning about size orthogonal to allocating the memory. @mkustermann and I discussed a design a while back that would separate allocation and sizing: class FooStruct extends Struct {
// Struct fields.
Pointer<FooStruct> allocate(Allocator a, int count) {
a.allocate(sizeOf<FooStruct>() * count).cast().
}
} In this design |
I like the Essentially your API surface for allocation becomes: // Represents how much memory to allocate, and the alignment requirements.
// Abstracts over computing things like repeating size (for arrays).
class Layout {
final int size, alignment;
const Layout(this.size, this.alignment);
// Assuming alignment is a power of 2.
// Equivalent to alloc::Layout::pad_to_align() in Rust
int get alignedSize => (size + (alignment - 1)) & ~(alignment - 1);
// Equivalent to alloc::Layout::repeat() in Rust
Layout repeat(int count) => Layout(alignedSize * count, alignment);
}
abstract class Allocator {
// Equivalent to alloc::Alloc::alloc in Rust
Pointer<Uint8> allocate(Layout layout);
// Equivalent to alloc::Alloc::dealloc in Rust
void free(Pointer<Uint8> ptr, Layout layout);
}
// Specialized to a constant T.#layout if T is statically known.
Layout layoutOf<T extends Struct>() => ...;
class MyStruct extends Struct {
// Generated static method
Pointer<MyStruct> allocate(Allocator alloc, int count) => alloc.allocate(layoutOf<MyStruct>().repeat(count)).cast();
} Then package:ffi can expose: class _Malloc extends Allocator { ... }
// Can also be final
const Allocator malloc = _Malloc(); And allocation becomes: MyStruct.allocate(malloc); // (with or without optional count) You can also introduce the concept of global allocators, but I feel that explicit allocators are a cleaner design. There are many situations where you'd want to avoid allocating from heap (like a bump allocation scheme for temporary objects, or the stack) and having the concept of allocators in dart:ffi gives a pretty nice starting point for library authors to jump off of. |
That's an excellent design. It would also work well with the We still have to refine the design to support that. Option 1: Merge Allocator and ResourceManagerWe could split up the abstract class Allocator {
// Equivalent to alloc::Alloc::alloc in Rust
Pointer<Uint8> allocate(Layout layout);
}
abstract class UnmanagedAllocator extends Allocator {
// Equivalent to alloc::Alloc::dealloc in Rust
void free(Pointer<Uint8> ptr, Layout layout);
}
abstract class ManagedAllocator extends Allocator {
// ..
} However, this conflates allocating/freeing with resource management in the same class hierarchy. One could have a Pro:
Con:
Option 2: Make the ResourceManager take an Allocator as argumentAlternatively, we should make the Pro:
Cons:
|
I think it's perfectly fine to have a single Allocator class with allocate and free. At the end of the day, free is just a hint to the allocator that the piece of memory referenced by ptr isn't going to be used by the application anymore. If free does nothing, the application will continue to function, but it may run out of memory after a while. For example, this bump allocation library https://crates.io/crates/bump_alloc, which does nothing on free. https://github.com/sheredom/bump_alloc/blob/master/src/lib.rs#L106 As far as Struct is concerned, it just needs something that is "layout in, pointer out". Other usages of Allocator (like a resizable vector) need a free, and maybe even a reallocate. Pool can totally extend Allocator, it just has to have an empty implementation of free. |
Quick note: I've found This have been useful for me when I needed something like: try {
ptr = pool.allocate();
...;
return pool.move(ptr); // remove ptr from pool
} finally {
pool.releaseAll();
} This avoid having to write |
Now that we have nested structs, objects a subtype of `Struct` can be backed by either a `Pointer` or a `TypedData`. Having this accessor is misleading. Instead of passing a struct around (which could be backed by either), the `Pointer<T extends Struct>` should be passed around and `.ref` should be used everywhere when access to the backing pointer is required. Issue: #40667 Related issues: * Optimize .ref #38648 * Support tree shaking of structs #38721 Change-Id: I3c73423480b91c00639df886bf1d6ac2e444beab Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177581 Reviewed-by: Martin Kustermann <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
As discussed in today's meeting we should separate allocation from sizeof calculation (similar to // 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>() /* <-- or rather it's lowered form */).cast<Foo>();
...
malloc.free(foop.cast<Void>());
} So we'll
Originally posted by @mkustermann in #44454 (comment) |
It's also useful to pass alignment into the allocator. Right now, if you want to write a proper bump pointer allocator, small types (for ex, Uint8) will have size overhead of the largest supported type alignment (8 bytes). Additionally a type like Float32x4 or Int32x4 would have different alignment requirements due to SIMD loads and stores. |
Then we would 'invoke' both If we would omit an |
To make this forwards compatible, we might want to make the abstract class Allocator {
Pointer<Void> allocate(int numBytes, int alignment);
void free(Pointer<Void> pointer);
} |
Introduces the Allocator API in `dart:ffi`. This CL does not yet roll `package:ffi` to use `Allocator`, because that breaks the checked in Dart in Fluter in g3. Instead, this coppies `_MallocAllocator` from `package:ffi` into the ffi tests for testing. This CL does not yet migrate off `allocate` and `free` in the SDK. That is done in a dependent CL. Issue: #44621 Issue: #38721 TEST=tests/ffi/allocator_test.dart TEST=tests/ffi/calloc_test.dart TEST=tests/ffi/vmspecific_static_checks_test.dart Change-Id: I173e213a750b8b3f594bb8d4fc72575f2b6b91f7 Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,analyzer-nnbd-linux-release-try,front-end-linux-release-x64-try,front-end-nnbd-linux-release-x64-try,benchmark-linux-try,dart-sdk-linux-try,pkg-linux-release-try,vm-ffi-android-release-arm-try,vm-ffi-android-release-arm64-try,vm-kernel-nnbd-win-debug-x64-try,vm-kernel-win-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177705 Reviewed-by: Clement Skau <[email protected]>
New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705 Issue: #44621 Issue: #38721 Change-Id: Id45274313edbb3842438b66b9c0917a86884c8ed Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178993 Reviewed-by: Aske Simon Christensen <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
This CL does not yet roll `package:ffi` to use `Allocator`, because that breaks the checked in Dart in Flutter in g3. Instead, this uses the copy of `_CallocAllocator` from `package:ffi` in `calloc.dart` in tests/ffi. New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705 Issue: #44621 Issue: #38721 Change-Id: Iedfc4a11d4606915a324c824372bca643016f5a3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178994 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
This CL does not yet roll `package:ffi` to use `Allocator`, because that breaks the checked in Dart in Flutter in g3. Instead, this copies `_CallocAllocator` from `package:ffi` into the benchmarks. (We need a copy per benchmark such that file-copying before running benchmarks works properly.) The copies can be deleted when we can update `package:ffi` in the DEPS file to contain `_CallocAllocator`. New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705 Issue: #44621 Issue: #38721 Change-Id: I546de7ec65ceb6f05644a5f269b83f64656892e5 Cq-Include-Trybots: luci.dart.try:benchmark-linux-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178995 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
This CL does not yet roll `package:ffi` to use `Allocator`, because that breaks the checked in Dart in Flutter in g3. Instead, this uses the copy of `_CallocAllocator` from `package:ffi` in `calloc.dart`. New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705 Issue: #44621 Issue: #38721 Change-Id: I10e53a363cd87c4c11e7042989e6957b82ae2a09 Cq-Include-Trybots: luci.dart.try:vm-kernel-win-debug-x64-try,vm-kernel-linux-debug-x64-try,vm-kernel-mac-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178992 Reviewed-by: Aske Simon Christensen <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
This CL does not yet roll `package:ffi` to use `Allocator`, because that breaks the checked in Dart in Flutter in g3. Instead, this copies `_CallocAllocator` from `package:ffi` into the samples. New API landed in: https://dart-review.googlesource.com/c/sdk/+/177705 Issue: #44621 Issue: #38721 Change-Id: I83da349c2e52d7f079aa1569b4726318fee24c9d Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177706 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
This can only be landed when `Allocator` and `Opaque` have rolled into Flutter/engine, that into Flutter/flutter, and that into g3. flutter/flutter/commit/a706cd211240f27be3b61f06d70f958c7a4156fe Deletes all the copies of `_CallocAllocator` and uses the one from `package:ffi` instead. Issue: #44622 Issue: #43974 Issue: #44621 Issue: #38721 Change-Id: I50b3b4c31a2b839b35e3e057bd54f463b90bc55e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/179540 Reviewed-by: Aske Simon Christensen <[email protected]>
This CL changes the semantics of `Pointer<T extends Struct>.ref` to use the compile-time `T` rather than the runtime `T`. This enables tree shaking of subtypes of Struct and optimizing `.ref`. Issue: #38721 TEST=tests/ffi/vmspecific_static_checks_test.dart TEST=tests/ffi/*struct*test_.dart Change-Id: I3f5b08c08ec0799ef8aab3c4177e2ac70d25501c Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,analyzer-nnbd-linux-release-try,front-end-linux-release-x64-try,front-end-nnbd-linux-release-x64-try,benchmark-linux-try,dart-sdk-linux-try,pkg-linux-release-try,vm-ffi-android-release-arm-try,vm-ffi-android-release-arm64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/177862 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
The analyzer and CFE now report a warning on calls to `Pointer<T extends Struct>.ref` and `Pointer<T extends Struct>.[]` where `T` is a generic. Adapted from https://dart-review.googlesource.com/c/sdk/+/180190 to only deprecate but not error out on generic calls to `.ref` and `[]`. Issue: #38721 TEST=tests/ffi/vmspecific_static_checks_test.dart Change-Id: I81353089d59f093730d63792e9dbcd0b2ff0c432 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180365 Reviewed-by: Vyacheslav Egorov <[email protected]>
The analyzer and CFE now report a warning on calls to `Pointer<T extends NativeType>.elementAt()` and `sizeOf<T extends NativeType>()` where `T` is a generic. Adapted from https://dart-review.googlesource.com/c/sdk/+/178200 to only deprecate but not error out on generic calls. Does not roll forward `package:ffi` to a version with the `Allocator` but keeps the generic `sizeOf` invocation. This causes extra warnings in the pkg/front_end testcases. Issue: #38721 TEST=tests/ffi/data_test.dart TEST=tests/ffi/sizeof_test.dart TEST=tests/ffi/structs_test.dart TEST=tests/ffi/vmspecific_static_checks_test.dart Change-Id: I8f41c4dc04fc44e7e6c540ba87a3f41604130fe9 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180560 Reviewed-by: Vyacheslav Egorov <[email protected]>
Landing the breaking change (#44621) is part of the milestone, not this. Removing milestone. |
This rewrites `sizeOf` calls in the CFE to skip the runtime entry when the type argument is constant. The runtime entry is still used when the type argument is generic. Forcing the type argument to be constant and removing the runtime entry will be done in follow up CLs. Bug: #44621 Bug: #38721 TEST=tests/ffi/sizeof_test.dart Change-Id: I17d14432e6ab22810729be6b5c2939a033d382c5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182262 Reviewed-by: Clement Skau <[email protected]>
This rewrites `elementAt` calls in the CFE to skip the runtime entry for `sizeOf` when the type argument is constant. The runtime entry is still used when the type argument is generic. Forcing the type argument to be constant and removing the runtime entry will be done in follow up CLs. Bug: #44621 Bug: #38721 TEST=tests/ffi/data_test.dart Change-Id: I480db43e7c115c24bd45f0ddab0cfea7eb8cfa58 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182263 Reviewed-by: Clement Skau <[email protected]>
This rewrites `Allocator.call` calls in the CFE to skip the runtime entry for `sizeOf` when the type argument is constant. The runtime entry is still used when the type argument is generic. Forcing the type argument to be constant and removing the runtime entry will be done in follow up CLs. Bug: #44621 Bug: #38721 TEST=test/ffi (almost all of them) Change-Id: I5e855fa2b63a5c1b7fa70dbaa1b89c122a82da6e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182264 Reviewed-by: Clement Skau <[email protected]>
This rewrites `Pointer<Struct>.ref` and `Pointer<Struct>[]` calls in the CFE to skip the runtime entry when the type argument is constant. The runtime entry is still used when the type argument is generic. Forcing the type argument to be constant and removing the runtime entry will be done in follow up CLs. Bug: #38721 Bug: #44621 Removing the runtime entry speeds up `.ref` significantly. before: FfiStruct.FieldLoadStore(RunTime): 18868.140186915887 us. after: FfiStruct.FieldLoadStore(RunTime): 270.5877976190476 us. Measurements from Linux x64 in JIT mode. Closes: #38648 TEST=tests/ffi/structs_test.dart Change-Id: I82abd930b5a9c5c78a8999c2bc49802d67d37534 Cq-Include-Trybots: luci.dart.try:analyzer-linux-release-try,analyzer-nnbd-linux-release-try,app-kernel-linux-debug-x64-try,dart-sdk-linux-try,front-end-nnbd-linux-release-x64-try,pkg-linux-debug-try,vm-kernel-linux-debug-x64-try,vm-kernel-nnbd-linux-debug-x64-try,vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-reload-linux-debug-x64-try,vm-kernel-reload-rollback-linux-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182265 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Clement Skau <[email protected]>
… type" This CL changes the semantics of `Pointer<T extends Struct>.ref` to use the compile-time `T` rather than the runtime `T`. This enables tree shaking of subtypes of Struct and optimizing `.ref`. Bug: #38721 TEST=tests/ffi/vmspecific_static_checks_test.dart TEST=tests/ffi/*struct*test_.dart Change-Id: Ie19bc3259d1cb721d0ce56d68e82d09dc3a4ad0e Cq-Include-Trybots: luci.dart.try:analyzer-linux-release-try,analyzer-nnbd-linux-release-try,app-kernel-linux-debug-x64-try,dart-sdk-linux-try,front-end-nnbd-linux-release-x64-try,pkg-linux-debug-try,vm-kernel-linux-debug-x64-try,vm-kernel-nnbd-linux-debug-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-kernel-reload-rollback-linux-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180190 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
After https://dart-review.googlesource.com/c/sdk/+/180190 the runtime entry has become dead code. This CL keeps the runtime entry itself but makes it unreachable as was the suggestion on previous a CL removing runtime entries: https://dart-review.googlesource.com/c/sdk/+/169406 Bug: #38648 Bug: #38721 TEST=tests/ffi/vmspecific_static_checks_test.dart TEST=tests/ffi/*struct*test_.dart Change-Id: I84c5c925215b9dbd999826fb390df91d8050e1dd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/182627 Reviewed-by: Aske Simon Christensen <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
This can only be landed when `Allocator`, `Opaque`, and `AllocatorAlloc` have rolled into Flutter/engine, that into Flutter/flutter, and into g3. Deletes all the copies of `_CallocAllocator` and uses the one from `package:ffi` instead. Bug: #44622 Bug: #43974 Bug: #44621 Bug: #38721 Change-Id: I486034b379b5a63cad4aefd503ccd0f2ce5dd45e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/180188 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Aske Simon Christensen <[email protected]>
This CL changes the semantics of `Pointer<T extends NativeType>.elementAt` and `sizeOf<T extends NativeType>` to use the compile-time `T` rather than the runtime `T`. Issue: #38721 TEST=tests/ffi/data_test.dart TEST=tests/ffi/sizeof_test.dart TEST=tests/ffi/structs_test.dart TEST=tests/ffi/vmspecific_static_checks_test.dart Change-Id: Ifb25a4bd66d50a385d3db6dec9213b96dff21722 Cq-Include-Trybots: luci.dart.try:vm-precomp-ffi-qemu-linux-release-arm-try,analyzer-analysis-server-linux-try,analyzer-linux-release-try,analyzer-nnbd-linux-release-try,front-end-linux-release-x64-try,front-end-nnbd-linux-release-x64-try,benchmark-linux-try,dart-sdk-linux-try,pkg-linux-release-try,vm-ffi-android-release-arm-try,vm-ffi-android-release-arm64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/178200 Reviewed-by: Aske Simon Christensen <[email protected]>
After changing `elementAt` and `sizeOf` to only accept static types (https://dart-review.googlesource.com/c/sdk/+/178200) and rewrite all the calls with static types in the CFE (https://dart-review.googlesource.com/c/sdk/+/182262), the runtime entry can now be removed. One less place where the runtime relies on `Struct` subtypes not being tree shaken. Issue: #38721 Because we're no longer using `NativeType`s in a RTE to calculate their size, these do no longer need to be available in the precompiled runtime. Closes: #42809 TEST=tests/ffi(_2)/* Change-Id: I8682a3bb4d2dc3ee71531cf71909e47489e96f12 Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-debug-x64-try,vm-kernel-precomp-nnbd-linux-debug-x64-try,vm-precomp-ffi-qemu-linux-release-arm-try,vm-ffi-android-debug-arm64-try,vm-ffi-android-debug-arm-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/185085 Commit-Queue: Daco Harkes <[email protected]> Reviewed-by: Clement Skau <[email protected]>
Removes the entry-point on the `#sizeOf` field of `Struct` subtypes, because we are no longer using it in the runtime. Does not remove the entry-point from the constructor yet, because FFI trampolines can instantiate these objects. Updated the TODOs to reflect this. Bug: #38721 TEST=tests/ffi(_2)/* in precompiled mode. Change-Id: If3889e782b8fe34ef1c34cf8af83da041b4d2ef5 Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-nnbd-linux-debug-x64-try Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/185540 Reviewed-by: Aske Simon Christensen <[email protected]> Commit-Queue: Daco Harkes <[email protected]>
Update: Everything except struct constructors is shaken out. Struct constructors are still retained, because the FFI trampolines creating structs (in FFI call returns, or FFI callback arguments) is not hooked up to the TFA yet. |
Per discussion in the VM sync meeting this morning this last piece is not critical for the March milestone and hence removing the milestone tag. |
Update 2021-03-10: All API changes are done.
Everything except struct constructors is shaken out. Struct constructors are still retained, because the FFI trampolines creating structs (in FFI call returns, or FFI callback arguments) is not hooked up to the TFA.
Update 2021-01-05: We should make the necessary API changes introducing an
Allocator
interface before Flutter 2.0, the actual implementation of tree-shaking can be done later.===============================================================================
The way FFI structs are implemented involves adding entry-point annotations to members of all generated struct classes.
This prevents any FFI structs from being tree-shaken. We should fix this.
The text was updated successfully, but these errors were encountered: