Почему 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)); }
    }
}

Написание метода, который передает параметр "состояния", который хочет избежать бокса:

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

(Изменить: я отредактировал тест выше, так как последняя версия сообщения использовала отладочную версию кода [я забыл изменить ее для выпуска] и не оказывал давления на сборщик мусора. Эта версия немного более реалистична и в моей системе это в TypedReferenceсреднем более чем в три раза быстрее .)

user541686
источник
Когда я запускаю ваш пример, я получаю совершенно другие результаты. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Независимо от того, что я пытаюсь (включая разные способы определения времени), упаковка / распаковка все еще быстрее в моей системе.
Сен
1
@Seph: Я только что видел твой комментарий. Это очень интересно - вроде бы быстрее на x64, но медленнее на x86. Странно ...
user541686
1
Я только что протестировал этот тестовый код на своей машине x64 под .NET 4.5. Я заменил Environment.TickCount на Diagnostics.Stopwatch и выбрал ms вместо тиков. Каждую сборку (x86, 64, Any) я запускал по три раза. Наилучшие из трех результатов были следующими: x86: 205/27 мс (такой же результат для 2/3 запусков в этой сборке) x64: 218/109 мс Любые: 205/27 мс (тот же результат для 2/3 запусков в этой сборке) -все случаи коробка / распаковка была быстрее.
kornman00
2
Странные измерения скорости можно отнести к этим двум фактам: * (T) (object) v на самом деле НЕ выделяет память в куче. В .NET 4+ он оптимизирован. На этом пути нет выделений, и он чертовски быстр. * Использование makeref требует, чтобы переменная была фактически размещена в стеке (в то время как метод своего рода коробки может оптимизировать ее в регистры). Кроме того, глядя на тайминги, я предполагаю, что это ухудшает встраивание даже с флагом force-inline. Итак, kinda-box встроен и зарегистрирован, в то время как makeref выполняет вызов функции и управляет стеком
hypersw
1
Чтобы увидеть прибыль от преобразования typeref, сделайте его менее тривиальным. Например, приведение базового типа к типу перечисления ( int-> DockStyle). Это коробки по-настоящему, и работает почти в десять раз медленнее.
hypersw

Ответы:

42

Краткий ответ: портативность .

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

Поскольку они определены в библиотеке Vararg, совершенно ясно, что они в первую очередь предназначены для поддержки списков аргументов переменной длины и не более того. Списки переменных-аргументов мало используются на платформах, которым не нужно взаимодействовать с внешним кодом C, использующим 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 и библиотекой базовых классов. BCL - единственное, что требуется для соответствующей реализации CLI / C # и задокументировано в ECMA TR / 84 . (Фактически, он FieldInfoсам является частью библиотеки Reflection и также не включен в профиль ядра CLI).

Как только вы используете метод вне BCL, вы немного отказываетесь от переносимости (и это становится все более важным с появлением реализаций CLI, отличных от .NET, таких как Silverlight и MonoTouch). Даже если реализация хотела бы повысить совместимость с библиотекой классов Microsoft .NET Framework, она могла бы просто предоставить GetValueDirectи SetValueDirectпринять, TypedReferenceне делая TypedReferenceспециально обрабатываемые средой выполнения (в основном, делая их эквивалентными своим objectаналогам без повышения производительности).

Если бы они задокументировали это на C #, это имело бы как минимум пару последствий:

  1. Как и любая функция, она может стать препятствием на пути к новым функциям, тем более, что эта функция действительно не вписывается в дизайн C # и требует странных расширений синтаксиса и специальной обработки типа средой выполнения.
  2. Все реализации C # должны каким-то образом реализовывать эту функцию, и это не обязательно тривиально / возможно для реализаций C #, которые вообще не работают поверх CLI или работают поверх CLI без Varargs.
Мехрдад Афшари
источник
4
Хорошие аргументы в пользу портативности, +1. А как насчет FieldInfo.GetValueDirectи FieldInfo.SetValueDirect? Они являются частью BCL, и для их использования вам нужно TypedReference , так что разве это не заставляет TypedReferenceвсегда быть определенным, независимо от спецификации языка? (Также еще одно примечание: даже если ключевые слова не существуют, пока существуют инструкции, вы все равно можете получить к ним доступ, динамически генерируя методы ... так что, пока ваша платформа взаимодействует с библиотеками C, вы можете использовать их, есть ли в C # ключевые слова.)
user541686
Да, и еще одна проблема: даже если он не переносится, почему они не задокументировали ключевые слова? По крайней мере, это необходимо при взаимодействии с C varargs, чтобы они хотя бы могли упомянуть об этом?
user541686
@ Mehrdad: А, это интересно. Думаю, я всегда предполагал, что файлы в папке BCL исходного кода .NET являются частью BCL, никогда особо не обращая внимания на часть стандартизации ECMA. Это довольно убедительно ... за исключением одной мелочи: разве не бессмысленно даже включать (необязательную) функцию в спецификацию CLI, если нет документации о том, как ее где угодно использовать? (Было бы разумно, если бы он TypedReferenceбыл задокументирован только для одного языка - скажем, управляемого C ++ - но если ни один язык не документирует его, и поэтому, если никто действительно не может его использовать, тогда зачем вообще определять эту функцию?)
user541686
@Mehrdad Я подозреваю, что основной мотивацией была потребность в этой функции для внутреннего взаимодействия ( например [DllImport("...")] void Foo(__arglist); ), и они реализовали ее на C # для собственного использования. На дизайн интерфейса командной строки влияют многие языки (аннотации «Стандарт аннотированной инфраструктуры общего языка» демонстрируют этот факт). Быть подходящей средой выполнения для максимально возможного количества языков, включая непредвиденные, определенно было целью дизайна (отсюда и name), и это функция, от которой, вероятно, могла бы выиграть гипотетическая управляемая реализация C.
Mehrdad Afshari
@ Мехрдад: А ... да, это довольно убедительная причина. Спасибо!
user541686
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и друзьям детали реализации. Вы указали на некоторые изящные способы их использования, и я думаю, что их стоит изучить, но действует обычное предостережение - полагаться на детали реализации - следующая версия может нарушить ваш код.

P Папа
источник
4
Ха ... "они тебе не нужны" - я должен был это предвидеть. :-) Это правда, но это тоже неправда. Что вы определяете как «потребность»? Например, действительно ли «нужны» методы расширения? Что касается вашего вопроса об использовании дженериков в call(): Это потому, что код не всегда такой связный - я больше имел в виду пример, больше похожий на тот IAsyncResult.State, где введение дженериков просто невозможно, потому что внезапно он представит дженерики для каждый задействованный класс / метод. +1 за ответ, хотя ... особенно за то, что указал на "быстрее, чем Java". :]
user541686
1
Да, и еще один момент: TypedReferenceвероятно, в ближайшее время не будут происходить критические изменения, учитывая, что от него зависит FieldInfo.SetValueDirect , который является общедоступным и, вероятно, используется некоторыми разработчиками. :)
user541686
Ах, но вы действительно нужны методы расширения для поддержки LINQ. Во всяком случае, я не говорю о разнице между желаниями и нуждами. Я бы не стал звонить TypedReferenceни одному из них. (Ужасный синтаксис и общая громоздкость дисквалифицируют его, на мой взгляд, из категории приятных вещей.) Я бы сказал, что это просто хорошая вещь, когда вам действительно нужно сократить несколько микросекунд тут и там. Тем не менее, я думаю о паре мест в моем собственном коде, которые я собираюсь изучить прямо сейчас, чтобы увидеть, могу ли я их оптимизировать, используя указанные вами методы.
P Daddy
1
@Merhdad: В то время я работал над сериализатором / десериализатором двоичных объектов для межпроцессного / межхостового взаимодействия (TCP и каналы). Моей целью было сделать его как можно меньшим (с точки зрения байтов, отправляемых по сети) и быстрым (с точки зрения времени, затрачиваемого на сериализацию и десериализацию). Я думал, что могу избежать упаковки и распаковки с помощью TypedReferences, но IIRC, единственное место, где я мог где-то избежать упаковки, было с элементами одномерных массивов примитивов. Небольшой выигрыш в скорости здесь не стоил той сложности, которую он добавил ко всему проекту, поэтому я отказался от него.
P Daddy
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 03
4

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

Печальные подробности здесь , но, к счастью, сейчас все это не имеет значения ...

Этот вопрос теперь становится спорным из-за новых функций ref locals и ref return в C # 7.

Эти новые языковые функции обеспечивают выдающуюся первоклассную поддержку в C # для объявления, совместного использования и управления настоящими CLR управляемыми ссылочными типами типов в тщательно продуманных ситуациях.

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

И, очевидно, прекращение существования TypedReference- или, по крайней мере, его почти полное прекращение - означает также выброс __makerefна свалку.

Гленн Слейден
источник