Блог переехал. Актуальная версия поста находится по адресу: 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. Приведённый пример имеет чисто академическое предназначение, он приведён как демонстрация использования заявленных ключевых слов. В продакшн-коде нужно несколько раз подумать, прежде чем решить, что вам действительно необходимы подобные конструкции.
Комментариев нет:
Отправить комментарий