[DotNetBook] Span: новый тип данных .NET

  • Tutorial

С этой статьей я продолжаю публиковать целую серию статей, результатом которой будет книга по работе .NET CLR, и .NET в целом (уже готово около 200 страниц книги, так что добро пожаловать в конец статьи за ссылками).


Как язык, так и платформа существуют уже много лет: и все это время существовало множество средств для работы с неуправляемым кодом. Так почему же сейчас выходит очередной API для работы с неуправляемым кодом если по сути он существовал уже много-много лет? Для того чтобы ответить на этот вопрос достаточно понять чего не хватало нам раньше.


Разработчики платформы и раньше пытались нам помочь скрасить будни разработки с использованием неуправляемых ресурсов: это и автоматические врапперы для импортируемых методов. И маршаллинг, который в большинстве случаев работатет автоматически. Это также инструкция stackallloc, о которой говорится в главе про стек потока. Однако, как по мне если ранние разработчики с использованием языка C# приходили из мира C++ (как сделал это и я), то сейчас они приходят из более высокоуровневых языков (я, например, знаю разработчика, который пришел из JavaScript). А что это означает? Это означает что люди со все большим подозрением начинают относиться к неуправляемым ресурсам и конструкциям, близким по духу к C/C++ и уж тем более — к языку Ассемблера.


Примечание


Глава, опубликованная на Хабре не обновляется и возможно, уже несколько устарела. А потому, прошу обратиться за более свежим текстом к оригиналу:



Как результат такого отношения — все меньшее и меньшее содержание unsafe кода в проектах и все большее доверие к API самой платформы. Это легко проверяется если поискать использование конструкции stackalloc по открытым репозиториям: оно ничтожно мало. Но если взять любой код, который его использует:


Класс Interop.ReadDir
/src/mscorlib/shared/Interop/Unix/System.Native/Interop.ReadDir.cs


unsafe
{
    // s_readBufferSize is zero when the native implementation does not support reading into a buffer.
    byte* buffer = stackalloc byte[s_readBufferSize];
    InternalDirectoryEntry temp;
    int ret = ReadDirR(dir.DangerousGetHandle(), buffer, s_readBufferSize, out temp);
    // We copy data into DirectoryEntry to ensure there are no dangling references.
    outputEntry = ret == 0 ?
            new DirectoryEntry() { 
               InodeName = GetDirectoryEntryName(temp), InodeType = temp.InodeType 
            } 
            : default(DirectoryEntry);

    return ret;
}

Становится понятна причина непопулярности. Посмотрите не вчитываясь на код и ответьте для себя на один вопрос: доверяете ли вы ему? Могу предположить что ответом будет "нет". Тогда ответьте на другой: почему? Ответ будет очевиден: помимо того что мы видим слово Dangerous, которое как-бы намекает что что-то может пойти не так, второй фактор, влияющий на наше отношение — это строчка byte* buffer = stackalloc byte[s_readBufferSize];, а если еще конкретнее — byte*. Эта запись — триггер для любого чтобы в голове появилась мысль: "а что, по-другому сделать нельзя было что-ли?". Тогда давайте еще чуть-чуть разберемся с психоанализом: отчего может возникнуть подобная мысль? С одной стороны мы пользуемся конструкциями языка и предложенный здесь синтаксис далек от, например, C++/CLI, который позволяет делать вообще все что угодно (в том числе делать вставки на чистом Assembler), а с другой он выглядит непривычно.


Так в чем же вопрос? Как вернуть разработчиков обратно в лоно неуправляемого кода? Необходимо дать им чувство спокойствия что они не могут сделать ошибку случайно, по незнанию. Итак, для чего же введены типы Span<T> и Memory<T>?


Span[T], ReadOnlySpan[T]


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


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


    var array = new [] {1,2,3,4,5,6};
    var span = new Span<int>(array, 1, 3);
    var position = span.BinarySearch(3);
    Console.WriteLine(span[position]);  // -> 3

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


Тут мы видим первое свойство этого типа данных: это создание некоторого контекста. Давайте разовьем нашу идею с контекстами:


void Main()
{
    var array = new [] {'1','2','3','4','5','6'};
    var span = new Span<char>(array, 1, 3);
    if(TryParseInt32(span, out var res))
    {
        Console.WriteLine(res);
    }
    else
    {
        Console.WriteLine("Failed to parse");
    }
}

public bool TryParseInt32(Span<char> input, out int result)
{
    result = 0;
    for (int i = 0; i < input.Length; i++)
    {
        if(input[i] < '0' || input[i] > '9')
            return false;
    result = result * 10 + ((int)input[i] - '0');
    }
    return true;
}

-----
234

Как мы видим, Span<T> вводит абстракцию доступа к некоторому участку памяти как на чтение так и на запись. Что нам это дает? Если вспомнить, на основе чего еще может быть сделан Span, то мы вспомним как про неуправляемые ресурсы, так и про строки:


// Managed array
var array = new[] { '1', '2', '3', '4', '5', '6' };
var arrSpan = new Span<char>(array, 1, 3);
if (TryParseInt32(arrSpan, out var res1))
{
    Console.WriteLine(res1);
}

// String
var srcString = "123456";
var strSpan = srcString.AsSpan().Slice(1, 3);
if (TryParseInt32(strSpan, out var res2))
{
    Console.WriteLine(res2);
}

// void *
Span<char> buf = stackalloc char[6];
buf[0] = '1'; buf[1] = '2'; buf[2] = '3';
buf[3] = '4'; buf[4] = '5'; buf[5] = '6';

if (TryParseInt32(buf.Slice(1, 3), out var res3))
{
    Console.WriteLine(res3);
}

-----
234
234
234

Т.е., получается, что Span<T> — это средство унификации по работе с памятью: управляемой и неуправляемой, которое гарантирует безопасность в работе с такого рода данными во время Garbage Collection: если участки памяти с управляемыми массивами начнут двигаться, то для нас это будет безопасно.


Однако, стоит ли так сильно радоваться? Можно ли было всего этого добиться и раньше? Например, если говорить об управляемых массивах, то тут даже сомневаться не приходится: достаточно просто обернуть массив в еще один класс, предоставив аналогичный интерфейс и все готово. Мало того, аналогичную операцию можно проделать и со строками: они обладают необходимыми методами. Опять же, достаточно строку завернуть в точно такой же тип и предоставить методы по работе с ней. Другое дело что для того чтобы хранить в одном типе строку, буфер или массив, придется сильно повозиться, храня в едином экземпляре ссылки на каждый из возможных вариантов (активным, понятное дело, будет только один):


public readonly ref struct OurSpan<T>
{
    private T[] _array;
    private string _str;
    private T * _buffer;

    // ...
}

Или же если отталкиваться от архитектуры, то делать три типа, наследующих единый интерфейс. Получается, что для того чтобы сделать средство единого интерфейса между этими типами данных managed, сохранив при этом максимальную производительность, отличного от Span<T> пути не существует.


Далее, если продолжить рассуждения, то что такое ref struct в понятиях Span? Это именно те самые "структуры, они только на стеке", о которых мы так часто слышим на собеседованиях. А это значит, что этот тип данных может идти только через стек и не имеет права уходить в кучу. А потому Span, будучи ref структурой, является контекстным типом данных, обеспечивающим работу методов, но не объектов в памяти. От этого для его понимания и надо отталкиваться.


Отсюда мы можем сформулировать определение типа Span и связанного с ним readonly типа ReadOnlySpan:


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

И действительно: если мы имеем примерно такой код:


public void Method1(Span<byte> buffer)
{
    buffer[0] = 0;
    Method2(buffer.Slice(1,2));
}
Method2(Span<byte> buffer)
{
    buffer[0] = 0;
    Method3(buffer.Slice(1,1));
}
Method3(Span<byte> buffer)
{
    buffer[0] = 0;
}

то скорость доступа к исходному буферу будет максимально высокой: вы работаете не с managed объектом, а с managed указателем. Т.е. не с .NET managed типом, а с unsafe типом, заключенным в managed оболочку.


Span[T] на примерах


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


ValueStringBuilder


Одним из самых алгоритмически интересных примеров является тип ValueStringBuilder, который прикопан где-то в недрах mscorlib и почему-то как и многие другие интереснейшие типы данных помечен модификатором internal, что означает что если бы не исследование исходного кода mscorlib, о таком замечательном способе оптимизации мы бы никогда не узнали.


Каков основной минус системного типа StringBuilder? Это конечно же его суть: как он сам, так и то, на чем он основан (а это массив символов char[]) — являются типами ссылочными. А это значит как минимум две вещи: мы все равно (хоть и немного) нагружаем кучу и второе — увеличиваем шансы промаха по кэшам процессора.


Еще один вопрос, который у меня возникал к StringBuilder — это формирование маленьких строк. Т.е. когда результирующая строка "зуб даю" будет короткой: например, менее 100 символов. Когда мы имеем достаточно короткие форматирования, к производительности возникают вопросы:


    $"{x} is in range [{min};{max}]"

Насколько эта запись хуже чем ручное формирование через StringBuilder? Ответ далеко не всегда очевиден: все сильно зависит от места формирования: как часто будет вызван данный метод. Ведь сначала string.Format выделяет память под внутренний StringBuilder, который создаст массив символов (SourceString.Length + args.Length * 8) и если в процессе формирования массива выяснится, что длина не была угадана, то для формирования продолжения будет создан еще один StringBuilder, формируя тем самым односвязный список. А в результате — необходимо будет вернуть сформированную строку: а это еще одно копирование. Транжирство и расточительство. Вот если бы избавиться от размещения в куче первого массива формируемой строки, было бы замечательно: от одной проблемы мы бы точно избавились.


Взглянем на тип из недр mscorlib:


Класс ValueStringBuilder
/src/mscorlib/shared/System/Text/ValueStringBuilder


    internal ref struct ValueStringBuilder
    {
        // это поле будет активно если у нас слишком много символов
        private char[] _arrayToReturnToPool;
        // это поле будет основным
        private Span<char> _chars;
        private int _pos;
        // тип принимает буфер извне, делигируя выбор его размера вызывающей стороне
        public ValueStringBuilder(Span<char> initialBuffer)
        {
            _arrayToReturnToPool = null;
            _chars = initialBuffer;
            _pos = 0;
        }

        public int Length
        {
            get => _pos;
            set
            {
                int delta = value - _pos;
                if (delta > 0)
                {
                    Append('\0', delta);
                }
                else
                {
                    _pos = value;
                }
            }
        }

        // Получение строки - копирование символов из массива в массив
        public override string ToString()
        {
            var s = new string(_chars.Slice(0, _pos));
            Clear();
            return s;
        }

        // Вставка в середину сопровождается развиганием символов
        // исходной строки чтобы вставить необходимый: путем копирования
        public void Insert(int index, char value, int count)
        {
            if (_pos > _chars.Length - count)
            {
                Grow(count);
            }

            int remaining = _pos - index;
            _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count));
            _chars.Slice(index, count).Fill(value);
            _pos += count;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Append(char c)
        {
            int pos = _pos;
            if (pos < _chars.Length)
            {
                _chars[pos] = c;
                _pos = pos + 1;
            }
            else
            {
                GrowAndAppend(c);
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void GrowAndAppend(char c)
        {
            Grow(1);
            Append(c);
        }

        // Если исходного массива, переданного конструктором не хватило
        // мы выделяем массив из пула свободных необходимого размера
        // На самом деле идеально было бы если бы алгоритм дополнительно создавал
        // дискретность в размерах массивов чтобы пул не был бы фрагментированным
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void Grow(int requiredAdditionalCapacity)
        {
            Debug.Assert(requiredAdditionalCapacity > _chars.Length - _pos);

            char[] poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2));

            _chars.CopyTo(poolArray);

            char[] toReturn = _arrayToReturnToPool;
            _chars = _arrayToReturnToPool = poolArray;
            if (toReturn != null)
            {
                ArrayPool<char>.Shared.Return(toReturn);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void Clear()
        {
            char[] toReturn = _arrayToReturnToPool;
            this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
            if (toReturn != null)
            {
                ArrayPool<char>.Shared.Return(toReturn);
            }
        }

        // Пропущенные методы: с ними и так все ясно
        private void AppendSlow(string s);
        public bool TryCopyTo(Span<char> destination, out int charsWritten);
        public void Append(string s);
        public void Append(char c, int count);
        public unsafe void Append(char* value, int length);
        public Span<char> AppendSpan(int length);
    }

Этот класс по своему функционалу сходен со своим старшим собратом StringBuilder, обладая при этом одной интересной и очень важной особенностью: он является значимым типом. Т.е. хранится и передается целиком по значению. А новейший модификатор типа ref, который приписан к сигнатуре объявления типа говорит нам о том что данный тип обладает дополнительным ограничением: он имеет право находиться только на стеке. Т.е. вывод его экмепляров в поля классов приведет к ошибке. К чему все эти приседания? Для ответа на этот вопрос достаточно посмотреть на класс StringBuilder, суть котоого мы только что описали:


Класс StringBuilder /src/mscorlib/src/System/Text/StringBuilder.cs


public sealed class StringBuilder : ISerializable
{
    // A StringBuilder is internally represented as a linked list of blocks each of which holds
    // a chunk of the string.  It turns out string as a whole can also be represented as just a chunk,
    // so that is what we do.
    internal char[] m_ChunkChars;                // The characters in this block
    internal StringBuilder m_ChunkPrevious;      // Link to the block logically before this block
    internal int m_ChunkLength;                  // The index in m_ChunkChars that represent the end of the block
    internal int m_ChunkOffset;                  // The logical offset (sum of all characters in previous blocks)
    internal int m_MaxCapacity = 0;

    // ...

    internal const int DefaultCapacity = 16;

StringBuilder — это класс, внутри которого находится ссылка на массив символов. Т.е. когда вы создаете его то по сути создается как минимум два объекта: сам StringBuilder и массив символов в как минимум 16 символов (кстати именно поэтому так важно задавать предполагаемую длину строки: ее построение будет идти через генерацию односвязного списка 16-символьных массивов. Согласитесь, расточительство). Что это значит в контексте нашего разговора о типе ValueStringBuilder: capacity по-умолчанию отсутствует, т.к. он заимствует память извне плюс он сам является значимым типом и заставляет пользователя расположить буфер для символов на стеке. Как итог весь экземпляр типа помещается на стек вместе с его содержимым и вопрос оптимизации здесь становится решенным. Нет выделения памяти в куче? Нет проблем с проседанием производительности по куче. Но вы мне скажите: почему тогда не пользоваться ValueStringBuilder (или его самописной версией: сам он internal и нам не доступен) всегда? Ответ такой: надо смотреть на задачу, которая вами решается. Будет ли результирующая строка известного размера? Будет ли она иметь некий известный максимум по длине? Если ответ "да" и если при этом размер строки не выходит за некоторые разумные границы, то можно использовать значимую версию StringBuilder. Иначе, если мы ожидаем длинные строки, переходим на использование обычной версии.


ValueListBuilder


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


Согласитесь: задача очень похожа на задачу ValueStringBuilder. Да и решена она очень похожим образом:


Файл ValueListBuilder.cs


Если говорить прямо, то такие ситуации достаточно частые. Однако раньше мы решали этот вопрос другим способом: создавали List, заполняли его данными и теряли ссылку. Если при этом метод вызывается достаточно часто, возникает печальная ситуация: множество экземпляров класса List повисает в куче, а вместе с ними повисают в куче и массивы, с ними ассоциированные. Теперь эта проблема решена: дополнительных объектов создано не будет. Однако, как и в случае с ValueStringBuilder решена она только для программистов Microsoft: класс имеет модификатор internal.


Правила и практика использования


Для того чтобы окончательно понять суть нового типа данных, необходимо "поиграться" с ним, написав пару-тройку, а лучше — больше методов, его использующих. Однако, основные правила можно почерпнуть уже сейчас:


  • Если ваш метод будет обрабатывать некоторый входящий набор данных, не меняя его размер, можно попробовать остановиться на типе Span. Если при этом не будет модификации этого буфера, то на типе ReadOnlySpan;
  • Если ваш метод будет работать со строками, вычисляя какую-то статистику либо производя синтаксический разбор строки, то ваш метод обязан принимать ReadOnlySpan<char>. Именно обязан: это новое правило. Ведь если вы принимаете строку, тем самым вы заставляете кого-то сделать для вас подстроку
  • Если необходимо в рамках работы метода сделать достаточно короткий массив с данными (скажем, 10Кб максимум), то вы с легкостью можете организовать такой массив при помощи Span<TType> buf = stackalloc TType[size]. Однако, конечно, TType должен быть только значимым типом, т.к. stackalloc работает только со значимыми типами.

В остальных случаях стоит присмотреться либо к Memory либо использовать классические типы данных.


Как работает Span


Дополнительно хотелось бы поговорить о том, как работает Span и что в нем такого примечательного. А поговорить есть о чем: сам тип данных делится на две версии: для .NET Core 2.0+ и для всех остальных.


Файл Span.Fast.cs, .NET Core 2.0


public readonly ref partial struct Span<T>
{
    /// Ссылка на объект .NET или чистый указатель
    internal readonly ByReference<T> _pointer;
    /// Длина буфера данных по указателю
    private readonly int _length;
    // ...
}

Файл ??? [decompiled]


public ref readonly struct Span<T>
{
    private readonly System.Pinnable<T> _pinnable;
    private readonly IntPtr _byteOffset;
    private readonly int _length;
    // ...
}

Все дело в том что большой .NET Framework и .NET Core 1.* не имеют специальным образом измененного сборщика мусора (в отличии от версии .NET Core 2.0+) и потому вынуждены тащить за собой дополнительный указатель: на начало буфера, с которым идет работа. Т.е., получается, что Span внутри себя работает с управляемыми объектами платформы .NET как с неуправляемыми. Взгляните на внутренности второго варианта структуры: там присутствует три поля. Первое поле — это ссылка на managed объект. Второе — смещение относительно начала этого объекта в байтах чтобы получить начало буфера данных (в строках это — буфер с символами char, в массивах — буфер с данными массива). И, наконец, третье поле — количество уложенных друг за другом элементов этого буфера.


Для примера возьмем работу Span для строк:


Файл coreclr::src/System.Private.CoreLib/shared/System/MemoryExtensions.Fast.cs


public static ReadOnlySpan<char> AsSpan(this string text)
{
    if (text == null)
        return default;

    return new ReadOnlySpan<char>(ref text.GetRawStringData(), text.Length);
}

Где string.GetRawStringData() выглядит следующим образом:


Файл с определением полей coreclr::src/System.Private.CoreLib/src/System/String.CoreCLR.cs


Файл с определением GetRawStringData coreclr::src/System.Private.CoreLib/shared/System/String.cs


public sealed partial class String :
    IComparable, IEnumerable, IConvertible, IEnumerable<char>,
    IComparable<string>, IEquatable<string>, ICloneable
{

    //
    // These fields map directly onto the fields in an EE StringObject.  See object.h for the layout.
    //
    [NonSerialized] private int _stringLength;

    // For empty strings, this will be '\0' since
    // strings are both null-terminated and length prefixed
    [NonSerialized] private char _firstChar;

    internal ref char GetRawStringData() => ref _firstChar;
}

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


Та же самая история происходит и с массивами: когда создается Span, то некий код внутри JIT рассчитывает смещение начала данных массива и этим смещением инициализирует Span. А как подсчитать смещения для строк и массивов, мы научились в главе про структуру объектов в памяти.


Span[T] как возвращаемое значение


Несмотря на всю идиллию, связанную со Span, существуют хоть и логичные, но неожиданные ограничения на его возврат из метода. Если взглянуть на следующий код:


unsafe void Main()
{
    var x = GetSpan();
}

public Span<byte> GetSpan()
{
    Span<byte> reff = new byte[100];
    return reff;
}

то все выглядит крайне логично и хорошо. Однако, стоит заменить одну инструкцию другой:


unsafe void Main()
{
    var x = GetSpan();
}

public Span<byte> GetSpan()
{
    Span<byte> reff = stackalloc byte[100];
    return reff;
}

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


Итак, я надеюсь, что вы подумали, построили догадки и предположения, а может даже и поняли причину. Если так, главу про стек потока я по винтикам расписывал не зря. Ведь дав таким образом ссылку на локальные параменные метода, который закончил работу, вы можете вызвать другой метод, дождаться окончания его работы и через x[0.99] прочитать его локальные переменные.


Однако, к счастью, когда мы делаем попытку написать такого рода код, компилятор дает на по рукам, выдав предупреждение: CS8352 Cannot use local 'reff' in this context because it may expose referenced variables outside of their declaration scope и будет прав: ведь если обойти эту ошибку, то возникет возможность, например, находясь в плагине подстроить такую ситуацию что станет возможным украсть чужие пароли или повысить привилегии выполнения нашего плагина.


Если появились вопросы


Если касательно Span<T> появились вопросы, давайте обсудим. Типы данных очень свежие и практически никем не используются, а потому разобрать use cases очень и очень сложно.


Ссылка на всю книгу



Семинары Станислава Сидристого
75.21
CLRium #6: Concurrency & Parallelism
Support the author
Share post

Similar posts

Comments 24

    0
    Вопрос есть, но в сторону: известна ли Вам возможность получить IL code скомпилированного Expression Trees, желательно прямо в Visual Studio (плагин) во время исполнения? Вот эти способы stackoverflow.com/questions/4764242/viewing-the-il-code-generated-from-a-compiled-expression очень неудобные и не совместимые со стандартной библиотекой (нет Save).
      0
      Если честно, не задумывался над этим :)
        0
        В студии не знаю, а вот в windbg — запросто.
          0
          А если имелось в виду получить в самом коде, то можно посмотреть что есть в clrmd, например.
          0
          Мало того, что эти способы неудобные, так они ещё и недостоверные.
          Всё дело в том, что из-за определённых моментов, связанных с безопасностью, код, генерируемый Expression<...>.Compile() и код, который генерится при создании динамической билиотеки, может оказаться разным.
            0
            Это да. Для полноты картины для археологов будующего напишу и что `CompileToMethod` даже некомпилирует во многих случаях ("'CompileToMethod cannot compile constant '..' because it is a non-trivial value, such as a live object. Instead, create an expression tree that can construct this value.'") в тех в которых `LambdaExpression.Compile` — компилирует.
          +1
          Далее, если продолжить рассуждения, то что такое ref struct в понятиях Span?
          Абзац, имхо, нечитабельный.

          В целом, много технических особенностей реализации, но я честно так и не понял, зачем о них знать. Знать надо, когда и как применять Span, как он устроен, а не как он написан в недрах C#.
            0
            Абзац изменю, согласен. А по примерам применения — нет. Тут вся статья в примерах (например, ValueStringBuilder). А про то как он устроен, как по мне, не так и много.
            –2
            Как я понял, посыл статьи, что unsafe пугает, поэтому мы спрячем unsafe за Span. Так вот Span ничем тут помочь не может, все равно, чтобы заалоцировать массив на стеке надо unsafe код, причем спрятать этот unsafe за библиотечным вызовом не получится, так как область видимости метод. Единственный способ, спрятать unsafe сейчас с помощью Span это коллбек, что довольно неуклюже.
            public delegate void Callback(Span span);

            public unsafe void Invoke(Callback callback) {
            Span s = stackalloc double[100];
            callback(s);
            }


            В той же Java это сделано значительно элегантнее, и там заалоцировать массив на стеке задача JVM и escape analysis.

            Span КМК сейчас это просто инструмент для создания АПИ доступных как из перформансного unsafe кода, так и для "обычного" safe кода. Ну, и дешевый способ создавать легковесные слайсы, без аллокаций на хипе. Пока не введут способ без unsafe кода и unsafe struct с fixed аллоцировать на стеке, это все равно игрушка для low level кода. Нужен высокоуровневый инструмент, для работы с массивами на стеке, в Java же он есть.
              +1

              То есть в java нет возможности регулировать выделение в стеке вручную?

                0
                Нет, там на уровне концепции языка нет стека практически. Java программа же не зависит от платформы, виртуальной машины итп. Она живет в виртуальном мире и сильно отвязана от конкретной среды исполнения. Но тот же new Object() может заалоцировать объект хоть в хипе, хоть на стеке, хоть вообще по регистрам раскидать его.
                  +1

                  А как object можно зааллоцировать на стеке, если на уровне концепции языка практически нет стека?

                    0
                    А вас это вообще не должно волновать. Как JIT-компилятор решил, так и будет. Язык скрывает от вас такие мелочи.
                    +1
                    Но тот же new Object() может заалоцировать объект хоть в хипе, хоть на стеке, хоть вообще по регистрам раскидать его.

                    www.beyondjava.net/escape-analysis-java
                    shipilev.net/jvm-anatomy-park/18-scalar-replacement
                    Вот это ваш прорыв в мире jvm?
                    dotnet делает это намного более предсказуемо и стабильно и делал это намного раньше чем появилось в java. Не думаю, что кто либо полагается на ваш new () аллокатор, который даже не может дать предсказуемого результата по выделении памяти в рамках метода. Как микрооптимизация, если сработало — ок, не сработало — сорри. Концептуально на проблемы работы с managed памятью это никак не повлияло — фрагментация, STW паузы и прочие прелести тормознутой managed платформы остались на месте.
                      +1
                      Нет, там на уровне концепции языка нет стека практически. Java программа же не зависит от платформы, виртуальной машины итп.

                      В том то и дело, все дело в языке — если он не имеет апи для поддержки работы с памятью это ограниченность языка, комплилятора и рантайма — не более, а не ограничение наложенной кросплатформенной моделью разработки.
                        0
                        А что работа со стеком платформо зависимая штука или с памятью? Или по вашему программа написанная на С выделяет память на стеке по разному под виндой и линуксом?

                        Вообще-то, да.
                        Есть разные соглашения о вызовах функций.
                        Есть куча процессорных архитектур (x86, x64, ARM, ARM64), в т.ч. с экзотикой в виде little-endian/big-endian.
                        И Java скрывает все эти нюансы от программиста.


                        Посмотрите на кросс-платформенный C++ код — он кишит платформо-зависимыми ifdef-ами, которых в Java нет.


                        Все дело в языке — если он не имеет апи для поддержки работы с памятью это ограниченность языка, комплилятора и рантайма — не более.

                        Это ограниченность by design.

                          0
                          Это ограниченность by design.

                          В чем проблема спрятать эти ifdef в реализации jvm и добавить api в java? Все дело в реализации рнтайма, компилятора, обратной совместимости и языковом дизайне, да и только.

                    +2

                    Дело не только в стеке, но и в возможности работать c native memory не задействуя механизмы CLR и значительно оптимизируясь механизмы выделения памяти, освобождения. в .net нам это апи переписаны многие стандартные net framework вещи — работа с сокетами, parsing api, работа с потоками(TPL) и т.д как результат интенсивные вычисления чувствительные к аллокациям даже из высокоуровневого api делают с производительностью native кода


                    https://www.codeproject.com/Articles/1223361/Benchmarking-NET-Core-SIMD-performance-vs-Intel-IS


                    Для Java это недосягаемые вершины на текущий момент.
                    Кстати можно почитать, что Java умеет делать с массивами на стеке без вмешательства девелопера?

                      0
                      Единственный способ, спрятать unsafe сейчас с помощью Span это коллбек, что довольно неуклюже.
                      public delegate void Callback(Span span);

                      public unsafe void Invoke(Callback callback) {
                      Span s = stackalloc double[100];
                      callback(s);
                      }


                      Еще можно вот так и в отлчии от Java аллокатора дает гарантировнный результат
                      	
                                      static void Main(string[] args)
                      		{
                      			var array = "1,3,5,".AsSpan();
                      			var separator = ",".AsSpan();
                      			Span<int> span = stackalloc int[3];
                      
                      			int idx, parsed = 0;
                      			while ((idx = array.IndexOf(separator)) != -1)
                      			{
                      				var val = int.Parse(array.Slice(0, idx));
                      				array = array.Slice(idx + 1);
                      				span[parsed++] = val;
                      			}
                      
                      			var res = 0;
                      			foreach (var i in span)
                      			{
                      				res += i;
                      			}
                      
                      			Console.WriteLine(res);
                      			Console.ReadLine();
                      		}


                      unsafe не нужен при работе со стеком.
                      –5

                      В шарпе изобрели слайсы? Привет, D!

                        +3
                        Причём здесь D?
                        Или вы думаете, что D — это первый язык, в котором появились слайсы?
                        +1
                        Опечатки?
                        image
                          0
                          Исправил, спасибо!
                          0
                          Большое спасибо за грамотную статью про Span., очень хорошо и понятно пишете!
                          Всегда слежу за новыми фичами c# и изучаю все по мере выхода. Когда появился Span, было сложно сходу понять что он делает, а времени эксперементировать самому как всегда нет) Прочитал много статей, но только после этой все понял и вопрос можно считать закрытым.

                          есть пару вопросов
                          1) пока читал статью ожидал упоминания ArraySegment и так его и не встретил. Видел использование этого класса в системных апи с сокетом, а в .net Core 1.x только они и были доступны — хотелось бы раскрытия вопроса, решает-ли Span проблемы которые решал ArraySegment, и что теперь использовать.

                          2) Поправьте меня если не прав — машина тьюринга описывает программирование на стеке, и куча не является необходимым элементом для создания тьюринг полных языков/программ, она существует чтобы было удобно строить абстракции. Глобальный вопрос состоит в том, возможно-ли использовать силу абстракции ООП и при этом остаться полностью на стеке, ну или хотя бы в той мере, чтобы куча не так сильно влияла на производительность. Самому пока чего-то не хватает чтобы разобраться.

                          Only users with full accounts can post comments. Log in, please.