-
Notifications
You must be signed in to change notification settings - Fork 166
Add sections talking about Swift SIMD types and interop with them #313
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
Changes from 2 commits
63b15bd
bc35ee7
922900c
5e8da38
ebd1bc8
8775dfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -118,7 +118,13 @@ When calling a function that returns an opaque struct, the Swift ABI always requ | |||||
|
||||||
At the lowest level of the calling convention, we do not consider Library Evolution to be a different calling convention than the Swift calling convention. Library Evolution requires that some types are passed by a pointer/reference, but it does not fundamentally change the calling convention. Effectively, Library Evolution forces the least optimizable choice to be taken at every possible point. As a result, we should not handle Library Evolution as a separate calling convention and instead we can manually handle it at the projection layer. | ||||||
|
||||||
For frozen structs and enums, Swift has a complicated lowering process where the struct or enum type's layout are recursively flattened to a sequence of primitives. If this sequence is length 4 or less, the values of this type are split into the elements of this sequence for parameter passing instead of passing the struct as a whole. Structs and enums that cannot be broken down in this way are passed by-reference to their specified frozen layout. Due to high implementation cost in the RyuJIT, in particular in the `UnmanagedCallersOnly` scenario, we should implement this first pass of lowering in the projection layer; the only types allowed for `CallConvSwift` calling convention in method or function pointer signatures are primitives, our special Swift register types, and pointer types. For reference, this lowering pass is done in the Swift compiler when lowering from Swift IL to LLVM IR. This design decision reinforces our direction of having the Runtime layer of Swift interop support similar features as the LLVM IR representation of Swift. | ||||||
For frozen structs and enums, Swift has a complicated lowering process where the struct or enum type's layout are recursively flattened to a sequence of primitives. If this sequence is length 4 or less, the values of this type are split into the elements of this sequence for parameter passing instead of passing the struct as a whole. Structs and enums that cannot be broken down in this way are passed by-reference to their specified frozen layout. Due to high implementation cost in the RyuJIT, in particular in the `UnmanagedCallersOnly` scenario, we should implement this first pass of lowering in the projection layer; the only types allowed for `CallConvSwift` calling convention in method or function pointer signatures are primitives, our special Swift register types, and pointer types. We will allow struct types as the return type in a signature to support scenarios where a return value is passed in multiple registers instead of passed indirectly; this occurs in some scenarios such as some larger-than-register SIMD types. For reference, this lowering pass is done in the Swift compiler when lowering from Swift IL to LLVM IR. This design decision reinforces our direction of having the Runtime layer of Swift interop support similar features as the LLVM IR representation of Swift. | ||||||
|
||||||
##### SIMD Types | ||||||
|
||||||
We will pass the `System.Runtime.Intrinsics.VectorX<T>` types in SIMD registers as we do with the managed calling convention. We will treat the `Vector2/3/4` types as non-SIMD types (and block their usage directly as parameters in the `CallConvSwift` signature as is the case with other structs). | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't think the managed convention does this on all platforms (today). Do we allow interop with these types in other interop scenarios? It sounds like it is going to add dotnet/runtime#8300 + dotnet/runtime#9578 as part of the work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we have support on ARM64 due to HFA/HVA support. If I'm wrong, then yes this would add in those two issues as part of this work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, on ARM64 we support it, but not on x64. Is the SIMD interop important enough to warrant implementing it for x64? Those two issues on their own are large work items. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like macOS x64 is still going to be widely supported when .NET 9 releases, so it depends on if the libraries we want to support are high enough priority. For example, the Accelerate framework has many APIs that take the SIMD types. @kotlarmilos what are the Apple libraries that we're targeting for .NET 9? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would
This should exist for Unix already as well and only be missing for Windows x64, since that doesn't pass vectors differently (only returns them differently). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My biggest concern here is that ABI is an extremely complex space and interop is one of those spaces where users want both simplicity and reduced overhead, especially when generating larger binding libraries. Apple has also notably broken ABI in the past or deviated conventions from the norm on new platforms and so it is entirely possible some new platform comes on and now every single bit of I think it is ultimately much better (even if its not what is done for the initial release due to timing constraints or w/e) that we have this support in the runtime as a detail of the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These calls are typically going to have try/catch block in them to convert the .NET exception into switft error. You would have to implement inlining of methods with exception handling to make this work... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is to say, a user should be able to export Swift bindings to C using official Apple tooling and then use another existing tool, such as ClangSharp, CppAst, etc; which can generate blittable P/Invoke bindings from a C header and expect it to work. If we can't achieve that, I expect we will have a lot of downstream pain/headaches from the community, especially as it gets into more complex bindings and libraries. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Don't tempt me :-) (Note that this is actually part of our .NET 9 plan, and I also think it would be much more likely we end up with this support than appetite for improving the UCO Swift case in the future.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re handling tuples: I think we can still handle tuples at the projection layer since the splitting of a tuple into separate arguments is done at the SIL layer and is very straightforward (it doesn't have nearly the same complexity as the "primitive type sequence" lowering) especially if JIT implementation cost for tuples would be too much. Additionally, the primitive type sequence lowering happens after the tuple lowering (so each tuple element can be lowered to a sequence of up to 4 primitives), so handling tuples in the projection layer doesn't interfere with the primitive sequence handling. |
||||||
|
||||||
CoreCLR and NativeAOT currently block the `VectorX<T>` types from P/Invokes as this behavior is currently not well-supported by RyuJIT. Depending on implementation cost and the number of APIs we wish to support, we may want to block the `VectorX<T>` types from `CallConvSwift` initially until we can implement the correct behavior. We can always add support for these types later. | ||||||
|
||||||
##### Automatic Reference Counting and Lifetime Management | ||||||
|
||||||
|
@@ -150,6 +156,15 @@ We plan to interop with Swift's Library Evolution mode, which brings an addition | |||||
|
||||||
If possible, Swift tuples should be represented as `ValueTuple`s in .NET. If this is not possible, then they should be represented as types with a `Deconstruct` method similar to `ValueTuple` to allow a tuple-like experience in C#. | ||||||
|
||||||
##### SIMD types | ||||||
|
||||||
Swift has its own built-in SIMD types; however they're named based on the number of elements, not based on the width of the vector type. For example, Swift has `SIMD2<T>`, `SIMD4<T>`, up to `SIMD64<T>`. When the instantiations of these types correspond to an intrinsic vector type, they are treated as that type. Otherwise, they are treated as a struct of vectors. In .NET, our vector types are named based on their vector with, so `Vector128<T>`, `Vector256<T>`, etc. | ||||||
|
||||||
For instantiated generic types that are within the size of an processor intrinsic vector type, there exists a correspondence between a Swift SIMD type and a .NET SIMD type. For example, `SIMD4<Int32>` corresponds to `Vector128<Int32>`. | ||||||
However, this correspondence breaks down for SIMD types larger than the largest vector register width (i.e. larger than 512 bytes) or for unconstrained generic types like `SIMD4<T>`. These cases; however, should be quite rare. In the "too-large" case, the values are passed into Swift as though the type is a struct of vectors. In the case of unconstrained generic types, the SIMD values are passed indirectly. Both of these cases are suboptimal and we don't know of any public Swift APIs that fall into either of these scenarios. | ||||||
|
||||||
We recommend that the projection tooling will map each Swift SIMD instantiation to the corresponding `VectorX<T>` type in .NET. For cases where there is no corresponding type or where an API takes or returns an unconstrained generic `SIMDX<T>` value, we can map the APIs to regular projected structs for the SIMD types based on the above rules. | ||||||
|
||||||
#### Projection Tooling Components | ||||||
|
||||||
The projection tooling should be split into these components: | ||||||
|
Uh oh!
There was an error while loading. Please reload this page.