Pourquoi TypedReference se trouve-t-il dans les coulisses? C'est tellement rapide et sûr… presque magique!

128

Attention: cette question est un peu hérétique ... les programmeurs religieux respectant toujours les bonnes pratiques, ne la lisez pas. :)

Est-ce que quelqu'un sait pourquoi l'utilisation de TypedReference est si déconseillée (implicitement, par manque de documentation)?

J'en ai trouvé de bonnes utilisations, par exemple lors du passage de paramètres génériques via des fonctions qui ne devraient pas être génériques (lors de l'utilisation d'un objectpeut être excessif ou lent, si vous avez besoin d'un type valeur), lorsque vous avez besoin d'un pointeur opaque, ou pour quand vous avez besoin d'accéder rapidement à un élément d'un tableau, dont vous trouvez les spécifications au moment de l'exécution (en utilisant Array.InternalGetReference). Puisque le CLR n'autorise même pas une utilisation incorrecte de ce type, pourquoi est-il déconseillé? Cela ne semble pas dangereux ou quoi que ce soit ...


D'autres utilisations que j'ai trouvées pour TypedReference:

"Spécialisation" des génériques en C # (c'est du type sécurisé):

static void foo<T>(ref T value)
{
    //This is the ONLY way to treat value as int, without boxing/unboxing objects
    if (value is int)
    { __refvalue(__makeref(value), int) = 1; }
    else { value = default(T); }
}

Écrire du code qui fonctionne avec des pointeurs génériques (c'est très dangereux en cas d'utilisation abusive, mais rapide et sûr s'il est utilisé correctement):

//This bypasses the restriction that you can't have a pointer to T,
//letting you write very high-performance generic code.
//It's dangerous if you don't know what you're doing, but very worth if you do.
static T Read<T>(IntPtr address)
{
    var obj = default(T);
    var tr = __makeref(obj);

    //This is equivalent to shooting yourself in the foot
    //but it's the only high-perf solution in some cases
    //it sets the first field of the TypedReference (which is a pointer)
    //to the address you give it, then it dereferences the value.
    //Better be 10000% sure that your type T is unmanaged/blittable...
    unsafe { *(IntPtr*)(&tr) = address; }

    return __refvalue(tr, T);
}

Ecrire une version méthode de l' sizeofinstruction, qui peut être parfois utile:

static class ArrayOfTwoElements<T> { static readonly Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

Ecriture d'une méthode qui passe un paramètre "state" qui veut éviter la boxe:

static void call(Action<int, TypedReference> action, TypedReference state)
{
    //Note: I could've said "object" instead of "TypedReference",
    //but if I had, then the user would've had to box any value types
    try
    {
        action(0, state);
    }
    finally { /*Do any cleanup needed*/ }
}

Alors pourquoi des utilisations comme celle-ci sont-elles «découragées» (faute de documentation)? Des raisons de sécurité particulières? Cela semble parfaitement sûr et vérifiable s'il n'est pas mélangé avec des pointeurs (qui ne sont pas sûrs ou vérifiables de toute façon) ...


Mettre à jour:

Exemple de code pour montrer que, en effet, TypedReferencepeut être deux fois plus rapide (ou plus):

using System;
using System.Collections.Generic;
static class Program
{
    static void Set1<T>(T[] a, int i, int v)
    { __refvalue(__makeref(a[i]), int) = v; }

    static void Set2<T>(T[] a, int i, int v)
    { a[i] = (T)(object)v; }

    static void Main(string[] args)
    {
        var root = new List<object>();
        var rand = new Random();
        for (int i = 0; i < 1024; i++)
        { root.Add(new byte[rand.Next(1024 * 64)]); }
        //The above code is to put just a bit of pressure on the GC

        var arr = new int[5];
        int start;
        const int COUNT = 40000000;

        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set1(arr, 0, i); }
        Console.WriteLine("Using TypedReference:  {0} ticks",
                          Environment.TickCount - start);
        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set2(arr, 0, i); }
        Console.WriteLine("Using boxing/unboxing: {0} ticks",
                          Environment.TickCount - start);

        //Output Using TypedReference:  156 ticks
        //Output Using boxing/unboxing: 484 ticks
    }
}

(Edit: j'ai édité le benchmark ci-dessus, puisque la dernière version du post utilisait une version de débogage du code [j'ai oublié de le changer en release], et je n'ai mis aucune pression sur le GC. Cette version est un peu plus réaliste, et sur mon système, c'est plus de trois fois plus rapide avec TypedReferenceen moyenne.)

user541686
la source
Lorsque je lance votre exemple, j'obtiens des résultats complètement différents. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Peu importe ce que j'essaye (y compris différentes façons de faire le chronométrage), la boxe / déballage est toujours plus rapide sur mon système.
Seph
1
@Seph: Je viens de voir votre commentaire. C'est très intéressant - cela semble être plus rapide sur x64, mais plus lent sur x86. Weird ...
user541686
1
Je viens de tester ce code de référence sur ma machine x64 sous .NET 4.5. J'ai remplacé Environment.TickCount par Diagnostics.Stopwatch et je suis allé avec ms au lieu de ticks. J'ai exécuté chaque build (x86, 64, Any) trois fois. Le meilleur des trois résultats était comme suit: x86: 205 / 27ms (même résultat pour 2/3 exécutions sur cette version) x64: 218 / 109ms Quelconque: 205/27ms (même résultat pour 2/3 exécutions sur cette version) -all- cas la boîte / unboxing était plus rapide.
kornman00
2
Les mesures de vitesse étranges pourraient être attribuées à ces deux faits: * (T) (objet) v ne fait PAS réellement d'allocation de tas. Dans .NET 4+, il est optimisé. Il n'y a pas d'allocations sur ce chemin, et c'est sacrément rapide. * L'utilisation de makeref nécessite que la variable soit réellement allouée sur la pile (alors que la méthode kinda-box pourrait l'optimiser dans les registres). De plus, en regardant les horaires, je suppose que cela nuit à l'inlining même avec l'indicateur force-inline. Donc, kinda-box est intégré et enregistré, tandis que makeref fait un appel de fonction et exploite la pile
hypersw
1
Pour voir les bénéfices du casting typeref, rendez-le moins trivial. Par exemple, convertir un type sous-jacent en type enum ( int-> DockStyle). Cela boit pour de vrai, et est presque dix fois plus lent.
hypersw

Réponses:

42

Réponse courte: portabilité .

Bien que __arglist, __makerefet __refvaluesont des extensions de langage et sont en situation irrégulière dans le langage C # Spécification, les constructions utilisées pour les mettre en œuvre sous le capot ( varargconvention d' appel, TypedReferencetype arglist, refanytype, mkanyrefet refanyvalinstructions) sont parfaitement documentées dans la spécification CLI (ECMA-335) en la bibliothèque Vararg .

Le fait d'être défini dans la bibliothèque Vararg montre clairement qu'ils sont principalement destinés à prendre en charge les listes d'arguments de longueur variable et pas grand-chose d'autre. Les listes d'arguments variables ont peu d'utilité dans les plates-formes qui n'ont pas besoin de s'interfacer avec du code C externe utilisant des varargs. Pour cette raison, la bibliothèque Varargs ne fait partie d'aucun profil CLI. Les implémentations CLI légitimes peuvent choisir de ne pas prendre en charge la bibliothèque Varargs car elle n'est pas incluse dans le profil du noyau CLI:

4.1.6 Vararg

L' ensemble de fonctionnalités vararg prend en charge les listes d'arguments de longueur variable et les pointeurs typés à l'exécution.

En cas d'omission: toute tentative de référencer une méthode avec la varargconvention d'appel ou les codages de signature associés aux méthodes vararg (voir Partition II) doit lever l' System.NotImplementedExceptionexception. Les méthodes utilisant les instructions du CIL arglist, refanytype, mkrefanyet refanyvaljetterons l' System.NotImplementedExceptionexception. Le moment précis de l'exception n'est pas spécifié. Le type System.TypedReferencen'a pas besoin d'être défini.

Mise à jour (réponse au GetValueDirectcommentaire):

FieldInfo.GetValueDirectne FieldInfo.SetValueDirectfont pas partie de la bibliothèque de classes de base. Notez qu'il existe une différence entre la bibliothèque de classes .NET Framework et la bibliothèque de classes de base. BCL est la seule chose requise pour une implémentation conforme de la CLI / C # et est documentée dans ECMA TR / 84 . (En fait, FieldInfolui-même fait partie de la bibliothèque Reflection et n'est pas non plus inclus dans le profil du noyau CLI).

Dès que vous utilisez une méthode en dehors de BCL, vous renoncez un peu à la portabilité (et cela devient de plus en plus important avec l'avènement des implémentations CLI non.NET comme Silverlight et MonoTouch). Même si une implémentation voulait augmenter la compatibilité avec la bibliothèque de classes Microsoft .NET Framework, elle pourrait simplement fournir GetValueDirectet SetValueDirectprendre un TypedReferencesans faire le TypedReferencespécialement géré par le runtime (en gros, ce qui les rend équivalents à leurs objecthomologues sans avantage en termes de performances).

S'ils l'avaient documenté en C #, cela aurait eu au moins quelques implications:

  1. Comme toute fonctionnalité, elle peut devenir un obstacle à de nouvelles fonctionnalités, d'autant plus que celle-ci ne rentre pas vraiment dans la conception de C # et nécessite des extensions de syntaxe étranges et une remise spéciale d'un type par le runtime.
  2. Toutes les implémentations de C # doivent en quelque sorte implémenter cette fonctionnalité et ce n'est pas nécessairement trivial / possible pour les implémentations C # qui ne s'exécutent pas du tout au-dessus d'une CLI ou ne s'exécutent pas sur une CLI sans Varargs.
Mehrdad Afshari
la source
4
Bons arguments pour la portabilité, +1. Mais qu'en est-il FieldInfo.GetValueDirectet FieldInfo.SetValueDirect? Ils font partie de la BCL et pour les utiliser, vous en avez besoin TypedReference , alors cela ne force-t-il pas fondamentalement TypedReferenceà toujours être définis, quelle que soit la spécification du langage? (Aussi, une autre note: même si les mots-clés n'existaient pas, tant que les instructions existaient, vous pouviez toujours y accéder en émettant dynamiquement des méthodes ... donc tant que votre plate-forme interagit avec les bibliothèques C, vous pouvez les utiliser, si C # a ou non les mots-clés.)
user541686
Oh, et un autre problème: même si ce n'est pas portable, pourquoi n'ont-ils pas documenté les mots-clés? À tout le moins, c'est nécessaire lors de l'interopérabilité avec les varargs C, alors au moins ils auraient pu le mentionner?
user541686
@Mehrdad: Huh, c'est intéressant. Je suppose que j'ai toujours supposé que les fichiers dans le dossier BCL de la source .NET faisaient partie de la BCL, sans jamais vraiment prêter attention à la partie normalisation ECMA. C'est assez convaincant ... sauf une petite chose: n'est-il pas un peu inutile d'inclure même la fonctionnalité (facultative) dans la spécification CLI, s'il n'y a pas de documentation sur la façon de l'utiliser n'importe où? (Cela aurait du sens si TypedReferenceétait documenté juste pour un langage - disons, géré C ++ - mais si aucun langage ne le documente et donc si personne ne peut vraiment l'utiliser, alors pourquoi même s'embêter à définir la fonctionnalité?)
user541686
@Mehrdad Je soupçonne que la motivation principale était le besoin de cette fonctionnalité en interne pour l'interopérabilité ( par exemple [DllImport("...")] void Foo(__arglist); ) et ils l'ont implémentée en C # pour leur propre usage. La conception de l'interface de ligne de commande est influencée par de nombreux langages (les annotations «The Common Language Infrastructure Annotated Standard» démontrent ce fait.) Être un environnement d'exécution approprié pour autant de langages que possible, y compris les imprévus, a certainement été un objectif de conception (d'où le name) et c'est une fonctionnalité dont, par exemple, une implémentation hypothétique de C géré pourrait probablement bénéficier.
Mehrdad Afshari
@Mehrdad: Ah ... ouais, c'est une raison assez convaincante. Merci!
user541686
15

Eh bien, je ne suis pas Eric Lippert, donc je ne peux pas parler directement des motivations de Microsoft, mais si je devais oser une supposition, je dirais que TypedReferenceet al. ne sont pas bien documentés car, franchement, vous n'en avez pas besoin.

Chaque utilisation que vous avez mentionnée pour ces fonctionnalités peut être accomplie sans elles, bien que dans certains cas, les performances soient pénalisées. Mais C # (et .NET en général) n'est pas conçu pour être un langage hautes performances. (Je suppose que "plus rapide que Java" était l'objectif de performances.)

Cela ne veut pas dire que certaines considérations de performances n'ont pas été prises en compte. En effet, des fonctionnalités telles que les pointeurs stackallocet certaines fonctions de framework optimisées existent en grande partie pour améliorer les performances dans certaines situations.

Les génériques, qui, je dirais, ont le principal avantage de la sécurité des types, améliorent également les performances de la même manière TypedReferencequ'en évitant la boxe et le déballage. En fait, je me demandais pourquoi vous préféreriez ceci:

static void call(Action<int, TypedReference> action, TypedReference state){
    action(0, state);
}

pour ça:

static void call<T>(Action<int, T> action, T state){
    action(0, state);
}

Les compromis, comme je les vois, sont que le premier nécessite moins de JIT (et, il s'ensuit, moins de mémoire), tandis que le second est plus familier et, je suppose, légèrement plus rapide (en évitant le déréférencement du pointeur).

J'appellerais TypedReferenceet les détails d'implémentation d'amis. Vous avez souligné certaines utilisations intéressantes pour eux, et je pense qu'ils valent la peine d'être explorés, mais la mise en garde habituelle de se fier aux détails d'implémentation s'applique - la prochaine version peut casser votre code.

Papa
la source
4
Huh ... "vous n'en avez pas besoin" - J'aurais dû voir ça venir. :-) C'est vrai mais ce n'est pas vrai non plus. Que définissez-vous comme «besoin»? Les méthodes d'extension sont-elles vraiment "nécessaires", par exemple? En ce qui concerne votre question sur l'utilisation des génériques dans call(): C'est parce que le code n'est pas toujours aussi cohérent - je faisais davantage référence à un exemple plus semblable à celui de IAsyncResult.State, où l'introduction de génériques ne serait tout simplement pas faisable car tout à coup, cela introduirait des génériques pour chaque classe / méthode impliquée. +1 pour la réponse, cependant ... surtout pour avoir souligné la partie "plus rapide que Java". :]
user541686
1
Oh, et un autre point: TypedReferencene subira probablement pas de changements de rupture de sitôt, étant donné que FieldInfo.SetValueDirect , qui est public et probablement utilisé par certains développeurs, en dépend. :)
user541686
Ah, mais vous ne avez besoin des méthodes d'extension pour le soutien LINQ. Quoi qu'il en soit, je ne parle pas vraiment d'une différence agréable à avoir / besoin d'avoir. Je n'appellerais TypedReferenceni l' un ni l'autre. (La syntaxe atroce et la lourdeur globale le disqualifient, dans mon esprit, de la catégorie agréable à avoir.) Je dirais que c'est juste une bonne chose à avoir lorsque vous avez vraiment besoin de couper quelques microsecondes ici et là. Cela dit, je pense à quelques endroits de mon propre code que je vais examiner tout de suite, pour voir si je peux les optimiser en utilisant les techniques que vous avez indiquées.
P Daddy
1
@Merhdad: Je travaillais à l'époque sur un sérialiseur / désérialiseur d'objets binaires pour les communications interprocessus / interhost (TCP et pipes). Mes objectifs étaient de le rendre aussi petit (en termes d'octets envoyés sur le fil) et rapide (en termes de temps passé à sérialiser et désérialiser) que possible. Je pensais que je pourrais éviter de boxer et de déballer avec TypedReferences, mais IIRC, le seul endroit où j'ai pu éviter la boxe quelque part était avec les éléments de tableaux unidimensionnels de primitives. Le léger avantage de vitesse ici ne valait pas la complexité qu'il ajoutait à l'ensemble du projet, alors je l'ai retiré.
P Daddy
1
Étant donné delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);qu'une collection de type Tpourrait fournir une méthode ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), mais le JITter devrait créer une version différente de la méthode pour chaque type de valeur TParam. L'utilisation d'une référence typée permettrait à une version JITted de la méthode de fonctionner avec tous les types de paramètres.
supercat le
4

Je ne peux pas comprendre si le titre de cette question est censé être sarcastique: il est établi depuis longtemps que TypedReferencec'est le cousin lent, gonflé et laid des `` vrais '' pointeurs gérés, ce dernier étant ce que nous obtenons avec C ++ / CLI interior_ptr<T> , ou même les paramètres par référence ( ref/ out) traditionnels en C # . En fait, il est assez difficile d' TypedReferenceatteindre les performances de base en utilisant simplement un entier pour réindexer à chaque fois le tableau CLR d'origine.

Les tristes détails sont , mais heureusement, rien de tout cela n'a d'importance maintenant ...

Cette question est maintenant rendue sans objet par les nouveaux locaux de référence et les fonctionnalités de retour de référence en C # 7

Ces nouvelles fonctionnalités de langage fournissent une prise en charge de premier ordre en C # pour la déclaration, le partage et la manipulation de vrais CLR types de types de référence gérés dans des situations soigneusement prédéfinies.

Les restrictions d'utilisation ne sont pas plus strictes que ce qui était auparavant requis TypedReference(et les performances sautent littéralement du pire au meilleur ), donc je ne vois aucun cas d'utilisation imaginable en C # pour TypedReference. Par exemple, auparavant, il n'y avait aucun moyen de conserver un TypedReferencedans le GCtas, donc la même chose est vraie pour les pointeurs gérés supérieurs maintenant n'est pas un retrait.

Et évidemment, la disparition de TypedReference- ou sa dépréciation presque complète au moins - signifie également jeter __makerefà la poubelle.

Glenn Slayden
la source