Pourquoi est-TypedReference dans les coulisses? Il est si rapide et sûr ... presque magique!

StackOverflow https://stackoverflow.com/questions/4764573

  •  16-10-2019
  •  | 
  •  

Question

Avertissement: Cette question est un peu hérétique ... programmeurs religieux respectueux toujours par les bonnes pratiques, s'il vous plaît ne le lisez pas. :)

Quelqu'un sait pourquoi l'utilisation de TypedReference est si découragé (implicitement, par manque de documentation)?

Je l'ai trouvé de grandes utilisations pour elle, comme lors du passage des paramètres génériques grâce à des fonctions qui ne doit pas être générique (lors de l'utilisation d'un object pourrait être surpuissant ou lent, si vous avez besoin d'un type de valeur), lorsque vous avez besoin d'un pointeur opaque, ou lorsque vous avez besoin d'accéder à un élément d'un tableau rapide, dont les spécifications vous trouvez lors de l'exécution (en utilisant Array.InternalGetReference). Étant donné que le CLR ne permet même pas l'utilisation incorrecte de ce type, pourquoi est-il découragé? Il ne semble pas être dangereux ou quoi que ce soit ...


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

"Spécialiste" génériques en C # (ce qui est de 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); }
}

L'écriture de code qui fonctionne avec des pointeurs génériques (ce qui est très dangereux si mal utilisé, mais rapide et sûre si elle est utilisée 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);
}

L'écriture d'un méthode version de l'instruction sizeof, 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)); }
    }
}

L'écriture d'une méthode qui passe un paramètre « état » 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 les utilisations comme celui-ci « découragées » (par manque de documentation)? Toutes les raisons particulières de sécurité? Il semble parfaitement sûr et vérifiable si elle est pas mélangé avec des pointeurs (qui ne sont pas de toute façon sécuritaire ou vérifiable) ...


Mise à jour:

Exemple de code pour montrer que, en effet, TypedReference peut ê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: Je modifié l'indice de référence ci-dessus, depuis la dernière version du message utilisé une version de débogage du code [j'ai oublié de changer pour libérer] et mettre aucune pression sur la GC Cette version est un peu plus. réaliste, et sur mon système, il est plus de trois fois plus rapide avec TypedReference en moyenne.)

Était-ce utile?

La solution

Réponse courte:. portabilité

Alors que __arglist, __makeref et __refvalue sont extensions de langage et sont sans papiers dans le langage C # Spécification, les constructions utilisées pour les mettre en œuvre sous le capot (convention d'appel vararg, type TypedReference, arglist, refanytype, mkanyref et instructions refanyval) sont parfaitement documentées dans le CLI Spécification (ECMA-335 ) dans le bibliothèque vararg .

étant défini dans la bibliothèque vararg rend tout à fait clair qu'ils sont principalement destinés à soutenir des listes d'arguments de longueur variable et pas grand-chose. listes variable arguments ont peu d'utilité dans les plates-formes qui ne nécessitent pas d'interface avec un code externe C utilisant VarArgs. Pour cette raison, la bibliothèque VarArgs ne fait pas partie d'un profil de CLI. implémentations CLI légitimes peuvent choisir de ne pas soutenir la bibliothèque VarArgs car il est pas inclus dans le profil CLI noyau:

  

4.1.6 vararg

     

fonction vararg ensemble supports listes d'arguments de longueur variable et des pointeurs d'exécution typé.

     

Si omis: Toute tentative pour faire référence à une méthode avec la convention d'appel vararg ou les codages de signature associés aux méthodes de vararg (voir la partition II) doit jeter l'exception System.NotImplementedException. Méthodes en suivant les instructions du CIL arglist, refanytype, mkrefany et refanyval jetterons l'exception de System.NotImplementedException. Le moment précis de l'exception n'est pas spécifiée. Le besoin type System.TypedReference pas défini.

Mise à jour (Répondre au commentaire de GetValueDirect):

FieldInfo.GetValueDirect sont FieldInfo.SetValueDirect sont pas de la classe de base Bibliothèque. Notez qu'il ya une différence entre .NET Framework Class Library et la bibliothèque de classes de base. BCL est la seule chose nécessaire pour une mise en œuvre conforme de la CLI / C # et est documenté dans TR ECMA / 84 . (En fait, FieldInfo lui-même fait partie de la bibliothèque de réflexion et n'est pas inclus dans le profil CLI du noyau soit).

Dès que vous utilisez une méthode en dehors de BCL, vous donnez un peu de portabilité (et cela devient de plus en plus importante avec l'avènement de non-.NET CLI mises en œuvre comme Silverlight et MonoTouch). Même si une mise en œuvre voulait augmenter compatiblility avec la bibliothèque Microsoft .NET Framework Class, il pourrait simplement fournir GetValueDirect et SetValueDirect prendre un TypedReference sans le TypedReference spécialement traitées par le moteur d'exécution (essentiellement, ce qui les rend équivalentes à leurs homologues de object sans le bénéfice de la performance ).

Avaient-ils documentés qu'il en C #, il aurait eu au moins une incidence couple:

  1. Comme toute fonctionnalité, il peut devenir un obstacle à de nouvelles fonctionnalités, d'autant plus que celui-ci ne correspond pas vraiment à la conception de C # et nécessite des extensions étranges de syntaxe et la remise spéciale d'un type par la Durée.
  2. Toutes les implémentations de C # doivent mettre en œuvre en quelque sorte cette fonction et il est pas forcément trivial / possible pour les implémentations C # qui ne fonctionne pas sur le dessus d'une CLI du tout ou exécutés sur une CLI sans VarArgs.

Autres conseils

Eh bien, je ne suis pas Eric Lippert, donc je ne peux pas parler directement des motivations de Microsoft, mais si je devais avancer une hypothèse, je dirais que TypedReference et al. ne sont pas bien documentés parce que, franchement, vous ne pas besoin.

Chaque utilisation que vous avez mentionné ces caractéristiques peut être accompli sans eux, mais à une pénalité de performance dans certains cas. Mais C # (et .NET en général) ne sont pas conçus pour être une langue de haute performance. (Je suppose que « plus vite que Java » était l'objectif de performance.)

Cela ne veut pas dire que certaines considérations de performance n'ont pas bénéficié. En effet, des caractéristiques telles que des pointeurs, stackalloc et certaines fonctions de cadre optimisées existent en grande partie à la performance de boost dans certaines situations.

Generics, que je dirais avoir principale bénéfice de la sécurité de type, également améliorer les performances de façon similaire à TypedReference en évitant la boxe et unboxing. En fait, je me demandais pourquoi vous préféreriez ceci:

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

à ceci:

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

Les compromis, comme je les vois, sont que le premier exige moins JIT (et, il suit, moins de mémoire), tandis que le second est plus familier et, je suppose, un peu plus rapide (en évitant le pointeur déréférencement) .

J'appelle TypedReference et les amis détails de mise en œuvre. Vous avez signalé certaines utilisations soignées pour eux, et je pense qu'ils méritent d'être explorées, mais la mise en garde habituelle de compter sur les détails de mise en œuvre s'applique-la prochaine version peut briser votre code.

Je ne peux pas comprendre si le titre de cette question est censé être sarcastique: Il a été établie de longue date TypedReference est lente, ballonnement, cousine laide de pointeurs gérés « vrais », ce dernier étant ce que nous obtenons avec C ++ / CLI interior_ptr<T>, ou même des paramètres traditionnels par référence (ref / out) C # . En fait, il est assez difficile de faire TypedReference même atteindre la performance de base de simplement utiliser un entier réindexer hors du tableau CLR d'origine à chaque fois.

Les détails tristes sont , mais heureusement, rien de tout cela ... maintenant les questions

  

Cette question est maintenant devenue sans objet par le nouveau noreferrer habitants ref et retour ref caractéristiques C # 7

Ces nouvelles fonctionnalités linguistiques offrent de premier plan, le soutien de première classe C # pour déclarer, le partage et la manipulation vrai CLR type de référence géré - types dans des situations soigneusement prescibed.

Les restrictions d'utilisation sont pas plus strictes que ce qui était auparavant nécessaire pour TypedReference (et la performance est littéralement sauter du pire au meilleur ), donc je ne vois pas en restant cas d'utilisation envisageable dans C # pour TypedReference. Par exemple, auparavant il n'y avait pas moyen de maintenir un TypedReference dans le tas de GC, de sorte que le même être vrai des pointeurs gérés supérieurs est maintenant pas à emporter.

Et évidemment, la disparition de TypedReference ou sa dévalorisation presque complète au moins-means jeter __makeref sur le junkheap ainsi.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top