Подтвердить что ты не робот

Почему TypedReference за кадром? Это так быстро и безопасно... почти волшебно!

Предупреждение: этот вопрос немного еретический... религиозные программисты всегда соблюдают хорошие практики, пожалуйста, не читайте его.:)

Кто-нибудь знает, почему использование TypedReference настолько обескуражено (неявно, из-за отсутствия документации)?

Я нашел для него большое применение, например, при передаче общих параметров с помощью функций, которые не должны быть общими (при использовании object может быть чрезмерным или медленным, если вам нужен тип значения), поскольку, когда вы нужен непрозрачный указатель или когда вам нужно быстро получить доступ к элементу массива, спецификации которого вы найдете во время выполнения (используя Array.InternalGetReference). Поскольку CLR даже не допускает неправильного использования этого типа, почему это не рекомендуется? Кажется, это небезопасно или что-то еще...


Другие использования, которые я нашел для TypedReference:

"Специализируется" в С# (это безопасно для типов):

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

Так почему же использование таких "обескураженных" (из-за отсутствия документации)? Какие-либо конкретные соображения безопасности? Это кажется совершенно безопасным и проверяемым, если оно не смешивается с указателями (которые в любом случае не являются безопасными или проверяемыми)...


Update:

Пример кода, чтобы показать, что, действительно, 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
    }
}

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

4b9b3361

Ответ 1

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

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

Определяется в библиотеке Vararg, что совершенно ясно, что они предназначены в первую очередь для поддержки списков аргументов переменной длины и не более того. Переменные списки аргументов мало используются на платформах, которым не нужно взаимодействовать с внешним кодом C, который использует varargs. По этой причине библиотека Varargs не является частью профиля CLI. Законные реализации CLI могут не поддерживать библиотеку Varargs, поскольку она не включена в профиль ядра CLI:

4.1.6 Vararg

Набор функций vararg поддерживает списки аргументов переменной длины и указатели, заданные во время выполнения.

Если пропущено: Любая попытка ссылаться на метод с помощью соглашения о вызове vararg или кодировки подписи, связанные с методами vararg (см. раздел II), должна вызывать исключение System.NotImplementedException. Методы с использованием инструкций CIL arglist, refanytype, mkrefany и refanyval должны вызывать исключение System.NotImplementedException. Точное время исключения не указывается. Тип System.TypedReference не обязательно должен быть определен.

Обновление (ответ на комментарий GetValueDirect):

FieldInfo.GetValueDirect являются FieldInfo.SetValueDirect частью не базовой библиотеки классов. Обратите внимание, что существует разница между библиотекой классов .NET Framework и базовой библиотекой классов. BCL - единственное, что требуется для соответствующей реализации CLI/С# и описано в ECMA TR/84. (Фактически, сам FieldInfo является частью библиотеки Reflection и не включен в профиль ядра CLI).

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

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

  • Как и любая функция, она может стать препятствием для новых функций, тем более что эта версия не очень подходит для дизайна С# и требует необычных расширений синтаксиса и специальной передачи типа во время выполнения.
  • Все реализации С# должны каким-то образом реализовать эту функцию, и это не обязательно тривиально/возможно для реализаций С#, которые вообще не выполняются поверх CLI или работают поверх CLI без Varargs.

Ответ 2

Ну, я не Эрик Липперт, поэтому я не могу говорить напрямую о мотивах Microsoft, но если бы я рискнул предположить, я бы сказал, что TypedReference et al. не являются хорошо документированными, потому что, честно говоря, они вам не нужны.

Каждое использование, которое вы упомянули для этих функций, может быть выполнено без них, хотя и в некоторых случаях. Но С# (и .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 и друзей. Вы указали на некоторые опрятные применения для них, и я думаю, что их стоит изучить, но обычное предостережение о применении деталей реализации - следующая версия может нарушить ваш код.

Ответ 3

Я не могу понять, должно ли это название вопроса быть саркастичным: он был давно установленным, что TypedReference является медленным, раздутым, уродливым двоюродный брат "истинных" управляемых указателей, а именно, что мы получаем с С++/CLI interior_ptr<T> или даже традиционными параметрами by-reference (ref/out) в С#/STRONG > . На самом деле, довольно сложно сделать TypedReference даже достижением базовой производительности, просто используя целое число, чтобы каждый раз повторно индексировать исходный массив CLR.

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

Этот вопрос теперь обсуждается новым ref locals и ref return в С# 7

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

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

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