Комментарии 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 за дженериком ограниченный интерфейсом и не вызывает аллокаций а за интерфейсом вызывает.
было интересно
А ещё было бы хорошо добавить в сравнение 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 |Спасибо!

Бенчмаркая foreach и энумераторы: аллокации, которые прятались 10 лет