Хабрахабр

Unsafe.AsSpan: Span<T> как замена указателям?

На нем можно писать не только бэкэнд или десктопные приложения. C# — невероятно гибкий язык. Хотя netcore захватывает повестку дня (учитывая, что после netstandard2. Я использую C# для работы, в том числе, и с научными данными, которые накладывают определенные требования на инструменты, доступные в языке. 0 большинство фич как языков, так и рантайма, не бэк-портируются в netframework), я продолжаю работать и с легаси-проектами.

В этой статье я рассматриваю одно неочевидное (но, наверное, желаемое?) применение Span<T> и отличие реализации Span<T> в netframework и netcore из-за особенностей clr.

Дисклеймер 1

Фрагменты кода в данной статье ни в коем случае не предназначены для использования в реальных проектах.

Предлагаемое решение (надуманной?) проблемы — это, скорее, proof-of-concept.
В любом случае, реализуя подобное в своем проекте, вы делаете это на свой страх и риск.

Дисклеймер 2

Я абсолютно уверен, что где-то, в каком-то случае это обязательно выстрелит кому-то в колено.

Обход типобезопасности в C# вряд ли приводит к чему-то хорошему.

По очевидным причинам, я не тестировал данный код во всех возможных ситуациях, однако предварительные результаты выглядят многообещающими.

Несмотря на тот факт, что поддержка спэнов в BCL netframework практически полностью отсутствует, несколько инструментов можно получить, используя System. Спэн позволяет работать с массивами unmanaged-типов в более удобной форме, уменьшая количество необходимых аллокаций. Buffers и System. Memory, System. CompilerServices. Runtime. В своем проекте я работаю с данными, получаемыми с научного инструмента. Unsafe.
Использование спэнов в моем легаси-проекте ограничено, однако я нашел им неочевидное применение, попутно наплевав на безопасность типов.
Что же это за применение? Для корректной сериализации этих изображений на диск, мне необходимо поддерживать невероятно неудобный легаси-формат, который был предложен в 1981-м, и с тех пор слабо поменялся. Это изображения, которые, в общем случае представляют собой массив T[], где T это один из unmanaged примитивных типов, например Int32 (он же int). Таким образом, чтобы записать (или прочитать) несжатый массив T[], нужно поменять endianess каждого элемента. Главная проблема этого формата — он BigEndian. Тривиальная задача.
Какие можно предложить очевидные решения?

  1. Итерируем по массиву T[], вызываем BitConverter.GetBytes(T), разворачиваем эти несколько байт, копируем в целевой массив.
  2. Итерируем по массиву T[], выполняем махинации вида new byte[] ; (должно работать на двухбайтовых типах), пишем в целевой массив.
  3. * Но ведь T[] это массив? Элементы находятся подряд, да? Значит можно пойти во все тяжкие, например Buffer.BlockCopy(intArray, 0, byteArray, 0, intArray.Length * sizeof(int));. Метод копирует массив в массив игнорируя проверку типов. Нужно лишь не промахнуться с границами и аллокацией. Перемешиваем байты уже в результате.
  4. * Говорят, что C# это (C++)++. Поэтому включаем /unsafe, вооружаемся fixed(int* p = &intArr[0]) byte* bPtr = (byte*)p; и вот уже можно бегать по байтовому представлению исходного массива, на лету менять endianess и писать блоками на диск (добавив stackalloc byte[] или ArrayPool<byte>.Shared для промежуточного буфера), не выделяя память на целый новый массив байт.

Тут нам на помощь и приходит Span<T>. Казалось бы, 4 пункт позволяет решить все проблемы, но явное использование unsafe-контекста и работа с указателями — это как-то совсем не то.

Такой GC-aware указатель с границами массива. Span<T> технически должен предоставлять инструменты для работы с участками памяти практически как работа через указатели, при этом исключая необходимость "закреплять" массив в памяти. Runtime. Все отлично и безопасно.
Одно лишь но — несмотря на богатство System. Unsafe, Span<T> гвоздями прибит к типу T. CompilerServices. Благо у нас есть public Span<T>(void* pointer, int length).
Напишем простой тест: Учитывая, что спэн это, по сути, указатель1 + длина, а что если вытащить этот ваш указатель, преобразовать его к другому типу, пересчитать длину и сделать новый спэн?

[Test]
public void Test()
{ void Flip(Span<byte> span) {/* тут вращаем endianess */} Span<int> x = new [] {123}; Span<byte> y = DangerousCast<int, byte>(x); Assert.AreEqual(123, x[0]); Flip(y); Assert.AreNotEqual(123, x[0]); Flip(y); Assert.AreEqual(123, x[0]);
}

Провалится ли тест? Более продвинутые чем я разработчики должны сразу сообразить, что здесь не так. На netcore тест должен работать, а на netframework — как получится.
Интересно, что, если убрать часть эссертов, тест начинает корректно работать в 100% случаев.
Давайте разбираться. Ответ, как это обычно бывает, — зависит.
В данном случае зависит в первую очередь от рантайма.

1 Я ошибался.

Почему же результат — зависит?
Уберем все лишнее и напишем вот такой вот код:

private static void Main() => Check(); private static void Check()
{ Span<int> x = new[] {999, 123, 11, -100}; Span<byte> y = As<int, byte>(ref x); Console.WriteLine(@"FRAMEWORK_NAME"); Write(ref x); Write(ref y); Console.WriteLine(); Write<int, int>(ref x, "Span<int> [0]"); Write<byte, int>(ref y, "Span<byte>[0]"); Console.WriteLine(); Write<int, int>(ref Offset<int, object>(ref x[0], 1), "Span<int> [0] offset by size_t"); Write<byte, int>(ref Offset<byte, object>(ref y[0], 1), "Span<byte>[0] offset by size_t"); Console.WriteLine(); GC.Collect(0, GCCollectionMode.Forced, true, true); Write<int, int>(ref x, "Span<int> [0] after GC"); Write<byte, int>(ref y, "Span<byte>[0] after GC"); Console.WriteLine(); Write(ref x); Write(ref y);
}

Иными словами, Write<int, int>(ref x) выведет адрес в памяти + число 999.
Обычный Write печатает массив.
Теперь про метод As<,>: Метод Write<T, U> принимает спэн типа T, считает адрес первого элемента, и считывает через этот указатель один элемент типа U.

private static unsafe Span<U> As<T, U>(ref Span<T> span) where T : unmanaged where U : unmanaged { fixed(T* ptr = span) return new Span<U>(ptr, span.Length * Unsafe.SizeOf<T>() / Unsafe.SizeOf<U>()); }

GetPinnableReference().
Запустим этот метод на netframework4. Сейчас синтаксис C# поддерживает такую запись fixed-стэйтмента через неявный вызов метода Span<T>. Смотрим, что получается: 8 в x64 режиме.

LEGACY
[ 999, 123, 11, -100 ]
[ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|02|8C|00|00|2F|B0 999 Span<int> [0]
0x|00|00|02|8C|00|00|2F|B0 999 Span<byte>[0] 0x|00|00|02|8C|00|00|2F|B8 11 Span<int> [0] offset by size_t
0x|00|00|02|8C|00|00|2F|B8 11 Span<byte>[0] offset by size_t 0x|00|00|02|8C|00|00|2B|18 999 Span<int> [0] after GC
0x|00|00|02|8C|00|00|2F|B0 6750318 Span<byte>[0] after GC [ 999, 123, 11, -100 ]
[ 110, 0, 103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]

То, что нужно.
Окей, попробуем сдвинуть начало спэна на размер одного IntPtr (или 2 X int на x64) и прочитать. Изначально оба спэна (несмотря на разный тип) ведут себя идентично, а Span<byte>, по сути, представляет byte-view исходного массива. А потом соберем мусор... Получаем третий элемент массива и корректный адрес.

GC.Collect(0, GCCollectionMode.Forced, true, true);

После вызова GC. Последний флаг в этом методе просит GC уплотнить кучу. Span<int> отражает эти изменения, а вот наш Span<byte> продолжает указывать на старый адрес, где теперь непонятно что. Collect GC перемещает исходный локальный массив. Отличный способ прострелить себе все колени сразу!

0. Теперь посмотрим на результат точно такого же фрагмента кода, вызванного на netcore3. 100-preview8.

CORE
[ 999, 123, 11, -100 ]
[ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ] 0x|00|00|01|F2|8F|BD|C6|90 999 Span<int> [0]
0x|00|00|01|F2|8F|BD|C6|90 999 Span<byte>[0] 0x|00|00|01|F2|8F|BD|C6|98 11 Span<int> [0] offset by size_t
0x|00|00|01|F2|8F|BD|C6|98 11 Span<byte>[0] offset by size_t 0x|00|00|01|F2|8F|BD|BF|38 999 Span<int> [0] after GC
0x|00|00|01|F2|8F|BD|BF|38 999 Span<byte>[0] after GC [ 999, 123, 11, -100 ]
[ 231, 3, 0, 0, 123, 0, 0, 0, 11, 0, 0, 0, 156, 255, 255, 255 ]

После уплотнения, оба спэна меняют свой указатель. Все работает, и работает стабильно, насколько я смог убедиться. Но как теперь заставить работать это в легаси-проекте? Отлично!

Иными словами, netcore умеет создавать внутренние указатели даже на фрагмент массива и корректно обновлять ссылки, когда GC его шевелит. Я абсолютно забыл, что поддержка спэнов реализована в netcore через интринсик. По сути, у нас есть два разных спэна: один создается из массива и отслеживает свои ссылки, второй — из указателя и понятия не имеет, на что он указывает. В netframework же nuget-реализация спэна — это костыль. Для сравнения, это примерная реализация спэна в netcore: После перемещения исходного массива, спэн-указатель продолжает указывать туда, куда указывал указатель, переданный в его конструктор.

readonly ref struct Span<T> where T : unmanaged
{ private readonly ByReference<T> _pointer; // Внутри - магия интринсика private readonly int _length;
}

и в netframework:

readonly ref struct Span<T> where T : unmanaged
{ private readonly Pinnable<T> _pinnable; private readonly IntPtr _byteOffset; private readonly int _length;
}

Если в конструктор передать указатель void*, его просто преобразуют в абсолютный _byteOffset. _pinnable содержит ссылку на массив, если таковой был передан в конструктор, _byteOffset содержит сдвиг (даже спэн по всему массиву имеет некий ненулевой сдвиг, связанный с тем, как массив представлен в памяти, наверное). Что делать в такой ситуации? Спэн будет прибит намертво к участку памяти, а все инстанс методы изобилуют условиями типа if(_pinnable is null) {/* верни по указателю */} else {/* посчитай сдвиг от _pinnable */}.

Этот раздел посвящен различным реализациям, поддерживаемым netframework, которые позволяют осуществить каст Span<T> -> Span<U>, сохраняя все нужные ссылки.
Предупреждаю: это зона ненормального программирования с, возможно, фундаментальными ошибками и Undefined Behavior в конце

Метод 1: Наивный

Нам нужно значение _pinnable. Как показал пример, преобразование указателей не даст нужного результата на netframework. Есть только одна ма-аленькая проблема: спэн это ref struct, он не может быть ни аргументом дженерика, ни упакованным в object. Окей, расчехлим рефлексию, вытащим приватные поля (очень плохо и не всегда возможно), запишем в новый спэн, порадуемся. Простого способа (еще и учитывая рефлексию по приватным полям) я не нашел. Стандартные методы рефлексии потребуют, так или иначе, запихать спэн в ссылочный тип.

Метод 2: We need to go deeper

Спэн — структура, вне зависимости от T три поля занимают одинаковое количество памяти (на одной архитектуре). Все уже было сделано до меня ([1], [2], [3]). Сказано — сделано. А что если [FieldOffset(0)]?

[StructLayout(LayoutKind.Explicit)]
ref struct Exchange<T, U> where T : unmanaged where U : unmanaged { [FieldOffset(0)] public Span<T> Span_1; [FieldOffset(0)] public Span<U> Span_2; }

Explicit. Но при запуске программы (а точнее при попытке использовать тип) нас встречает TypeLoadException — дженерик не может быть LayoutKind. Окей, не беда, пойдем по сложному пути:

[StructLayout(LayoutKind.Explicit)]
public ref struct Exchange
{ [FieldOffset(0)] public Span<byte> ByteSpan; [FieldOffset(0)] public Span<sbyte> SByteSpan; [FieldOffset(0)] public Span<ushort> UShortSpan; [FieldOffset(0)] public Span<short> ShortSpan; [FieldOffset(0)] public Span<uint> UIntSpan; [FieldOffset(0)] public Span<int> IntSpan; [FieldOffset(0)] public Span<ulong> ULongSpan; [FieldOffset(0)] public Span<long> LongSpan; [FieldOffset(0)] public Span<float> FloatSpan; [FieldOffset(0)] public Span<double> DoubleSpan; [FieldOffset(0)] public Span<char> CharSpan;
}

Теперь можно сделать так:

private static Span<byte> As2(Span<int> span)
{ var exchange = new Exchange() { IntSpan = span }; return exchange.ByteSpan;
}

Метод работает с одной лишь проблемой — поле _length копируется как есть, поэтому при касте int -> byte длина байт-спэна в 4 раза меньше реального массива.
Не проблема:

[StructLayout(LayoutKind.Sequential)]
public ref struct Raw
{ public object Pinnable; public IntPtr Pointer; public int Length;
}
[StructLayout(LayoutKind.Explicit)]
public ref struct Exchange
{ /* */ [FieldOffset(0)] public Raw RawView;
}

Теперь через RawView можно получить доступ к каждому отдельному полю спэна.

private static Span<byte> As2(Span<int> span)
{ var exchange = new Exchange() { IntSpan = span }; var exchange2 = new Exchange() { RawView = new Raw() { Pinnable = exchange.RawView.Pinnable, Pointer = exchange.RawView.Pointer, Length = exchange.RawView.Length * sizeof<int> / sizeof<byte> } }; return exchange2.ByteSpan;
}

Минус — дженерик версию конвертера создать нельзя, придется довольствоваться предопределенными типами. И это работает так, как надо, если игнорировать применение грязных трюков.

Метод 3: Безумный

Необходимость написания конвертеров для любой пары unmanaged типов меня не радовала. Как и любой нормальный программист, я люблю автоматизировать вещи. Правильно, заставить CLR написать код за вас. Какое решение можно предложить?

Есть разные способы, есть статьи. Как этого добиться? В моем случае это был dotPeek.
Создание type builder выглядит примерно так: Если коротко, процесс выглядит так:
Создаем билдер сборки -> создаем билдер модуля -> строим тип -> {Поля, Методы, и т.д.} -> на выходе получаем инстанс типа Type.
Чтобы точно понять, как должен выглядеть тип (ведь это ref struct), используем любой инструмент типа ildasm.

var typeBuilder = _mBuilder.DefineType($"Generated_{typeof(T).Name}", TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.ExplicitLayout // <- Вот это важно | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, typeof(ValueType));

Так как напрямую скопировать Span<T> в Span<U> из-за разницы длин мы не можем, нам нужно на каждый каст создавать по два типа вида Теперь — поля.

[StructLayout(LayoutKind.Explicit)]
ref struct Generated_Int32
{ [FieldOffset(0)] public Span<Int32> Span; [FieldOffset(0)] public Raw Raw;
}

Не забываем про IsByRefLikeAttribute. Здесь Raw мы можем объявить руками и переиспользовать. С полями все просто:

var spanField = typeBuilder.DefineField("Span", typeof(Span<T>), FieldAttributes.Private);
spanField.SetOffset(0);
var rawField = typeBuilder.DefineField("Raw", typeof(Raw), FieldAttributes.Private);
rawField.SetOffset(0);

Теперь кэшируем сборку, модуль. На этом все, простейший тип готов. Создаем обертку, которая по двум типам TIn и TOut генерирует два типа-хэлпера и выполняет нужные операции над спэнами. Кастомные типы кэшируем, например, в словарь (T -> Generated_{nameof(T)}). Как и в случае с рефлексией, использовать ее на спэнах (или на других ref struct) практически невозможно. Есть одно но. Как же быть? Либо я не нашел простого решения.

Delegates to the rescue

Методы рефлексии выглядят обычно примерно так:

object Invoke(this MethodInfo mi, object @this, object[] otherArgs)

Давайте представим что у типа есть геттер и сеттер методы (не свойства, а вручную созданные простейшие методы).
Например: Они не несут в себе информацию о типах, поэтому если боксинг (= упаковка) для вас приемлемы — проблем нет.
В нашем случае, @this и otherArgs должны содержать в себе ref struct, что мне обойти не удалось.
Однако есть способ проще.

void Generated_Int32.SetSpan(Span<Int32> span) => this.Span = span;

В дополнение к методу мы можем объявить тип делегата (явно в коде):

delegate void SpanSetterDelegate<T>(Span<T> span) where T : unmanaged;

SpanSetterDelegate, однако, абсолютно валидный делегат.
Создадим себе нужные делегаты. На такое нам приходится идти, потому что стандартный экшен должен был бы иметь сигнатуру Action<Span<T>>, но спэны нельзя использовать как дженерик-аргументы. Для этого нужно провести стандартные манипуляции:

var mi = type.GetMethod("Method_Name"); // Предполагая, что наш метод public & instance
var spanSetter = (SpanSetterDelegate<T>) mi.CreateDelegate(typeof(SpanSetterDelegate<T>), @this);

Empty);. Теперь spanSetter можно использовать как, например, spanSetter(Span<T>. CreateInstance(type), ведь у структуры есть дефолтный конструктор без аргументов. Что до @this2, то это инстанс нашего динамического типа, созданный, разумеется, через Activator.

Итак, последний рубеж — нам нужно динамически сгенерировать методы.

CreateInstance() упаковывает ref struct-инстанс. 2 Можно обратить внимание, что здесь происходит что-то не то — Activator. конец следующего раздела. См.

Знакомьтесь, Reflection.Emit

тела наших тривиальных геттеров/сеттеров состоят из буквально пары выражений. Я думаю, что методы можно было бы сгенерировать, используя Expression, т.к. Я же выбрал другой, более прямолинейный подход.

8) Если посмотреть на IL-код тривиального геттера, то можно увидеть что-то типа (Debug, X86, netframework4.

nop
ldarg.0
ldfld /* что-то */
stloc.0
br.s /* адрес */
ldloc.0
ret

Здесь куча мест для остановки и отладки.
В релизной же версии остается только самое важное:

ldarg.0
ldfld /* что-то */
ret

Таким образом, в IL написано следующее:
1) Загрузи this
2) Загрузи значение поля
3) Верни Нулевым аргументом инстанс-метода является… this.

В Reflection. Просто, да? Как раз такой, как мы получали ранее, например spanField. Emit есть специальная перегрузка, принимающая, кроме оп-кода, еще и параметр-дескриптор поля.

var getSpan = type.DefineMethod("GetSpan", MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.Standard, typeof(Span<T>), Array.Empty<Type>()); gen = getSpan.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, spanField);
gen.Emit(OpCodes.Ret);

Для сеттера немного сложнее, нужно загрузить на стэк this, загрузить первый аргумент функции, затем вызвать инструкцию записи в поле и вернуть ничего:

ldarg.0
ldarg.1
stfld /* идентификатор поля */
ret

Проделав такую процедуру и для поля Raw, объявив нужные делегаты (или использовав стандартные), получаем динамический тип и четыре метода-аксессора, из которых сгенерированы корректные дженерик-делегаты.

Пишем класс-обертку, который по двум дженерик-параметрам (TIn, TOut) получает инстансы типа Type, ссылающиеся на соответствующие (закэширвоанные) динамические типы, после чего, создает по одному объекту каждого типа, и генерирует четыре дженерик-делегата, а именно

  1. void SetSpan(Span<TIn> span) чтобы записать исходный спэн в структуру
  2. Raw GetRaw() чтобы считать содержимое спэна как Raw-структуру
  3. void SetRaw(Raw raw) чтобы записать модифицированную Raw структуру во второй объект
  4. Span<TOut> GetSpan() чтобы вернуть спэн желаемого типа с корректно выставленными и пересчитанными полями.

При создании делегата ссылка на эти объекты передается как параметр @this. Интересно что инстансы динамических типов нужно создать один раз. Activator. Здесь происходит нарушение правил. По всей видимости связано это с тем, что сам по себе динамический тип не получился ref-like (type. CreateInstance возвращает object. Видимо, такое ограничение присутствует в языке, но CLR это переваривает. IsByRefLike == false), однако ref-like поля создать удалось. Возможно, именно здесь будут простреливаться колени в случае нестандартного использования. 3

Делегаты и структуры можно переиспользовать при выполнении одинаковых кастов подряд. Итак, получаем инстанс дженерик типа, содержащий в себе четыре делегата и две неявные ссылки на инстансы динамических классов. Для пущего быстродействия, снова кэшируем (уже тип-конвертер) по паре (TIn, TOut) -> Generator<TIn, TOut>.

Штрих последний: приводим типы, Span<TIn> -> Span<TOut>

public Span<TOut> Cast(Span<TIn> span)
{ // Быстрый путь если спэн пуст if (span.IsEmpty) return Span<TOut>.Empty; // Caller ответственен за то, чтобы размеры спэнов совпадали в байтах if (span.Length * Unsafe.SizeOf<TIn>() % Unsafe.SizeOf<TOut>() != 0) throw new InvalidOperationException(); // Загружаем спэн в первую структуру // Span<TIn> _input.Span = span; _spanSetter(span); // Считываем Raw // Raw raw = _input.Raw; var raw = _rawGetter(); var newRaw = new Raw() { Pinnable = raw.Pinnable, // Вточности тот же Pinnable Pointer = raw.Pointer, // Идентичный сдвиг Length = raw.Length * Unsafe.SizeOf<TIn>() / Unsafe.SizeOf<TOut>() // Новая длина }; // Загружаем новый Raw во вторую структуру // Raw _output.Raw = newRaw; _rawSetter(newRaw); // Вытаскиваем спэн нужного типа // Span<TOut> _output.Span return _spanGetter();
}

Разумеется, на свой страх и риск. Иногда — ради спортивного интереса — можно обойти некоторые ограничения языка и реализовать нестандартный функционал. Очевидным минусом является необходимость рефлексии и генерации типов. Стоит отметить, что динамический метод позволяет полностью отказаться от указателей и unsafe / fixed контекста, что может быть бонусом.

Результаты наивного бенчмарка

А насколько это все быстро?
Я сравнил скорость кастов в глупом сценарии, который не отражает реальное/потенциальное использование таких кастов и спэнов, но хотя бы дает представление о скорости.

  1. Cast_Explicit использует преобразование через явно декларируемый тип, как в Методе 2. Каждый каст требует аллокации двух небольших структур и доступов к полям;
  2. Cast_IL реализует Метод 3, но каждый раз заново создает экземпляр Generator<TIn, TOut>, что приводит к постоянным поискам по словарям, после того как первый проход генерирует все типы;
  3. Cast_IL_Cached кэширует непосредственно инстанс конвертера Generator<TIn, TOut>, из-за чего в среднем оказывается быстрее, т.к. весь каст сводится к вызовам четырех делегатов;
  4. Buffer достигает аналогичного функционала, копируя побайтово исходный массив в массив байтов, после чего выполняя аналогичный пэйлоад. Целевой массив байтов всегда переиспользуется.

В качестве пэйлоада — подсчет суммы байтов в байтовом представлении части массива int[N] размера порядка N/2 со случайным сдвигом.

Каст спэнов с помощью кэшированного конвертера не сильно отстает от жадной реализации через копирование, выигрывая у двух других реализаций за счет отсутствия аллокаций и поиска по словарям. Из таблицы следует, что копирование данных в соседний массив оказывается быстрее, чем пляски со спэнами. Таким образом, можно почти эффективно немножечко нарушая правила игры модифицировать байтовое представление элементов unmanaged типа без этих ваших указателей. В общем и целом, если первый вызов каста однозначно будет долгим за счет рефлексии, последующие вызовы преобразования из и в уже известные типы происходят довольно быстро.

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
Intel Core i7-2700K CPU 3.50GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.8.3815.0 Job=Clr Runtime=Clr InvocationCount=1 UnrollFactor=1

Acknowledgements

Спасибо BenchmarkDotNet за BenchmarkDotNet, youtube-каналам NDC Conferences и DotNext за доступ к докладам, и вам, за то что потратили время и прочитали до конца. Спасибо JetBrains (у вас классный офис в СПБ :-)) и команде R# за отличные инструменты VS и standalone-приложение dotPeek, а также за студенческую лицензию.

Работа над ошибками

Решением этой проблемы (и проблемы упаковки спэна) может быть следующим. 3 Уже после написания статьи я осознал проблему с тем, что динамический тип оказался не ref, и что это не совсем то, что было обещано. Учитывая все ограничения ref structs, можно генерировать вместо аксессоров статические методы вида

static Raw Generated_Int32.GetRaw(Span<int> span)
{ var inst = new Generated_Int32() { Span = span }; return inst.Raw;
}

Emit. Это все еще валидный код, который можно записать через Reflection. DeclareLocal. Здесь потребуется уже целая локальная переменная, но нам должен помочь ILGenerator. Добавив в пару метод

static Span<int> Generated_Int32.GetSpan(Raw raw);

и пару делегатов

delegate Raw GetRaw<T>(Span<T> span) where T : unmanaged;
delegate Span<T> GetSpan<T>(Raw raw) where T : unmanaged;

Т.к. можно, я полагаю, добиться корректно работающего кода в случае ref — структур. статические методы не требуют инстансов, генерация делегатов будет выглядеть как

var getter = type.GetMethod(@"GetRaw", BindingFlags.Static | BindingFlags.Public).CreateDelegate(typeof(GetRaw<T>), null) as GetRaw<T>;

а вызов — как

Raw raw = getter(Span<TIn>.Empty);
Raw newRaw = convert(raw);
Span<TOut> = setter(newRaw);

UPD01: Борьба с очепятками

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть