Обновить

Комментарии 5

Спасибо за статью!

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

Боксинг — это когда struct нужно передать туда, где ждут ссылку на объект. Рантайм выделяет объект на куче и копирует туда значение структуры. Каждый боксинг — это аллокация, и ее видно в колонке Allocated у BenchmarkDotNet.

Вовсе не так. Нет никакой проблемы передать struct по ссылке. Проблема в другом - невозможно выделить на стеке struct, размер которого не знает компилятор. Это важное ограничение стека.

var list = new List<int> { 1, 2, 3 };
 
// Тип переменной - List<int>. foreach берет метод №1.
// Struct-энумератор живет на стеке. Аллокаций: 0.
foreach (var n in list) { }

public struct Enumerator : IEnumerator<T>, IEnumerator
        {
            private readonly List<T> _list;
            private readonly int _version;

            private int _index;
            private T? _current;
  ...
}

Здесь конкретный struct размер которого понятен компилятору. Размер T будет определен для каждого конкретного типа, остальные размеры фиксированные. Можно выделять в стеке без проблем.

// Тип переменной - IEnumerable<int>. foreach берет метод №2.
// Тот же struct, но боксированный на кучу.
// Аллокаций: 1 объект на каждый foreach.
IEnumerable<int> seq = list;
foreach (var n in seq) { }

public interface IEnumerator<out T> : IDisposable, IEnumerator
    where T : allows ref struct
{
    new T Current
    {
        get;
    }
}

А вот здесь уже, все что знает компилятор так это размер T. В остальное в зависимости от реализации может гулять. Нельзя формировать стек размер которого будут гулять.

Именно по этому, в данном случае, компилятор проводит боксинг т.е. делает из неизвестного размера struct известный - размер ссылки и решает эту проблему.

И это проклятье любого интерфейса за которым спрятан struct. Именно поэтому struct за дженериком ограниченный интерфейсом и не вызывает аллокаций а за интерфейсом вызывает.

боксинг не про размер! Даже стек там не светился!! sharpobject obj = 42; // размер известен — боксинг! void P(T num) { } // размер неизвестен боксинга нет! интерфейсу и object объект в куче, иначе виртуальные вызовы не работают. Вот struct и заворачивают

было интересно

А ещё было бы хорошо добавить в сравнение AOT. Для некоторых платформ это единственный вариант. Возможно, я что-то делаю не так, но вот с AOT всё не так радужно и красиво как c прогретым JIT. Мало того - первый или единственный запуск будет скорее всего именно таким.

BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2)
12th Gen Intel Core i7-12700 2.10GHz, 1 CPU, 20 logical and 12 physical cores
.NET SDK 10.0.301
  [Host]         : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
  .NET 10.0      : .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v3
  NativeAOT 10.0 : .NET 10.0.9, X64 NativeAOT x86-64-v3
| Method                           | Runtime        | Mean      | Ratio | Allocated | Alloc Ratio |
|--------------------------------- |--------------- |----------:|------:|----------:|------------:|
| Dictionary_Foreach               | .NET 10.0      |  7.576 ns |  1.00 |         - |          NA |
| SortedList_Foreach               | .NET 10.0      |  9.573 ns |  1.26 |         - |          NA |
| Dictionary_AsIEnumerable_Foreach | .NET 10.0      |  9.167 ns |  1.21 |         - |          NA |
| Dictionary_Foreach               | NativeAOT 10.0 | 28.415 ns |  3.75 |         - |          NA |
| SortedList_Foreach               | NativeAOT 10.0 | 45.332 ns |  5.98 |      48 B |          NA |
| Dictionary_AsIEnumerable_Foreach | NativeAOT 10.0 | 90.230 ns | 11.91 |      48 B |          NA |
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации