
C#
est un langage incroyablement flexible. Sur celui-ci, vous pouvez écrire non seulement le backend ou les applications de bureau. J'utilise C#
pour travailler avec des données scientifiques, qui imposent certaines exigences aux outils disponibles dans le langage. Bien que netcore
l'agenda (étant donné qu'après netstandard2.0
plupart des fonctionnalités des deux langues et du runtime ne sont pas netframework
sur netframework
), je continue de travailler avec les projets hérités.
Dans cet article, je considère une application non évidente (mais probablement souhaitée?) De Span<T>
et la différence entre l'implémentation de Span<T>
dans netframework
et netcore
raison des spécificités de clr
.
Clause de non-responsabilité 1Les extraits de code de cet article ne sont en aucun cas destinés à être utilisés dans des projets du monde réel.
La solution proposée au problème (farfelu?) Est plutôt une preuve de concept.
Dans tous les cas, en implémentant cela dans votre projet, vous le faites à vos risques et périls.
Clause de non-responsabilité 2Je suis absolument sûr que quelque part, dans certains cas, cela va certainement tirer sur quelqu'un dans le genou.
Le contournement de sécurité de type en C#
peu susceptible de conduire Ă quelque chose de bon.
Pour des raisons évidentes, je n'ai pas testé ce code dans toutes les situations possibles, cependant, les résultats préliminaires semblent prometteurs.
Pourquoi ai-je besoin de Span<T>
?
Spen vous permet de travailler avec des tableaux de types unmanaged
sous une forme plus pratique, réduisant le nombre d'allocations nécessaires. Malgré le fait que la prise en charge de l'intervalle dans le netframework
BCL
netframework
presque totalement absente, plusieurs outils peuvent ĂŞtre obtenus Ă l'aide de System.Memory
, System.Buffers
et System.Runtime.CompilerServices.Unsafe
.
L'utilisation des travées dans mon projet hérité est limitée, cependant, je les ai trouvées une utilisation non évidente, tout en crachant sur la sécurité des types.
Quelle est cette application? Dans mon projet, je travaille avec des données obtenues à partir d'un outil scientifique. Ce sont des images, qui, en général, sont un tableau de T[]
, oĂą T
est l'un des types primitifs unmanaged
, par exemple Int32
(alias int
). Pour sérialiser correctement ces images sur le disque, je dois prendre en charge le format hérité incroyablement gênant, qui a été proposé en 1981 , et a depuis peu changé. Le principal problème de ce format est qu'il est BigEndian . Ainsi, pour écrire (ou lire) un tableau non compressé de T[]
, vous devez changer l'endianess de chaque élément. La tâche triviale.
Quelles sont les solutions évidentes?
- Nous parcourons le tableau
T[]
, appelons BitConverter.GetBytes(T)
, développons ces quelques octets, copions dans le tableau cible. - Nous parcourons le tableau
T[]
, effectuons des fraudes de la forme new byte[] {(byte)((x & 0xFF00) >> 8), (byte)(x & 0x00FF)};
(devrait fonctionner sur les types à deux octets), écrivez dans le tableau cible. - * Mais
T[]
un tableau? Les éléments sont alignés, non? Vous pouvez donc aller jusqu'au bout, par exemple, Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int));
. La méthode copie le tableau dans le tableau en ignorant la vérification de type. Il suffit seulement de ne pas manquer les limites et l'allocation. Nous mélangons les octets en conséquence. - * Ils disent que
C#
est (C++)++
. Par conséquent, activez /unsafe
, fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p;
et maintenant vous pouvez parcourir la représentation en octets du tableau source, modifier l'endianess à la volée et écrire des blocs sur le disque (en ajoutant stackalloc byte[]
ou ArrayPool<byte>.Shared
pour le tampon intermédiaire) sans allouer de mémoire pour un tout nouveau tableau d'octets.
Il semblerait que le point 4 vous permette de résoudre tous les problèmes, mais l'utilisation explicite d'un contexte unsafe
et le travail avec des pointeurs sont en quelque sorte complètement différents. Puis Span<T>
vient Ă notre aide.
Span<T>
Span<T>
devrait techniquement fournir des outils pour travailler avec des tracés de mémoire presque comme travailler avec des pointeurs, tout en éliminant la nécessité de «réparer» la matrice en mémoire. Un tel pointeur compatible GC
avec des limites de tableau. Tout va bien et en sécurité.
Une chose mais - malgré la richesse de System.Runtime.CompilerServices.Unsafe
, Span<T>
cloué sur le type T
Étant donné que le spen est essentiellement un pointeur de longueur 1 +, que se passe-t-il si vous retirez votre pointeur, le convertissez en un autre type, recalculez la longueur et créez un nouveau span? Heureusement, nous avons public Span<T>(void* pointer, int length)
.
Écrivons un test simple:
[Test] public void Test() { void Flip(Span<byte> span) {} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]); }
Des développeurs plus avancés que je ne devrais immédiatement réaliser ce qui ne va pas ici. Le test échouera-t-il? La réponse, comme cela arrive généralement, dépend .
Dans ce cas, cela dépend principalement de l'exécution. Sur netcore
test devrait fonctionner, mais sur netframework
, comment ça se netframework
.
Fait intéressant, si vous supprimez certains des essais, le test commence à fonctionner correctement dans 100% des cas.
Faisons les choses correctement.
1 J'avais tort .
Bonne réponse: dépend
Pourquoi le résultat dépend-il ?
Supprimons tous les inutiles et écrivons ici un tel code:
private static void Main() => Check(); private static void Check() { Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y); }
La méthode Write<T, U>
accepte une plage de type T
, lit l'adresse du premier élément et lit à travers ce pointeur un élément de type U
En d'autres termes, Write<int, int>(ref x)
affichera l'adresse en mémoire + le nombre 999.
L' Write
normale imprime un tableau.
Maintenant sur la méthode As<,>
:
private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); }
C#
syntaxe C#
prend désormais en charge cet enregistrement à état fixed
en appelant implicitement la méthode Span<T>.GetPinnableReference()
.
Exécutez cette méthode sur netframework4.8
en mode x64
. Nous regardons ce qui se passe:
LEGACY [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0] 0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t 0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC 0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
Initialement, les deux travées (malgré un type différent) se comportent de manière identique, et la Span<byte>
, en substance, représente une vue octet du tableau d'origine. Ce dont vous avez besoin.
D'accord, essayons de décaler le début de la plage à la taille d'un IntPtr
(ou 2 X int
sur x64
) et lisons. Nous obtenons le troisième élément du tableau et la bonne adresse. Et puis nous ramasserons les ordures ...
GC.Collect(0, GCCollectionMode.Forced, true, true);
Le dernier indicateur de cette méthode demande au GC
compacter le tas. Après avoir appelé GC.Collect
GC
déplace la baie locale d'origine. Span<int>
reflète ces changements, mais notre Span<byte>
continue de pointer vers l'ancienne adresse, où maintenant il n'est pas clair quoi. Une excellente façon de vous tirer tous les genoux à la fois!
Examinons maintenant le résultat du même fragment de code appelé sur netcore3.0.100-preview8
.
CORE [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t 0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC 0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ] [ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ]
Tout fonctionne, et cela fonctionne de manière stable , pour autant que je puisse voir. Après compactage, les deux espagne changent de pointeur. Super! Mais comment le faire fonctionner maintenant dans un projet hérité?
Jit intrinsèque
J'ai absolument oublié que la prise en charge des portées est implémentée dans netcore
via intrinsik . En d'autres termes, netcore
peut créer des pointeurs internes même vers un fragment de tableau et mettre à jour correctement les liens lorsque le GC
déplace. Dans le netframework
, l'implémentation par nuget
d'une travée est une béquille. En fait, nous avons deux spen différents: l'un est créé à partir du tableau et suit ses liens, le second à partir du pointeur et n'a aucune idée de ce vers quoi il pointe. Après avoir déplacé le tableau d'origine, le pointeur span continue de pointer vers l'endroit où le pointeur est passé dans son constructeur pointé. À titre de comparaison, netcore
un exemple d' implémentation de span dans netcore
:
readonly ref struct Span<T> where T : unmanaged { private readonly ByReference<T> _pointer;
et en netframework
:
readonly ref struct Span<T> where T : unmanaged { private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length; }
_pinnable
contient une référence au tableau, si elle a été transmise au constructeur, _byteOffset
contient un décalage (même l'étendue dans le tableau a un décalage non nul lié à la façon dont le tableau est représenté en mémoire, probablement ). Si vous passez le pointeur void*
au constructeur, il est simplement converti en _byteOffset
absolu. Span sera étroitement fixé à la zone mémoire, et toutes les méthodes d'instance regorgent de conditions comme if(_pinnable is null) {/* */} else {/* _pinnable */}
. Que faire dans une telle situation?
Comment le faire n'en vaut pas la peine, mais je l'ai quand mĂŞme fait
Cette section est consacrée aux différentes implémentations prises en charge par netframework
, qui permettent de netframework
Span<T> -> Span<U>
, en conservant tous les liens nécessaires.
Je vous préviens: il s'agit d'une zone de programmation anormale avec éventuellement des erreurs fondamentales et un comportement indéfini à la fin
Méthode 1: naïve
Comme l'exemple l'a montré, la conversion des pointeurs ne donnera pas le résultat souhaité sur le netframework
. Nous avons besoin de la valeur _pinnable
. D'accord, nous découvrirons le reflet en retirant les champs privés (très mauvais et pas toujours possible), nous l'écrirons dans un nouveau spen, nous serons heureux. Il n'y a qu'un petit problème: spen est une ref struct
, elle ne peut être ni un argument générique, ni être compressée dans un object
. Les méthodes de réflexion standard nécessiteront, d'une manière ou d'une autre, de pousser la portée dans le type de référence. Je n'ai pas trouvé de moyen simple (même en considérant la réflexion sur les domaines privés).
Méthode 2: nous devons approfondir
Tout a déjà été fait avant moi ( [1] , [2] , [3] ). Spen est une structure, quel que soit T
trois champs occupent la même quantité de mémoire ( sur la même architecture ). Et si [FieldOffset(0)]
? AussitĂ´t dit, aussitĂ´t fait.
[StructLayout(LayoutKind.Explicit)] ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; }
Mais lorsque vous démarrez le programme (ou plutôt, lorsque vous essayez d'utiliser un type), une TypeLoadException
rencontre - un générique ne peut pas être LayoutKind.Explicit
. D'accord, cela n'a pas d'importance, allons sur le chemin difficile:
[StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan; }
Vous pouvez maintenant faire ceci:
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan; }
La méthode fonctionne avec un seul problème - le champ _length
copié tel _length
, donc lors de la _length
-> byte
la plage d'octets est 4 fois plus petite que le tableau réel.
Pas de problème:
[StructLayout(LayoutKind.Sequential)] public ref struct Raw { public object Pinnable; public IntPtr Pointer; public int Length; } [StructLayout(LayoutKind.Explicit)] public ref struct Exchange { [FieldOffset(0)] public Raw RawView; }
Maintenant, grâce à RawView
vous pouvez accéder à chaque champ de portée individuel.
private static Span<byte> As2(Span<int> span) { var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan; }
Et cela fonctionne comme il se doit , si vous ignorez l'utilisation de trucs sales. Moins - la version générique du convertisseur ne peut pas être créée, vous devez vous contenter de types prédéfinis.
Méthode 3: fou
Comme tout programmeur normal, j'aime automatiser les choses. La nécessité d'écrire des convertisseurs pour n'importe quelle paire de types unmanaged
ne m'a pas plu. Quelle solution peut-on proposer? C'est vrai, demandez au CLR
d'écrire du code pour vous .
Comment y parvenir? Il y a différentes manières, il y a des articles . En bref, le processus ressemble à ceci:
Créer un générateur de construction -> créer un générateur de module -> créer un type -> {Champs, méthodes, etc.} -> à la sortie, nous obtenons une instance de Type
.
Pour comprendre exactement Ă quoi le type devrait ressembler (c'est une ref struct
), nous utilisons n'importe quel outil comme ildasm
. Dans mon cas, c'était dotPeek .
La création d'un générateur de type ressemble à ceci:
var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout
Maintenant les champs. Comme nous ne pouvons pas copier directement Span<T>
dans Span<U>
raison de la différence de longueur, nous devons créer deux types de chaque distribution
[StructLayout(LayoutKind.Explicit)] ref struct Generated_Int32 { [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw; }
Ici Raw
nous pouvons déclarer avec nos mains et réutiliser. N'oubliez pas IsByRefLikeAttribute
. Avec les champs, tout est simple:
var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private); spanField.SetOffset(0); var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private); rawField.SetOffset(0);
C'est tout, le type le plus simple est prêt. Cachez maintenant le module d'assemblage. Les types personnalisés sont mis en cache, par exemple, dans le dictionnaire ( T -> Generated_{nameof(T)}
). Nous créons un wrapper qui, selon les deux types TIn
et TOut
génère deux types d'aides et effectue les opérations nécessaires sur les travées. Il y en a un mais. Comme dans le cas de la réflexion, il est presque impossible de l'utiliser sur des travées (ou sur d'autres ref struct
). Ou je n'ai pas trouvé de solution simple . Comment être?
Délégués à la rescousse
Les méthodes de réflexion ressemblent généralement à ceci:
object Invoke(this MethodInfo mi, object @this, object[] otherArgs)
Ils ne portent pas d'informations sur les types, donc si la boxe (= emballage) vous convient, il n'y a aucun problème.
Dans notre cas, @this
et otherArgs
doivent contenir une ref struct
, que je n'ai pas pu contourner.
Cependant, il existe un moyen plus simple. Imaginons qu'un type possède des méthodes getter et setter (pas des propriétés, mais des méthodes simples créées manuellement).
Par exemple:
void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span;
En plus de la méthode, nous pouvons déclarer un type délégué (explicitement dans le code):
delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged;
Nous devons le faire car l'action standard devrait avoir une signature Action<Span<T>>
, mais les séquences ne peuvent pas être utilisées comme arguments génériques. SpanSetterDelegate
, cependant, est un délégué absolument valide.
Créez les délégués nécessaires. Pour ce faire, effectuez des manipulations standard:
var mi = type.GetMethod("Method_Name");
Maintenant spanSetter
peut être utilisé comme, par exemple, spanSetter(Span<T>.Empty);
. Quant Ă @this
2 , il s'agit d'une instance de notre type dynamique, créée, bien sûr, via Activator.CreateInstance(type)
, car la structure a un constructeur par défaut sans arguments.
Donc, la dernière frontière - nous devons générer dynamiquement des méthodes.
2 Vous pouvez remarquer que quelque chose ne va pas ici - Activator.CreateInstance()
compresse une instance de ref struct
. Voir la fin de la section suivante.
Rencontrez Reflection.Emit
Je pense que les méthodes pourraient être générées en utilisant Expression
, comme les corps de nos getters / setters triviaux se composent littéralement de quelques expressions. J'ai choisi une approche différente et plus directe.
Si vous regardez le code IL d'un getter trivial, vous pouvez voir quelque chose comme ( Debug
, X86
, netframework4.8
)
nop ldarg.0 ldfld /* - */ stloc.0 br.s /* */ ldloc.0 ret
Il y a des tonnes d'endroits pour s'arrêter et déboguer.
Dans la version finale, il ne reste que le plus important:
ldarg.0 ldfld /* - */ ret
L'argument null de la méthode d'instance est ... this
. Ainsi, ce qui suit est écrit en IL :
1) Téléchargez this
2) Charger la valeur du champ
3) Ramenez-le
Juste hein? Reflection.Emit
a une surcharge spéciale qui prend, en plus du code op, également un paramètre de descripteur de champ. Tout comme nous l'avons reçu précédemment, par exemple spanField
.
var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); gen.Emit(OpCodes.Ldfld, spanField); gen.Emit(OpCodes.Ret);
Pour le setter, c'est un peu plus compliqué, vous devez le charger sur la pile, charger le premier argument de la fonction, puis appeler l'instruction d'écriture dans le champ et ne rien retourner:
ldarg.0 ldarg.1 stfld ret
Après avoir effectué cette procédure pour le champ Raw
, en déclarant les délégués nécessaires (ou en utilisant les délégués standard), nous obtenons un type dynamique et quatre méthodes d'accesseur, à partir desquelles les délégués génériques corrects sont générés.
Nous écrivons une classe wrapper qui, à l'aide de deux paramètres génériques ( TIn
, TOut
), reçoit des instances de type Type
qui référencent les types dynamiques correspondants (mis en cache), après quoi elle crée un objet de chaque type et génère quatre délégués génériques, à savoir
void SetSpan(Span<TIn> span)
pour écrire le span source dans la structureRaw GetRaw()
pour lire le contenu d'un span comme une structure Raw
void SetRaw(Raw raw)
pour écrire la structure Raw
modifiée dans le deuxième objetSpan<TOut> GetSpan()
pour renvoyer la plage du type souhaité avec des champs correctement définis et recalculés.
Fait intéressant, les instances de type dynamique doivent être créées une fois. Lors de la création d'un délégué, une référence à ces objets est transmise en tant que paramètre @this
. Voici une violation des règles. Activator.CreateInstance
renvoie un object
. Apparemment, cela est dû au fait que le type dynamique lui-même ne s'est pas avéré ref
type.IsByRef
( type.IsByRef
Like == false
), mais il a été possible de créer des champs de type ref
. Apparemment, une telle restriction est présente dans la langue, mais le CLR
digère. C'est peut-être ici que les genoux seront abattus en cas d'utilisation non standard. 3
Ainsi, nous obtenons une instance d'un type générique qui contient quatre délégués et deux références implicites aux instances de classes dynamiques. Les délégués et les structures peuvent être réutilisés lors de l'exécution des mêmes castes d'affilée. Pour améliorer les performances, nous mettons à nouveau en cache (déjà un convertisseur de type) une paire (TIn, TOut) -> Generator<TIn, TOut>
.
Le trait est le dernier: nous donnons des types, Span<TIn> -> Span<TOut>
public Span<TOut> Cast(Span<TIn> span) {
Conclusion
Parfois - pour des raisons sportives - vous pouvez contourner certaines limitations du langage et implémenter des fonctionnalités non standard. Bien sûr, à vos risques et périls. Il convient de noter que la méthode dynamique vous permet d'abandonner complètement les pointeurs et unsafe / fixed
contextes unsafe / fixed
, ce qui peut être un bonus. L'inconvénient évident est le besoin de réflexion et de génération de type.
Pour ceux qui ont lu jusqu'au bout.
Résultats de référence naïfsEt à quelle vitesse est-ce tout?
J'ai comparé la vitesse des castes dans un scénario stupide qui ne reflète pas l'utilisation réelle / potentielle de telles castes et étendues, mais donne au moins une idée de la vitesse.
Cast_Explicit
utilise la conversion via un type explicitement déclaré, comme dans la méthode 2 . Chaque caste nécessite l'attribution de deux petites structures et des accès aux champs;Cast_IL
implémente la méthode 3 , mais à chaque fois crée une instance à nouveau Generator<TIn, TOut>
, ce qui conduit à des recherches constantes dans les dictionnaires, après la première passe génère tous les types;Cast_IL_Cached
Generator<TIn, TOut>
, - , .. ;Buffer
, , . .
— int[N]
N/2
.
, , . , . , , . , unmanaged
.
BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362 Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1
PS
3 , ref
, , . ( ) . ref
structs,
static Raw Generated_Int32.GetRaw(Span<int> span) { var inst = new Generated_Int32() { Span = span }; return inst.Raw; }
, Reflection.Emit
. , ILGenerator.DeclareLocal
.
static Span<int> Generated_Int32.GetSpan(Raw raw);
delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged; delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged;
, , ref
— . Parce que ,
var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>;
—
Raw raw = getter(Span<TIn>.Empty); Raw newRaw = convert(raw); Span<TOut> = setter(newRaw);
UPD01: