Repository navigation
Developers using reflection invoke should be able to use ref struct #45152
Description
Activity
- addedtenet-performancePerformance related issuePerformance related issue
on Nov 24, 2020 - addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Nov 24, 2020 - removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Nov 24, 2020 This isn't just for JSON, what we've we doing should work for any framework that uses reflection today I assume. I'm hoping to see a deep analysis here of the scenarios that Would use these APIs
PowerShell could be benefit from this - it is based on reflection, emit and dynamic.
Reacted by Patrick Meinecke and Cole TobinAs mentioned in some of the previous issues, and to add a note on the requirements as there is a reference to Reflection.Emit that might make this not clear: the solution should not involve any managed objects from
System.Reflectionbut go through onlyRuntimeXXXHandlethat can work directly withldtoken&co.We have discussed that these APIs should be efficient and avoid allocating lots of managed memory: #28001 (comment) . Avoiding any managed objects from
System.Reflectionis much more stringent and unlikely to be practical.Reacted by Alexandre MutelAvoiding any managed objects from
System.Reflectionis much more stringent and unlikely to be practical.Fair enough. I don't know the cases that would involve reflection, but as long as simple cases like accessing statically known e.g fields directly from IL via ldtoken, are working without, that's ok.
How much work is this? Is there any way to make incremental progress in 6.0 towards it?
I'm writing up something on the code generation techniques used in ASP.NET Core and for the future reference I'd like an idea of how our APIs would need to evolve. The closest I've found is @jkotas 's comment here #10057 (comment).
We may be able to make some incremental progress on faster reflection in .NET by refactoring the internal implementation.
We won't be able to expose the new faster reflection APIs for .NET 6. For that, we need #26186 that depends on https://github.com/dotnet/csharplang/blob/master/proposals/low-level-struct-improvements.md that was cut for .NET 6.
Got it. For .NET 7 then. I think I understand how to model the APIs we need to take advantage of this. Unfortunately this and async are at odds, but we can probably optimize the sync path with this and box in the async path. Still that would mean we need a way to create a TypedReference from this boxed value in a way that won't explode because the new API won't handle type coercion i.e a mix of this API needs to support a mix of boxed and unboxed values somehow. I can provide examples if needed.
Bumping again on the topic, more precisely for the support of
offsetofand so by extensionhandleof(A.MyField)that we discussed here), can we add here that it might require a language + Roslyn support, so that we can build higher primitives/intrinsics around it? (e.g likeUnsafe.OffsetOf(fieldToken)).A scenario example for
offsetofis when building for example an animation system that can animate dynamic (resolution based on data, not code) field values of a struct that is stored in an unmanaged memory buffer.cc: @jaredpar (for the question about Language + Roslyn support)
Reacted by Dennis FehrI'm not sure how the language can implement
offsetofwithout some underlying runtime support as it's a value you can't determine until runtime. As for that andhandleofI think it comes down to if there is enough benefit in adding it, particularly when it's just a think wrapper over .NET API calls.72 remaining items
Just tested and calling the following code is working with .NET 7 JIT, but I haven't checked the specs in a while for such operation.
This is invalid IL. If you run it on checked JIT, you will see all sorts of asserts. The first one is:
Assertion failed 'genActualType(op1->TypeGet()) != TYP_REF && genActualType(op2->TypeGet()) != TYP_REF : Possibly bad IL with CEE_add at offset 0002h (op1=ref op2=long stkDepth=0)' in 'Test:AddByteOffset[ubyte](System.Object,long):byref' during 'Importation' (IL size 4; hash 0xcdd16edf; Tier0)It is likely that that the JIT can either crash or produce bad code when this gets method inlined into a callsite of a particular shape.
Reacted by Alexandre Mutel and Hamish ArblasterThis is invalid IL. If you run it on checked JIT, you will see all sorts of asserts. The first one is:
Fair enough. We definitely don't want invalid IL 😅
So, exposing
RuntimeHelpers.GetRawDatawould be the way and it would require to shift the offset with the method table in the UnsafeAccessor code in my PR, or could we provide aref byte Unsafe.AsByteRef(object) { ldarg.0; ret; }?I'm ok with
RuntimeHelpers.GetRawDataif it is the preferred way.My opinion is that the following set of APIs makes the most sense:
namespace System.Runtime.CompilerServices { public static class RuntimeHelpers { public static ref byte GetRawData(object? o); public static object? GetObject(ref byte reference, out nuint offset); } }
This provides the forward and reverse API to convert between byrefs and objects.
The lack of
<T>reduces the number of generic instantiations we will get (if that still matters on some platforms? I can't recall).Note
object?: since we will need to check null anyway, I think it makes sense to check for null and give a null byref forGetRawData, instead of throwing, since it should be able to emit better code for that, and forGetObjectit makes sense sincereferencecould point to unmanaged/stack memory (in which caseoffsetwould equal the pointer).When the documentation is written for these pair of APIs, we will need to consider their behaviour with strings and arrays - currently they would probably return a ref to the start of the length field - if this is how we want it to work (which probably makes sense), then we should document the offset from the length field to the first entry in these cases so people can use it correctly for these OR we could document that it's undefined to use these APIs and then try to determine/select what specific index it's at (this would allow us to change them to be 64 bit length in the future if needed), which could make sense since there are other APIs for working with these special cases in the "intended" way when indexing is desired.
Reacted by Alexandre Mutel and EnderlookLinking Roslyn issue dotnet/roslyn#68000 where you can't get an offset of a
reffield.We now have the PR at #93946 which adds
UnsafeAccessorKind.FieldOffset(API issue pending).But we need a champion to create the API issue for the
RuntimeHelpers.GetRawData()and.GetObject()proposal above so we can move the discussion there.Reacted by Alexandre Mutel and Hamish ArblasterBut we need a champion to create the API issue for the
RuntimeHelpers.GetRawData()I simply reopened #28001 since it already has a bunch of discussion and it's exactly that proposal. I cleared the milestone and marked untriaged.
Reacted by Alexandre Mutel and Steve HarterIt'd be very nice if the
System.TypedReferencemodernization can happen for 10.0, all the pieces should be in place. For the time being I'm using my own version.readonly ref struct TypedReference { readonly ref byte _value; public Type Type { get; } TypedReference(ref byte target, Type type) { _value = ref target; Type = type; } public ref T UnsafeAsRef<T>() => ref Unsafe.As<byte, T>(ref _value); public ref T AsRef<T>() { if (typeof(T) != Type) ThrowInvalid(); return ref Unsafe.As<byte, T>(ref _value); } static void ThrowInvalid() => throw new InvalidCastException("Invalid type given."); public static TypedReference Create<T>(ref T value) => new(ref Unsafe.As<T, byte>(ref value), typeof(T)); }
Within controlled and easily verifiable areas I have also used an UnsafeReference variant. This is essentially just a
ref byteref struct with some validation baked in for debug (though that won't really help if the BCL would ship that to consumers).Maybe somebody finds this useful:
#if DEBUG readonly ref struct UnsafeReference { readonly ref byte _value; readonly Type _type; UnsafeReference(ref byte target, Type type) { _value = ref target; _type = type; } public ref T UnsafeAsRef<T>() => ref Unsafe.As<byte, T>(ref _value); public ref T AsRef<T>() { Debug.Assert(typeof(T) == _type); return ref Unsafe.As<byte, T>(ref _value); } public static implicit operator UnsafeReference(TypedReference typedReference) => new(ref typedReference.UnsafeAsRef<byte>(), typedReference.Type); public static UnsafeReference Create<T>(ref T value) => new(ref Unsafe.As<T, byte>(ref value), typeof(T)); } #else readonly ref struct UnsafeReference { readonly ref byte _value; UnsafeReference(ref byte target) { _value = ref target; } public ref T UnsafeAsRef<T>() => ref Unsafe.As<byte, T>(ref _value); public ref T AsRef<T>() => ref UnsafeAsRef<T>(); public static implicit operator UnsafeReference(TypedReference typedReference) => new(ref typedReference.UnsafeAsRef<byte>()); public static UnsafeReference Create<T>(ref T value) => new(ref Unsafe.As<T, byte>(ref value)); } #endif
It'd be very nice if the System.TypedReference modernization can happen for 10.0, all the pieces should be in place.
The pieces are not all in place yet.
TypedReferenceis effectively an opaque storage for arefvalue of any type. That includesref structand our lifetime model does not support that yet. There is a proposal that will close this gap enough that we could correctly modelTypedReferencein the type system. It would requireunsafewhen unpacking an instance and getting a value out byrefbut otherwise would be safe.It'd be very nice if the System.TypedReference modernization can happen for 10.0, a
That seems unlikely at this point. The language work is mostly understood but this is tied to runtime changes. At the moment I don't think the combined runtime + language work will fit into the .NET 10 schedule. Think 11 is more likely.
It would require unsafe when unpacking an instance and getting a value out by ref but otherwise would be safe.
This would be a really unfortunate outcome. As a user of a type like this I would much prefer to have two operations to confer it is specifically the ref struct types that need user diligence to be used safely. Concretely I would prefer it to be one operation for unsafe extraction - with
T : allow ref struct- and another safe one without, doing the safe fallible runtime cast that it resembles. Allowing it to be used 100% safely for most types avoids unnecessary unsafe proliferation in the ecosystem.
I previously left a good reason for wanting this type to be usable in safe context in dotnet/roslyn#65255 (comment)
It's a fantastic mechanism for avoiding generic specialization bloat of internal code. Where there is a sizable non-generic spine of code acting on a generic value that - if not for wanting to keep structs unboxed - would just take a callback and object? state (or more commonly won't be modeled to allow for structs at all, opting to refer to some abstract base class instead).The bloat incurred in those cases is a fundamental problem for using structs in 'open world' api designs. Passing TypedReferences through the internals instead can make structs viable in *all* dimensions for high perf code, only requiring a cheap indirection to isolate the generic call from the non-generic code. These advantages of structs won't diminish with better PGO or more on stack allocation of classes and so on. Structs guarantee most of their advantages in overhead and predictability, for NativeAOT, crossing abstractions, in tier 0 and so on, which is a huge advantage in itself.
The pieces are not all in place yet. TypedReference is effectively an opaque storage for a ref value of any type. That includes ref struct and our lifetime model does not support that yet. There is a proposal that will close this gap enough that we could correctly model TypedReference in the type system
var span = Span<byte>.Empty; var typedReference = __makeref(span); // error CS1601
The above doesn't work today, including in an unsafe context, so it's currently not usable for any type anyway. It would be fantastic to remove that limitation, but that same reasoning holds for Spans, of either form (though safely only ReadOnlySpan applies as I understand it). All could benefit, but there is already lots of value in supporting the current safe set of types (Span's baseline usefulness being much higher of course).
The modernized version of TypedReference should allow for the future addition of
T : allow ref structon the relevant apis, but that being a supported scenario - and an evident future need - I still can't see what isn't in place to do any modernization today. I fully understand that doing the design and changes in two phases could require more investment than the type warrants but that's not clear to me at this point, so please bear with me.It would require unsafe when unpacking an instance and getting a value out by ref but otherwise would be safe.
This would be a really unfortunate outcome ... Concretely I would prefer it to be one operation for unsafe extraction - with T : allow ref struct - and another safe one without, doing the safe fallible runtime cast that it resembles.
Sorry, let me clarify. Extracting a value by
ref Tfrom aTypedReferencewould be unsafe ifTis potentially aref struct. Otherwise, it would be safe. The rational forref structcase being unsafe is that safely representing that is not possible without adding explicit lifetimes to the language where as today C# works primarily by inferring lifetimes.The above doesn't work today, including in an unsafe context, so it's currently not usable for any type anyway.
That is by design. We didn't want to allow
ref structto be usable with__makerefuntil we had a plan on how to handle the lifetimes. If we adopt the proposal I listed then we would remove this restriction.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsFuture
Support
ref structand "fast invoke" by adding new reflection APIs. The new APIs will be faster than the currentobject[]boxing approach by leveraging the existingSystem.TypedReference<T>.TypedReferenceis a special type and is super-fast because since it is aref structwith its own opcodes. By extending it with this feature, it provides alloc-free, stack-based “boxing” with support for all argument types (reference types, value types, pointers and ref structs [pending]) along with all modifiers (byval, in, out, ref, ref return). Currently reflection does not support passing or invoking aref structsince it can’t be boxed toobject; the new APIs are to supportref structwith new language features currently being investigated.Example syntax (actual TBD):
Dependencies
The Roslyn and runtime dependencies below are required for the programming model above. These are listed in the order in which they need to be implemented.
reffield support in .NET runtimes #63768ref structas a generic argument. This would be used when adding the static factory methodpublic static TypedReference CreateFromRefStruct<T>(ref T myRefStruct) where T : ref structwhich basically wraps__makeref(myrefstruct). It could also be used to enableSpan<T> where T : ref structwhich is the ideal stack-based collection implementation that can containTypedReferences.params. The new invoke APIs require a collection ofTypeReferences. There are several possible implementations; the most normalized solution would be supportingSpan<T> where T : ref structthus enabling anyref struct(not justTypedReference) to be used in a container. UPDATE: done in prototype per [API Proposal]: byref parameter collection for invoke #75349TypedReference. (Roslyn link TBD). Ideally,TypedReferenceis a normalref struct. Note thatTypedReferencecurrently hasByReference<byte>to contain an interior pointer to the value so this will likely need a different type. Also,TypedReferencehas several compile-time limitations including not be able to be passed to another method that should be removed resulting in only standardref structsemantics.reffields.Motivation
Reflection is ~20x slower than a Delegate call for a typical method. Many users including our own libraries use IL Emit instead which is non-trivial and error-prone. The expected gains are ~10x faster with no allocs; verified with a prototype. Internally, IL Emit is used but with a proposed slow-path fallback for AOT (non-emit) cases. The existing reflection invoke APIs may also layer on this.
In Scope
APIs to invoke methods using
TypedReferenceincluding passing aTypedReferencecollection.TypedReferencemust be treated as a normalref struct(today it has nuances and special cases).Support ref struct (passing and invoking).
Performance on par with existing ref emit scenarios:
To scope this feature, the minimum functionality that results in a win by allowing
System.Text.Jsonto remove its dependency to System.Reflection.Emit for inbox scenarios.Out of Scope
This issue is an incremental improvement of reflection by adding new Invoke APIs and leveraging the existing
TypedReferencewhile requiring some runtime\Roslyn changes. Longer-term we should consider a more holistic runtime and Roslin support for reflection including JIT intrinsics and\or new "dynamic invoke" opcodes for performance along with perhaps C# auto-stack-boxing to\from aref TypedReference.Implementation
A design doc is forthcoming.
The implementation will likely cache the generated method on the corresponding
MethodBaseandMemberInfoobjects.100% backwards compat with the existing object[]-based Invoke APIs is not necessary but will be designed with laying in mind (e.g. parameter validation, special types like ReflectionPointer, the
Binderpattern, CultureInfo for culture-aware methods) so that in theory the existing object[]-based Invoke APIs could layer on this new work.This issue supersedes other reflection performance issues that overlap: