Comments 16

На самом деле, нарушение ref соглашения означает, что Span<T> оказывается в куче. С учетом того, что как динамические типы, так и инстансы — кэшируются, мы получаем классическую утечку памяти. Если спэн построен поверх массива, в нем (или, что проще, в Raw.Pinnable) остается, вероятно, ссылка на массив, из-за чего он весь "утекает". Обнуление инстансов динамических типов в конце метода Cast скорее всего решит эту проблему. Если же на кучу утекакет спэн-указатель, мы в итоге получаем потенциально указатель на невалидный участок памяти (но, скорее всего, никакой утечки).


Решение этой проблемы через стаические методы и полную изоляцию от использования инстансов динамического типа в пользовательском коде — скорее всего тема отедльной статьи.

Несмотря на тот факт, что поддержка спэнов в BCL netframework практически полностью отсутствует

Это в netstandard2.0. В netstandard2.1 или netcore2.1 и более с поддержкой спаном все лучше. В частности, их можно юзать в методах Parse и TryParse.

Именно ограчичения netstandard2.0 и заставили меня копнуть эту тему глубже.

А вы не пробовали перед тем, как делать безумные хаки с LayoutKind.Explicit, просто сгенерировать DynamicMethod? По идее, динамические методы же имеют доступ к приватным членам класса...

Не пробовал, так как не знал. Быстрый поиск показывает, что динамический метод действительно можно ассоциировать с типом, такой метод получает доступ вообще ко всем внутренностям. Я вижу здесь несколько проблем:


  1. RestrictedMemberAccess. Не ясно, как он влияет на [StructLayout]. Возможно, явный лэйаут позволяет это обойти.
  2. Могут возникнуть проблемы с преобразованием поля Pinnable<T> в Pinnable<U>.
  3. Мне неизвестны ограничения на применение динамического метода к типу. Для совсем простого решения в лоб необходим Span<TOther> Span<T>.Cast<TOther>() с доступом к приватным членам как Span<T>, так и Span<TOther>. Если это решаемо — это действительно может ускорить процесс, хотя все равно потребует генерации методов на каждую используемую пару типов и написание кода в IL.

Нарушение инкапсуляции через [StructLayout] мне показалось более простым, т.к. для не-дженерик случая все можно сделать руками, полностью избежав кодогенерации.

RestrictedMemberAccess. Не ясно, как он влияет на [StructLayout]. Возможно, явный лэйаут позволяет это обойти.

А зачем вам вообще [StructLayout] при использовании динамического метода?


Могут возникнуть проблемы с преобразованием поля Pinnable<T> в Pinnable<U>.

Вот тут-то Unsafe.As и пригодится.

Да, вы абсолютно правы, это наиболее простой способ. Единственное тербование — динамический метод должен получить доступ к internal типам и методам соответствующего модуля (т.к. свойства .Pinnable и .ByteOffset у Spaninternal, как и сам тип Pinnable<T>)

Доступ есть:


The dynamic method created with this constructor has access to all members of the type owner, and to public and internal (Friend in Visual Basic) members of all the other types in the module that contains owner.

Это не говоря уже о варианте restrictedSkipVisibility: true, позволяющем вообще пропускать любые проверки видимости.

Не смотрели на возможности метода Unsafe.As? Span-то через него реализован, значит если Span есть — то и Unsafe.As должен быть доступен.

Смотрел. Есть два достунпых метода. ref T Unsafe.As<T>(object o), работающий для ссылочных типов, и ref TOut Unsafe.As<TIn, TOut>(ref TIn input), который вроде бы должен подойти. Проблема в том, что ref byte b = Unasfe<int, byte>(ref intSpan[0]) дает вам ref byte, а не Span<byte>, что уже хорошо, потому что с помощью того же Unsafe можно перемещаться и читать, однако прямого способа реконструировать Span<byte> из ref byte я не знаю. Если вы воспользуетесь голым указателем из ref byte, на netframework вы столкнетесь с проблемой, о чем, собственно, и идет речь.

Кажется я чего-то не понимаю. Если вам нужно работать с массивом произвольного типа, как с массивом байт, то почему не использовать метод Span<TTo> Cast<TFrom,TTo> (Span<TFrom> span) из типа MemoryMarshal?
Я так делаю, когда нужно представить массив byte в виде массива short, чтобы пихать short'ы в энкодер опуса.

Объяснение простое. Я исполльзую Span<T> в netframewrok проектах, которые ограничены netstandard2.0. MemoryMarshal доступен для netstnadard2.1 и совместимых реализаций, но отсутствует в полноценном фрэймворке (или я что-то делаю не так?). В конце концов, элементарный каст через указатели, реализованный руками, корректно работает в netcore, где есть поддержка стандарта 2.1+. Однако в netframework это не работает (как и MemoryMarshal) и вряд ли заработает из-за реализации спэнов.

А его точно нельзя из пакета поставить? Реализация-то конкретно у метода Cast простейшая:


    checked
    {
        int length = (int)(((long)span.Length) * (long)Unsafe.SizeOf<TFrom>()) / Unsafe.SizeOf<TTo>());
        return new Span<TTo>(Unsafe.As<Pinnable<TTo>>(span.Pinnable), span.ByteOffset, length);
    }

Вроде System.Memory имеет реализацию под .netstandard 1.1

Реализация по своей сути такая же, единственная проблема — для меня — поля (кроме Length) приватны, а конструктор — кажется internal. Напрямую рефлексией это не решить (fieldInfo.GetValue(Span<T>) не скомпилируется), однако динамический метод, предложенный выше, скорее всего решит все проблемы.
Ах да, сам Pinnable<T> тоже не является публичным, что только усложняет задачу (на мой взгляд).
В любом случае, этот эксперимент позволяет почувствовать разницу между рантаймами и пощупать ограничения ref strcut, которые на самом деле не такие уж и жесткие.

Хм, может я чего то не понял, чтоб поменять индейность массива чисел на little endian платформе достаточно for(var i = 0; i < arr.Length; i++) arr[i] = IPAddress.HostToNetworkOrder(arr[i]);
Only those users with full accounts are able to leave comments. Log in, please.