Чому TypedReference знаходиться поза кадром? Це так швидко і безпечно ... майже магічно!


128

Попередження: Це питання єретично ... релігійні програмісти завжди дотримуються належної практики, не читайте цього. :)

Хтось знає, чому використання TypedReference настільки відмовляється (неявно, через відсутність документації)?

Я знайшов для цього велику користь, наприклад, при передачі загальних параметрів через функції, які не повинні бути загальними (коли використання objectможе бути надмірним або повільним, якщо вам потрібен тип значення), коли вам потрібен непрозорий вказівник, або коли вам потрібно швидко отримати доступ до елемента масиву, специфікації якого ви знайдете під час виконання (за допомогою Array.InternalGetReference). Оскільки CLR навіть не допускає неправильного використання цього типу, чому це не рекомендується? Це не здається небезпечним чи нічого ...


Інші види використання, які я знайшов для TypedReference:

"Спеціалізовані" дженерики на C # (це безпечно для типу):

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); }
}

Написання коду, який працює з загальними покажчиками (це дуже небезпечно при неправильному використанні, але швидкий і безпечний при правильному використанні):

//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);
}

Написання версії методикиsizeof інструкції, яка може бути корисною:

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)); }
    }
}

Написання методу, який передає параметр "state", який хоче уникнути боксу:

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*/ }
}

То чому ж такі подібні "відсторонені" (через відсутність документації) такі використання? Якісь конкретні причини безпеки? Це здається абсолютно безпечним і перевіреним, якщо він не змішується з покажчиками (які все одно не є безпечними або перевіреними) ...


Оновлення:

Приклад коду, який показує, що дійсно TypedReferenceможе бути вдвічі швидшим (або більше):

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
    }
}

(Редагувати: я відредагував тест вище, оскільки в останній версії публікації була використана налагоджена версія коду [я забув змінити його, щоб випустити], і не тиснув на GC. Ця версія трохи реалістичніша, і в моїй системі це втричі швидше, TypedReferenceв середньому.)


Коли я запускаю ваш приклад, я отримую зовсім інші результати. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Незалежно від того, що я намагаюся (включаючи різні способи зробити таймінги), бокс / розблокування все ще швидше в моїй системі.
Seph

1
@Seph: Я щойно побачив ваш коментар. Це дуже цікаво - це здається, що воно швидше на x64, але повільніше на x86. Дивно ...
користувач541686

1
Я щойно перевірив цей контрольний код на моїй машині x64 під .NET 4.5. Я замінив Environment.TickCount на Diagnostics.Stopwatch і пішов з ms замість кліщів. Я тричі проводив кожну збірку (x86, 64, Any) три рази. Найкращими з трьох результатів були: follws: x86: 205 / 27ms (той самий результат для 2/3 пробірок у цій збірці) x64: 218 / 109ms Будь-який: 205 / 27ms (такий же результат для 2/3 пробігів у цій збірці) У -всі - коробка справ / розпакування була швидшою.
kornman00

2
Дивні вимірювання швидкості можуть бути віднесені до цих двох фактів: * (T) (об'єкт) v насправді НЕ здійснює розподілу купи. У .NET 4+ він оптимізований. На цьому шляху немає виділень, і це чорт швидко. * Використання makeref вимагає, щоб змінна була фактично розподілена на стеку (в той час як метод kinda-box може оптимізувати її в регістри). Також, дивлячись на таймінги, я припускаю, що це погіршує накреслення навіть із прапорцем в праву. Таким чином, своєрідне поле є вкладеним та зареєстрованим, а makeref робить виклик функції та керує стеком
hypersw

1
Щоб побачити прибуток від кастингу, зробіть її менш тривіальною. Наприклад, переливання базового типу до типу enum ( int-> DockStyle). Це ящики справжні, і майже в десять разів повільніше.
hypersw

Відповіді:


42

Коротка відповідь: портативність .

Хоча __arglist, __makerefі __refvalueє розширеннями мови і НЕ мають документи в Специфікація мови C #, конструкція , яка використовується для реалізації їх під капотом ( varargугода про виклик, TypedReferenceтип, arglist, refanytype, mkanyref, і refanyvalінструкція) прекрасно задокументовані в специфікації CLI (ECMA-335) в бібліотека Vararg .

Визначившись у бібліотеці Vararg, цілком зрозуміло, що вони в першу чергу мають на увазі підтримку списків аргументів змінної довжини, а не багато іншого. Списки змінних аргументів мало використовуються в платформах, які не потребують взаємодії з зовнішнім кодом С, який використовує varargs. З цієї причини бібліотека Varargs не є частиною жодного профілю CLI. Легальні реалізації CLI можуть не підтримувати бібліотеку Varargs, оскільки вона не включена в профіль ядра CLI:

4.1.6 Вараг

Набір функцій vararg підтримує змінну довжину списки аргументів і час виконання типізованих покажчики.

Якщо пропущено: Будь-яка спроба посилання на метод із varargумовою виклику або кодуванням підпису, пов'язаними з методами vararg (див. Розділ II), викидає System.NotImplementedExceptionвиключення. Методи , які використовують інструкції CIL arglist, refanytype, mkrefanyі refanyvalбуде кидати System.NotImplementedExceptionвиняток. Точний термін винятку не вказаний. Тип System.TypedReferenceне потрібно визначати.

Оновлення (відповідь на GetValueDirectкоментар):

FieldInfo.GetValueDirectє FieldInfo.SetValueDirectє НЕ частиною бібліотеки базових класів. Зауважте, що існує різниця між .NET Framework Class Library і бібліотекою базових класів. BCL - єдине, що потрібно для відповідного впровадження CLI / C # і задокументовано в ECMA TR / 84 . (Насправді вона FieldInfoсама є частиною бібліотеки Reflection, і вона також не включена до профілю ядра CLI).

Як тільки ви використовуєте метод поза BCL, ви відмовляєтеся від переносимості (і це стає все більш важливим із появою не-.NET CLI-реалізацій, таких як Silverlight та MonoTouch). Навіть якби реалізація хотіла збільшити сумісність з бібліотекою Microsoft .NET Framework Class, вона могла просто забезпечити GetValueDirectта SetValueDirectприйняти, TypedReferenceне роблячи TypedReferenceспеціально оброблювані під час виконання програми (в основному, роблячи їх еквівалентними objectаналогам без переваг від продуктивності).

Якби вони це задокументували на C #, це мало б принаймні пару наслідків:

  1. Як і будь-яка функція, вона може стати дорожнім блоком для нових функцій, тим більше, що ця не дуже вписується в дизайн C # і вимагає дивних розширень синтаксису та спеціальних передач типу під час виконання.
  2. Усі реалізації C # повинні якось реалізувати цю функцію, і це не обов'язково тривіально / можливо для C # -реалізацій, які взагалі не працюють над CLI або працюють над CLI без Varargs.

4
Хороші аргументи для портативності, +1. Але про що FieldInfo.GetValueDirectі FieldInfo.SetValueDirect? Вони є частиною BCL, і використовувати їх вам потрібно TypedReference , чи не в основному TypedReferenceце завжди змушує визначатись незалежно від мовної специфікації? (Крім того, ще одна примітка. Навіть якщо ключові слова не існували, доки існували інструкції, ви все одно можете отримати доступ до них, динамічно випромінюючи методи ... так що поки ваша платформа взаємодіє з бібліотеками С, ви можете використовувати їх, чи має ключові слова C # чи ні.)
користувач541686

О, і ще одне питання: Навіть якщо це не портативно, чому вони не задокументували ключові слова? Принаймні, це потрібно під час взаємодії з варагмами C, щоб принаймні вони могли про це згадати?
користувач541686

@Mehrdad: Так, це цікаво. Напевно, я завжди вважав, що файли в папці BCL джерела .NET є частиною BCL, ніколи не звертаючи уваги на частину стандартизації ECMA. Це досить переконливо ... окрім однієї дрібниці: чи не є трохи безглуздим навіть включення (необов'язкової) функції у специфікацію CLI, якщо немає документації щодо її використання де-небудь? (Було б сенс, якщо це TypedReferenceбуло задокументовано лише для однієї мови - скажімо, керованого C ++ - але якщо жодна мова не документує це, і якщо ніхто не може реально користуватися нею, то навіщо навіть турбуватись про визначення функції?)
user541686

@Mehrdad Я підозрюю, що основною мотивацією була потреба в цій функції внутрішньо для інтеропа ( наприклад [DllImport("...")] void Foo(__arglist); ), і вони реалізували її в C # для власного використання. На дизайн CLI впливає багато мов (примітки "Загальна мовна інфраструктура з анотованими стандартами" демонструють цей факт.) Бути підходящим часом виконання для якомога більшої кількості мов, включаючи непередбачувані, безумовно, була метою дизайну (отже назва), і це особливість, від якої, наприклад, гіпотетична керована реалізація C могла б отримати користь.
Мехрдад Афшарі

@Mehrdad: Ага ... так, це досить переконлива причина. Дякую!
користувач541686

15

Ну, я не Ерік Ліпперт, тому я не можу говорити безпосередньо про мотивації Microsoft, але якби я мав намір здогадатися, я б сказав, що TypedReferenceта ін. недостатньо задокументовані, бо, чесно кажучи, вони вам не потрібні.

Кожне використання, яке ви згадали для цих функцій, може бути здійснено без них, хоча в деяких випадках покарання за продуктивність. Але C # (і .NET взагалі) не розроблений як високопродуктивна мова. (Я здогадуюсь, що "швидше, ніж Java" була метою продуктивності.)

Це не означає, що певних міркувань щодо ефективності не було надано. Дійсно, такі функції, як покажчики stackallocта певні оптимізовані рамкові функції, значною мірою існують для підвищення продуктивності в певних ситуаціях.

Загальні переваги , як я б сказав, мають головну перевагу безпеки типу, також покращують продуктивність аналогічно TypedReference, уникаючи боксу та розпакування. Насправді мені було цікаво, чому ви віддасте перевагу саме такому:

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

до цього:

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

Як я їх бачу, компроміси полягають у тому, що перший вимагає меншої кількості JIT (і, як наслідок, менше пам'яті), а другий більш звичний і, я вважаю, трохи швидше (уникаючи перенаправлення покажчиків).

Я б зателефонував TypedReferenceі друзям про деталі реалізації Ви вказали на деякі акуратні способи їх використання, і я думаю, що їх варто вивчити, але застосовується звичайний застереження покладатися на деталі реалізації - наступна версія може порушити ваш код.


4
Ага ... "вони вам не потрібні" - я повинен був бачити, що це прийде :-) Це правда, але це теж неправда. Що ви визначаєте як "потребу"? Чи реально потрібні, наприклад, методи розширення? Що стосується вашого питання щодо використання дженериків у call(): Це тому, що код не завжди є настільки згуртованим - я мав на увазі більше подібний приклад IAsyncResult.State, коли введення генерики просто було б нездійсненним, оскільки раптом воно ввело б дженерики для кожен клас / метод, що беруть участь. Хоча для відповіді +1, особливо ... особливо для вказівки на частину "швидше, ніж Java". :]
користувач541686

1
Ну, і ще один момент: TypedReferenceнапевно, незабаром не зазнаватимуть змін, зважаючи на те, що FieldInfo.SetValueDirect , який є загальнодоступним, і, ймовірно, використовується деякими розробниками, залежить від цього. :)
користувач541686

Ах, але ви дійсно потрібні методи розширення для підтримки LINQ. У всякому разі, я не дуже розмовляю про приємну різницю. Я б не називав TypedReferenceжодного з них. (Жорстокий синтаксис і загальна недбалість дискваліфікують його, на мій погляд, із категорії приємних, щоб мати.) Я б сказав, що це просто гарна річ, коли вам справді потрібно обрізати кілька мікросекунд тут і там. Це означає, що я думаю про декілька місць у власному коді, які я зараз хочу переглянути, щоб побачити, чи можу я їх оптимізувати, використовуючи методи, які ви вказали.
P тато

1
@Merhdad: Я працював над серіалізатором / десеріалізатором бінарних об'єктів у той час для міжпроцесорних / міжгалузевих комунікацій (TCP та труби). Мої цілі полягали в тому, щоб зробити це максимально невеликим (у переліку байтів, що надсилаються по дроту) і швидким (за часовим витрачанням на серіалізацію та десеріалізацію), наскільки це можливо. Я думав, що міг би уникнути якихось боксів та розпакування боків із TypedReferences, але IIRC, єдине місце, де мені вдалося уникнути боксу, було десь з елементами одновимірних масивів примітивів. Невелика користь від швидкості тут не варта того складності, яку вона додала до всього проекту, тому я його вийняв.
P тато

1
Дана delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);колекція типів Tможе забезпечити метод ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param), але JITter повинен був створити іншу версію методу для кожного типу значень TParam. Використання введеної посилання дозволило б одній версії методу JITted працювати з усіма типами параметрів.
supercat

4

Я не можу зрозуміти, чи має бути назва цього питання саркастичним: давно встановлено, що TypedReferenceце повільний, роздутий, потворний двоюрідний брат «справжніх» керованих покажчиків, останній - те, що ми отримуємо з C ++ / CLI interior_ptr<T> , або навіть традиційні параметри посилання ( ref/ out) в C # . Насправді, досить важко домогтися TypedReferenceнавіть досягнення базової продуктивності просто за допомогою цілого числа, щоб кожен раз переіндексувати вихідний масив CLR.

Сумні деталі є тут , але, на щастя, зараз нічого з цього не має значення ...

Це запитання тепер виноситься новими місцевими жителями і повертаються в C # 7

Ці нові мовні функції забезпечують помітну, першокласну підтримку в C # для декларування, обміну та маніпулювання справжніми CLR керованими типами еталонів у ретельно визначених ситуаціях.

Обмеження щодо використання не суворіші за те, що раніше вимагалося TypedReference(а продуктивність буквально переходить від гіршого до кращого ), тому я не бачу жодного можливого випадку використання у C # for TypedReference. Наприклад, раніше не було можливості втриматись TypedReferenceу GCкупі, тому те саме, що стосується вищих керованих покажчиків, зараз не є виносним.

І очевидно, що смерть - TypedReferenceабо принаймні її майже повне знецінення - означає також кинути __makerefна смітник.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.