Ostrzeżenie: To pytanie jest trochę heretyckie ... religijni programiści zawsze przestrzegają dobrych praktyk, proszę go nie czytać. :)
Czy ktoś wie, dlaczego korzystanie z TypedReference jest tak odradzane (domyślnie z powodu braku dokumentacji)?
Znalazłem dla niego świetne zastosowania, na przykład podczas przekazywania parametrów ogólnych przez funkcje, które nie powinny być ogólne (gdy użycie object
może być przesadne lub wolne, jeśli potrzebujesz typu wartości), gdy potrzebujesz nieprzezroczystego wskaźnika lub gdy potrzebujesz szybko uzyskać dostęp do elementu tablicy, którego specyfikacje znajdziesz w czasie wykonywania (używając Array.InternalGetReference
). Skoro CLR nie pozwala nawet na nieprawidłowe użycie tego typu, dlaczego jest odradzane? To nie wydaje się być niebezpieczne ani nic ...
Inne zastosowania, które znalazłem TypedReference
:
„Specjalizacja” typów ogólnych w C # (to jest bezpieczne dla typów):
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); }
}
Pisanie kodu, który działa z ogólnymi wskaźnikami (jest to bardzo niebezpieczne, jeśli zostanie niewłaściwie użyte, ale szybkie i bezpieczne, jeśli zostanie użyte poprawnie):
//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);
}
Pisanie wersji instrukcji metodysizeof
, która może być czasami przydatna:
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)); }
}
}
Pisanie metody, która przekazuje parametr „stan”, który chce uniknąć pakowania:
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*/ }
}
Dlaczego więc takie zastosowania są „odradzane” (z powodu braku dokumentacji)? Jakieś szczególne względy bezpieczeństwa? Wydaje się całkowicie bezpieczne i sprawdzalne, jeśli nie jest pomieszane ze wskaźnikami (które i tak nie są bezpieczne ani weryfikowalne) ...
Aktualizacja:
Przykładowy kod, aby pokazać, że rzeczywiście TypedReference
może być dwukrotnie szybszy (lub więcej):
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
}
}
(Edycja: edytowałem powyższy test porównawczy, ponieważ ostatnia wersja postu używała wersji kodu do debugowania [zapomniałem go zmienić do wydania] i nie naciskałem na GC. Ta wersja jest nieco bardziej realistyczna i w moim systemie jest TypedReference
średnio ponad trzy razy szybszy ).
źródło
TypedReference: 203 ticks
,boxing/unboxing: 31 ticks
. Bez względu na to, co próbuję (w tym różne sposoby mierzenia czasu), pakowanie / rozpakowywanie jest nadal szybsze w moim systemie.int
->DockStyle
). To pudełko naprawdę i jest prawie dziesięciokrotnie wolniejsze.Odpowiedzi:
Krótka odpowiedź: przenośność .
Chociaż
__arglist
,__makeref
i__refvalue
są rozszerzeniami języka i nieudokumentowanych w C # Language Specification, konstrukty stosowane do ich wdrożenia pod maską (vararg
nazywając konwencję,TypedReference
typ,arglist
,refanytype
,mkanyref
, irefanyval
instrukcje) są doskonale udokumentowane w CLI Specification (ECMA-335) w biblioteka Vararg .Zdefiniowanie w Bibliotece Vararg jasno pokazuje, że mają one przede wszystkim wspierać listy argumentów o zmiennej długości i niewiele więcej. Listy zmiennych argumentów mają niewielkie zastosowanie na platformach, które nie muszą łączyć się z zewnętrznym kodem C, który używa varargs. Z tego powodu biblioteka Varargs nie jest częścią żadnego profilu CLI. Legalne implementacje CLI mogą nie obsługiwać biblioteki Varargs, ponieważ nie jest ona uwzględniona w profilu jądra CLI:
Aktualizacja (odpowiedź na
GetValueDirect
komentarz):FieldInfo.GetValueDirect
sąFieldInfo.SetValueDirect
to nie część Base Class Library. Zauważ, że istnieje różnica między biblioteką klas .NET Framework a biblioteką klas podstawowych. BCL jest jedyną rzeczą wymaganą do zgodnej implementacji CLI / C # i jest udokumentowane w ECMA TR / 84 . (W rzeczywistościFieldInfo
jest częścią biblioteki Reflection i nie jest też uwzględniona w profilu jądra CLI).Gdy tylko użyjesz metody poza BCL, rezygnujesz z nieco przenośności (a to staje się coraz ważniejsze wraz z pojawieniem się implementacji innych niż .NET CLI, takich jak Silverlight i MonoTouch). Nawet gdyby implementacja chciała zwiększyć kompatybilność z biblioteką klas Microsoft .NET Framework, mogłaby po prostu dostarczyć
GetValueDirect
iSetValueDirect
wziąć plikTypedReference
bez tworzeniaTypedReference
specjalnie obsługiwanego przez środowisko wykonawcze (zasadniczo czyniąc je równoważnymi z ichobject
odpowiednikami bez korzyści wydajnościowych).Gdyby udokumentowali to w C #, miałoby to co najmniej kilka konsekwencji:
źródło
FieldInfo.GetValueDirect
iFieldInfo.SetValueDirect
? Są częścią BCL i aby ich używać, potrzebujeszTypedReference
, więc czy to w zasadzie nie wymuszaTypedReference
ich definiowania, niezależnie od specyfikacji języka? (Kolejna uwaga: nawet jeśli słowa kluczowe nie istniały, o ile istniały instrukcje, nadal można było uzyskać do nich dostęp za pomocą dynamicznych metod ... tak długo, jak Twoja platforma współpracuje z bibliotekami C, możesz ich używać, czy C # ma słowa kluczowe, czy nie)TypedReference
zostało udokumentowane tylko dla jednego języka - powiedzmy, zarządzanego C ++ - ale jeśli żaden język tego nie dokumentuje, więc jeśli nikt nie może go naprawdę używać, to po co w ogóle zawracać sobie[DllImport("...")] void Foo(__arglist);
) I zaimplementowali ją w C # na własny użytek. Projekt CLI jest pod wpływem wielu języków (adnotacje „Common Language Infrastructure Annotated Standard” demonstrują ten fakt). Stworzenie odpowiedniego środowiska wykonawczego dla jak największej liczby języków, w tym nieprzewidzianych, było zdecydowanie celem projektu (stąd name) i jest to funkcja, na której przykładowo hipotetyczna zarządzana implementacja języka C mogłaby prawdopodobnie skorzystać.Cóż, nie jestem Ericem Lippertem, więc nie mogę bezpośrednio mówić o motywacjach Microsoftu, ale gdybym zaryzykował zgadywanie, powiedziałbym, że
TypedReference
i in. nie są dobrze udokumentowane, ponieważ, szczerze mówiąc, nie potrzebujesz ich.Każde użycie tych funkcji, o którym wspomniałeś, można wykonać bez nich, aczkolwiek w niektórych przypadkach ze spadkiem wydajności. Ale C # (i ogólnie .NET) nie jest zaprojektowany jako język o wysokiej wydajności. (Domyślam się, że celem wydajności było „szybsze niż Java”).
Nie oznacza to, że nie uwzględniono pewnych kwestii dotyczących wydajności. Rzeczywiście, takie funkcje, jak wskaźniki
stackalloc
i pewne zoptymalizowane funkcje struktury istnieją głównie w celu zwiększenia wydajności w określonych sytuacjach.Leki generyczne, które, jak powiedziałbym, mają główną zaletę w postaci bezpieczeństwa typów, również poprawiają wydajność, podobnie jak
TypedReference
dzięki unikaniu pakowania i rozpakowywania. Właściwie zastanawiałem się, dlaczego wolisz to:static void call(Action<int, TypedReference> action, TypedReference state){ action(0, state); }
do tego:
static void call<T>(Action<int, T> action, T state){ action(0, state); }
Jak widzę, kompromisy polegają na tym, że ten pierwszy wymaga mniej JIT (i, co za tym idzie, mniej pamięci), podczas gdy drugi jest bardziej znany i, jak zakładam, nieco szybszy (unikając dereferencji wskaźnika).
Zadzwoniłem
TypedReference
i znajomi o szczegóły realizacji. Wskazałeś dla nich kilka zgrabnych zastosowań i myślę, że warto je zbadać, ale obowiązuje zwykłe zastrzeżenie polegające na poleganiu na szczegółach implementacji - następna wersja może złamać twój kod.źródło
call()
: To dlatego, że kod nie zawsze jest tak spójny - odnosiłem się bardziej do przykładu bardziej podobnego do tegoIAsyncResult.State
, w którym wprowadzenie typów generycznych byłoby po prostu niewykonalne, ponieważ nagle wprowadziłoby to generyczne dla każda klasa / metoda. +1 za odpowiedź ... szczególnie za wskazanie części „szybsza niż Java”. :]TypedReference
prawdopodobnie nie będzie w najbliższym czasie podlegać przełomowym zmianom, biorąc pod uwagę, że FieldInfo.SetValueDirect , który jest publiczny i prawdopodobnie używany przez niektórych programistów, zależy od tego. :)TypedReference
żadnego z nich. (Okropna składnia i ogólna nieporęczność dyskwalifikują go, moim zdaniem, z kategorii miłych do posiadania). Powiedziałbym, że dobrze jest mieć w pobliżu, gdy naprawdę trzeba skrócić kilka mikrosekund tu i tam. To powiedziawszy, myślę o kilku miejscach w moim własnym kodzie, którym zamierzam się teraz przyjrzeć, aby sprawdzić, czy mogę je zoptymalizować przy użyciu wskazanych przez Ciebie technik.TypedReference
s, ale IIRC, jedyne miejsce, w którym udało mi się gdzieś uniknąć boksu, to elementy jednowymiarowych tablic prymitywów. Niewielka korzyść z szybkości nie była warta złożoności, jaką dodała do całego projektu, więc ją usunąłem.delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
kolekcję typu,T
może dostarczyć metodęActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param)
, ale JITter musiałby utworzyć inną wersję metody dla każdego typu wartościTParam
. Użycie wpisanego odwołania pozwoliłoby jednej wersji metody JITted na pracę ze wszystkimi typami parametrów.Nie mogę się dowiedzieć, czy tytuł tego pytania ma być sarkastyczny: od dawna ustalono, że
TypedReference
jest powolnym, rozdętym, brzydkim kuzynem `` prawdziwych '' zarządzanych wskaźników, które otrzymujemy w C ++ / CLIinterior_ptr<T>
, lub nawet tradycyjne parametry przez odniesienie (ref
/out
) w C # . W rzeczywistości dość trudno jest osiągnąćTypedReference
nawet podstawową wydajność, używając tylko liczby całkowitej do ponownego indeksowania oryginalnej tablicy CLR za każdym razem.Smutne szczegóły są tutaj , ale na szczęście nic z tego nie ma teraz znaczenia ...
Te nowe funkcje językowe zapewniają widoczną, pierwszorzędną obsługę języka C # w celu deklarowania, udostępniania i manipulowania typami prawdziwie
CLR
zarządzanych typów referencyjnych w dokładnie określonych sytuacjach.Ograniczenia użycia nie są bardziej rygorystyczne niż to, co było wcześniej wymagane
TypedReference
(a wydajność dosłownie przeskakuje od najgorszego do najlepszego ), więc nie widzę żadnego innego możliwego przypadku użycia w C # dlaTypedReference
. Na przykład wcześniej nie było sposobu, aby utrzymać sięTypedReference
wGC
stercie, więc to samo dotyczy lepszych wskaźników zarządzanych, teraz nie jest to kwestia na wynos.I oczywiście upadek
TypedReference
- a przynajmniej jego prawie całkowite wycofanie się - oznacza również wyrzucenie__makeref
na śmietnik.źródło