Недокументированные ключевые слова C# или превращаем объект в тыкву

.NET C# IL Benchmarking

Стандартный компилятор C# поддерживает 4 недокументированных ключевых слова: __makeref, __reftype, __refvalue, __arglist. Эти слова даже успешно распознаются в Visual Studio (хотя, ReSharper на них ругается). Они не даром исключены из стандарта — их использование может повлечь серьёзные проблемы с безопасностью. Поэтому не нужно их использовать везде подряд, но в отдельных исключительных случаях они могут пригодиться. В этом посте я обсужу предназначение недокументированных команд, рассмотрю вопросы их производительности и научусь превращать объект в тыкву.

Описание ключевых слов

Все рассматриваемые слова связаны со структурой TypedReference. Она хранит в себе два поля: указатель на область памяти и тип данных объекта, который расположен по этому указателю. Помимо рассмотренных ниже ключевых слов для операций над этой структурой могут пригодиться методы GetTargetType, MakeTypedReference, SetTypedReference, TargetTypeToken, ToObject.

Теперь перейдём непосредственно к ключевым словам. __makeref принимает на входе объект и возвращает TypedReference ссылку на него. __reftype и __refvalue способны достать из TypedReference значения двух его полей: тип и значение. Посмотрим простой пример, который поясняет использование ключевых слов:

double value = 10;
TypedReference typedReference = __makeref(value); // typedReference = &value;
Console.WriteLine( __refvalue(typedReference, double)); // 10
__refvalue(typedReference, double) = 11; // *typedReference = 11
Console.WriteLine( __refvalue(typedReference, double)); // 11
Type type = __reftype(typedReference); // value.GetType()
Console.WriteLine(type.Name); // Double

Данный пример развернётся в IL-код, который представлен ниже. Как можно понять, рассмотренные ключевые слова транслируются в IL-команды mkrefany , refanyval , refanytype.

.maxstack 2
.locals init (
    [0] float64 'value',
    [1] valuetype [mscorlib]System.TypedReference typedReference,
    [2] class [mscorlib]System.Type 'type')
L_0000: ldc.r8 10
L_0009: stloc.0 
L_000a: ldloca.s 'value'
L_000c: mkrefany float64
L_0011: stloc.1 
L_0012: ldloc.1 
L_0013: refanyval float64
L_0018: ldind.r8 
L_0019: call void [mscorlib]System.Console::WriteLine(float64)
L_001e: ldloc.1 
L_001f: refanyval float64
L_0024: ldc.r8 11
L_002d: stind.r8 
L_002e: ldloc.1 
L_002f: refanyval float64
L_0034: ldind.r8 
L_0035: call void [mscorlib]System.Console::WriteLine(float64)
L_003a: ldloc.1 
L_003b: refanytype 
L_003d: call class [mscorlib]System.Type 
          [mscorlib]System.Type::GetTypeFromHandle
          (valuetype [mscorlib]System.RuntimeTypeHandle)
L_0042: stloc.2 
L_0043: ldloc.2 
L_0044: callvirt instance string 
          [mscorlib]System.Reflection.MemberInfo::get_Name()
L_0049: call void [mscorlib]System.Console::WriteLine(string)
L_004e: ret 

__arglist позволяет создать метод с переменным количеством параметров. Причём это не передача массива объектов через params, а в чистом виде переменное количество параметров. Получить переданные значения можно через структуру ArgIterator. Ниже приведён пример, который иллюстрирует использование команды.

public void Run()
{
    Foo(__arglist(1, 2.0, "3", new int[0]));
}

public void Foo(__arglist)
{
    var iterator = new ArgIterator(__arglist);
    while (iterator.GetRemainingCount() > 0)
    {
        TypedReference typedReference = iterator.GetNextArg();
        Console.WriteLine("{0} / {1}", 
            TypedReference.ToObject(typedReference), 
            TypedReference.GetTargetType(typedReference));
    }
}

И соответствующий IL-код, в котором можно познакомиться с командой arglist:

.method public hidebysig instance void Run() cil managed
{
.maxstack 8
L_0000: ldarg.0 
L_0001: ldc.i4.1 
L_0002: ldc.r8 2
L_000b: ldstr "3"
L_0010: ldc.i4.0 
L_0011: newarr int32
L_0016: call instance vararg void Program::Foo(..., int32, float64, string)
L_001b: ret 
}

.method public hidebysig instance vararg void Foo() cil managed
{
.maxstack 3
.locals init (
    [0] valuetype [mscorlib]System.ArgIterator iterator,
    [1] valuetype [mscorlib]System.TypedReference typedReference)
L_0000: ldloca.s iterator
L_0002: arglist 
L_0004: call instance void 
          [mscorlib]System.ArgIterator::.ctor
          (valuetype [mscorlib]System.RuntimeArgumentHandle)
L_0009: br.s L_0029
L_000b: ldloca.s iterator
L_000d: call instance valuetype 
          [mscorlib]System.TypedReference 
          [mscorlib]System.ArgIterator::GetNextArg()
L_0012: stloc.1 
L_0013: ldstr "{0} / {1}"
L_0018: ldloc.1 
L_0019: call object [mscorlib]System.TypedReference::ToObject
          (valuetype [mscorlib]System.TypedReference)
L_001e: ldloc.1 
L_001f: call class [mscorlib]System.Type 
          [mscorlib]System.TypedReference::GetTargetType
          (valuetype [mscorlib]System.TypedReference)
L_0024: call void [mscorlib]System.Console::WriteLine(string, object, object)
L_0029: ldloca.s iterator
L_002b: call instance int32 [mscorlib]System.ArgIterator::GetRemainingCount()
L_0030: ldc.i4.0 
L_0031: bgt.s L_000b
L_0033: ret 
}

Поговорим о производительности

На StackOverflow есть обсуждение, в котором утверждается, что якобы работа с TypedReference осуществляется быстрее, чем упаковка/распаковка. Но бенчмарк у автора очень странный. Плюс, как мне кажется, автор запускал его в Debug mode with debugging — в этом случае действительно могут получиться такие результаты. Но некоторые люди писали в комментариях, что на самом деле упаковка/распаковка работает намного быстрее. Я решил проверить это, составив бенчмарк с помощью BenchmarkDotNet v0.5.1 (Update: данный пост был написан в 2013 году, в ту пору BenchmarkDotNet только начинал развиваться. С тех пор библиотека была значительно доработана, API был изменён.):

private const int IterationCount = 10000000;
private int[] array;

public void Run()
{
    array = new int[5];

    var competition = new BenchmarkCompetition();
    competition.AddTask("MakeRef", MakeRef);
    competition.AddTask("Boxing", Boxing);
    competition.Run();
}

public void MakeRef()
{
    for (int i = 0; i < IterationCount; i++)
        Set1(array, 0, i);
}

public void Boxing()
{
    for (int i = 0; i < IterationCount; i++)
        Set2(array, 0, i);
}

public void Set1(T[] a, int i, int v)
{
    __refvalue(__makeref(a[i]), int) = v;
}

public void Set2(T[] a, int i, int v)
{
    a[i] = (T)(object)v;
}

Не забывайте, что бенчмарки нужно запускать только в Release mode without debugging. Результаты, которые получились на моём ноутбуке:

MakeRef : 313ms
Boxing  :  34ms

У нас имеются классы MyObject, который содержит одно поле на 64 бита, и Pumpkin, который содержит два поля по 32 бита. В методе Run выполняются следующие вещи: мы создаём объект myObject, инициализируем его поле, получаем на него ссылку, а затем создаём pumpkin, который ссылается на ту же область памяти. В качестве теста мы пробуем поменять значение 64-х битного поля изначально объекта и смотрим на изменение соответствующих полей в тыкве.

Особый интерес представляют методы GetAddress и Convert<T> . Начнём с первого: он получает указатель IntPtr на переданный объект. В первой строчке всё просто: мы получаем TypedReference на переданный объект, а вот во второй строчке происходит немного магии. Первое поле TypedReference хранит IntPtr -ссылку на наш объект, но явно мы получить эту ссылку не можем. Поэтому мы получаем указатель на наш TypedReference (который также является указателем на его первое поле), приводим его к указателю на IntPtr , а потом разыменовываем. В итоге имеем своего рода неуправляемое получение адреса объекта.

А теперь переходим к методу Convert<T>. Этот метод должен нам создать объект типа T, который ссылается на заданную область памяти. В первой строке мы создаём дефолтный экземпляр типа T . Единственное его предназначение — это получить соответствующий typedReference, который создаётся во второй строчке. Второе поле полученной структуры указывает на нужный нам тип. Третьей строчкой мы записываем переданный нам адрес в первое поле структуры с помощью уже знакомой нам конструкции *(IntPtr*)(&typedReference) . И в последней четвёртой строчке мы собираем из нашей typedReference структуры готовый объект целевого типа с помощью __refvalue . Вуаля: тыква готова.

P.S. Приведённый пример имеет чисто академическое предназначение, он приведён как демонстрация использования заявленных ключевых слов. В продакшн-коде нужно несколько раз подумать, прежде чем решить, что вам действительно необходимы подобные конструкции.