Блог переехал. Актуальная версия поста находится по адресу: http://aakinshin.net/ru/blog/dotnet/undocumented-keywords-in-cs/.
Стандартный компилятор 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. Выглядит он следующим образом (полная версия кода: MakeRefVsBoxingProgram.cs):
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
Превращаем объект в тыкву
А теперь обещанный пример с тыквой. Рассмотрим код:
public class MyObject
{
public long X;
}
public class Pumpkin
{
public int Y1;
public int Y2;
}
public unsafe IntPtr GetAddress(object obj)
{
var typedReference = __makeref(obj);
return *(IntPtr*)(&typedReference);
}
public unsafe T Convert<T>(IntPtr address)
{
var fakeInstance = default(T);
var typedReference = __makeref(fakeInstance);
*(IntPtr*)(&typedReference) = address;
return __refvalue(typedReference, T);
}
public void Run()
{
var myObject = new MyObject { X = 1 + (2L << 32) };
var pumpkin = Convert<Pumpkin>(GetAddress(myObject));
Console.WriteLine(pumpkin.Y1 + " " + pumpkin.Y2); // 1 2
myObject.X = 3 + (4L << 32);
Console.WriteLine(pumpkin.Y1 + " " + pumpkin.Y2); // 3 4
}
У нас имеются классы 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. Приведённый пример имеет чисто академическое предназначение, он приведён как демонстрация использования заявленных ключевых слов. В продакшн-коде нужно несколько раз подумать, прежде чем решить, что вам действительно необходимы подобные конструкции.
Комментариев нет:
Отправить комментарий