02Эффективная работа с памятью

Уровень 1: Foundation

2.1 Модель памяти .NET

Структура процесса .NET

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

┌─────────────────────────────────────────────────┐
        │                 Process Memory                   │
        ├─────────────────────────────────────────────────┤
        │  Stack (per thread)                              │
        │  ├── Локальные переменные                        │
        │  ├── Параметры методов                           │
        │  └── Адреса возврата                             │
        ├─────────────────────────────────────────────────┤
        │  Managed Heap (GC Heap)                          │
        │  ├── Gen 0 (Ephemeral Segment)                   │
        │  ├── Gen 1 (Ephemeral Segment)                   │
        │  ├── Gen 2                                       │
        │  ├── LOH (Large Object Heap)                     │
        │  └── POH (Pinned Object Heap, .NET 5+)           │
        ├─────────────────────────────────────────────────┤
        │  Unmanaged Heap                                  │
        │  ├── Native allocations                          │
        │  └── P/Invoke memory                             │
        ├─────────────────────────────────────────────────┤
        │  Code / JIT Compiled Code                        │
        ├─────────────────────────────────────────────────┤
        │  Reserved / Free                                 │
        └─────────────────────────────────────────────────┘

Stack (Стек потока)

Характеристики:

  • Размер по умолчанию: 1 MB (64-bit), 256 KB (32-bit)
  • Выделение/освобождение: O(1) — просто сдвиг указателя
  • Автоматическая очистка при выходе из scope
  • LIFO структура (Last In, First Out)

Что хранится на стеке:

  • Локальные переменные value types
  • Параметры методов (включая ссылки на reference types)
  • Адреса возврата из методов
  • Ссылки на объекты в куче (но не сами объекты!)
void Example()
        {
            int x = 42;              // На стеке (value type)
            string s = "hello";      // Ссылка 's' на стеке, объект "hello" в куче
            var obj = new MyClass(); // Ссылка 'obj' на стеке, объект в куче
        }

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

Managed Heap (GC Heap)

Управляемая куча — это область памяти, управляемая Garbage Collector. Все reference type объекты размещаются здесь.

Ключевые свойства:

  • Выделение памяти: O(1) — просто инкремент указателя
  • Очистка: управляется GC (non-deterministic)
  • Фрагментация: минимальна благодаря compacting GC

Generations (Поколения)

GC использует generational hypothesis:

  1. Молодые объекты быстро умирают
  2. Старые объекты живут долго

Gen 0 (Поколение 0)

  • Назначение: Кратковременные объекты
  • Размер: ~16 MB (зависит от платформы)
  • Частота коллекции: Очень высокая (каждые несколько ms)
  • Скорость: Самая быстрая коллекция

Примеры объектов в Gen 0:

// Эти объекты почти всегда живут только в Gen 0
        var temp = new StringBuilder();
        var result = someString.Split(',');
        using var stream = new MemoryStream();

Gen 1 (Поколение 1)

  • Назначение: Буфер между Gen 0 и Gen 2
  • Размер: ~256 MB
  • Частота коллекции: Средняя

Объекты попадают сюда, если пережили коллекцию Gen 0.

Gen 2 (Поколение 2)

  • Назначение: Долгоживущие объекты
  • Размер: Динамический (до доступной памяти)
  • Частота коллекции: Редкая (может быть секунды/минуты)
  • Стоимость: Самая дорогая коллекция (full GC)

Примеры объектов в Gen 2:

// Эти объекты обычно попадают в Gen 2
        static readonly Dictionary<string, object> Cache = new();
        static readonly HttpClient SharedClient = new();

Переход между поколениями

Allocation → Gen 0 ──(survived GC)──→ Gen 1 ──(survived GC)──→ Gen 2
                            ↑                                         │
                            │                                         │
                            └──────(survived Gen 1 GC)────────────────┘

Правила перехода:

  1. Новый объект всегда выделяется в Gen 0
  2. Если объект пережил GC Gen 0 → переходит в Gen 1
  3. Если объект пережил GC Gen 1 → переходит в Gen 2
  4. Gen 2 объекты остаются там до финализации или завершения приложения

Large Object Heap (LOH)

Что такое LOH

LOH — это отдельная куча для больших объектов (>= 85,000 bytes по умолчанию).

Характеристики LOH:

  • Не компактируется по умолчанию (до .NET 4.5.1)
  • Коллекции LOH происходят только при Gen 2 collection
  • Объекты сразу попадают в Gen 2
  • Массивы double[] >= 1000 элементов всегда в LOH (даже если < 85KB)

Пороговые значения

// Попадет в LOH (>= 85,000 bytes)
        var largeArray = new byte[100_000];

        // Попадет в LOH (double[] >= 1000 элементов)
        var doubles = new double[1_000]; // 8,000 bytes, но все равно LOH

        // Обычная куча
        var smallArray = new byte[1_000];

Проблемы LOH

Фрагментация:

LOH до фрагментации:
        [Obj 100KB][Obj 50KB][Obj 200KB][Free 300KB]

        После освобождения Obj 50KB:
        [Obj 100KB][Free 50KB][Obj 200KB][Free 300KB]

        Новый объект 60KB не может использовать Free 50KB:
        [Obj 100KB][Free 50KB][Obj 200KB][Free 300KB][New 60KB]

Решение:

// .NET 4.5.1+ - включение компактизации LOH
        GCSettings.LargeObjectHeapCompactionMode = 
            GCLargeObjectHeapCompactionMode.CompactOnce;

        // Или через appsettings.json:
        {
          "runtimeOptions": {
            "gcLargeObjectHeapCompaction": true
          }
        }

Pinned Object Heap (POH)

Что такое POH

Pinned Object Heap (POH) — это отдельная куча, добавленная в .NET 5, которая предназначена исключительно для размещения закреплённых (pinned) объектов.

Проблема до .NET 5: Когда объект закрепляется в куче (например, с помощью ключевого слова fixed или класса GCHandle), Garbage Collector не может перемещать его во время фазы сжатия (compaction). Это приводило к фрагментации памяти в стандартных поколениях (Gen 0, Gen 1, Gen 2) и на LOH, мешая GC эффективно уплотнять кучу.

Решение с POH: Объекты, размещённые на POH, изначально считаются закреплёнными. GC никогда не перемещает их и не пытается сжать POH, поэтому POH управляется с помощью списка свободных блоков (free list), что исключает негативное влияние закреплённых объектов на другие поколения GC кучи.

Как выделить память на POH

Память на POH выделяется явно с помощью вызова:

// Выделение массива байт на POH
        byte[] pinnedArray = GC.AllocateArray<byte>(1024, pinned: true);

        // Выделение неинициализированного массива на POH (быстрее)
        byte[] uninitializedPinnedArray = GC.AllocateUninitializedArray<byte>(1024, pinned: true);

Ключевые преимущества:

  • Отсутствие фрагментации: стандартные поколения кучи остаются свободными от неподвижных объектов, что улучшает эффективность сжатия.
  • Меньше работы для GC: сборщику мусора не нужно отслеживать сложную логику перемещения вокруг закрепленных областей в SOH/LOH.

GC Algorithms

Workstation GC vs Server GC

ХарактеристикаWorkstation GCServer GC
Потоки GC11 на логический процессор
Память на GCМинимальная~16 MB на core
LatencyНижеВыше
ThroughputНижеВыше
Подходит дляDesktop, UIServer, backend

Конфигурация:

<!-- .csproj -->
        <PropertyGroup>
          <ServerGarbageCollection>true</ServerGarbageCollection>
          <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
        </PropertyGroup>

Или через环境变量:

DOTNET_gcServer=1
        DOTNET_gcConcurrent=1

Concurrent GC

Принцип: GC работает параллельно с приложением, минимизируя паузы.

Timeline:
        App:  ████████████████████████████████████████
        GC:       ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░

        ░ = GC работает в фоновом потоке
        █ = приложение работает

Преимущества:

  • Минимальные паузы (< 50ms для Gen 0/1)
  • Подходит для interactive приложений

Недостатки:

  • Выше общее потребление памяти
  • Может быть lower throughput

Background GC (.NET 4.0+)

Принцип: Gen 2 коллекции выполняются в фоновом потоке, пока приложение продолжает работать.

Background GC:
        Gen 0/1:  ████░░████░░████░░████  (foreground, быстрые)
        Gen 2:    ░░░░░░░░░░░░████████░░  (background, медленные)

        Приложение работает во время Gen 2 GC!

Режимы latency:

// Низкая задержка (по умолчанию)
        GCSettings.LatencyMode = GCLatencyMode.Interactive;

        // Минимальная задержка (для real-time)
        GCSettings.LatencyMode = GCLatencyMode.LowLatency;

        // Sustained low latency (.NET 4.5+)
        GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;

        // Batch mode (максимальный throughput)
        GCSettings.LatencyMode = GCLatencyMode.Batch;

Value Types vs Reference Types

Где хранятся данные

Распространенное заблуждение: "Value types всегда на стеке"

Реальность:

class MyClass
        {
            public int ValueField;  // На куче (часть объекта MyClass)
        }

        void Method()
        {
            int localValue = 42;          // На стеке
            var obj = new MyClass();      // obj.ValueField на куче!
            int[] arr = new int[10];      // Все элементы на куче!
        }

Правило:

  • Если value type — часть reference type объекта → на куче
  • Если value type — локальная переменная → на стеке
  • Если value type — элемент массива → на куче

Boxing/Unboxing

Boxing — преобразование value type в reference type (object/interface).

// Boxing (аллокация в куче!)
        int x = 42;
        object boxed = x;  // Создает новый объект в куче

        // Unboxing (проверка типа + копирование)
        int unboxed = (int)boxed;

Стоимость boxing:

  • Аллокация объекта в куче
  • Копирование данных
  • Давление на GC

Избежание boxing:

// ПЛОХО - boxing при каждом вызове
        void Log(object value) => Console.WriteLine(value);
        Log(42);  // Boxing!

        // ХОРОШО - generics без boxing
        void Log<T>(T value) => Console.WriteLine(value);
        Log(42);  // No boxing!

        // ХОРОШО - интерфейсы с struct
        interface IProcessor { void Process(); }
        struct MyProcessor : IProcessor { public void Process() { } }

        IProcessor p = new MyProcessor();  // Boxing!
        p.Process();

        // Без boxing:
        void Execute<T>(T processor) where T : IProcessor 
        {
            processor.Process();  // No boxing!
        }
        Execute(new MyProcessor());

Когда происходит boxing

  1. Приведение value type к object или ValueType
  2. Приведение struct к interface
  3. Использование non-generic коллекций (ArrayList, Hashtable)
  4. String interpolation с value types (частично mitigated в новых версиях)
// Boxing примеры:
        int i = 10;
        object o = i;                    // Boxing
        IComparable c = i;               // Boxing
        ArrayList list = new(); 
        list.Add(i);                     // Boxing

        // No boxing:
        List<int> list2 = new();
        list2.Add(i);                    // No boxing (generics)
        var s = $"{i}";                  // No boxing (span-based в .NET 8+)

Ephemeral Segment

Что такое Ephemeral Segment

Ephemeral segment — это специальный участок памяти, содержащий Gen 0 и Gen 1.

Ephemeral Segment (~16-256 MB):
        ├── Gen 0 (активное выделение)
        ├── Gen 1 (пережившие Gen 0 GC)
        └── Free space

Зачем нужен:

  • Быстрое выделение памяти (просто инкремент указателя)
  • Быстрая коллекция Gen 0 (маленький участок памяти)
  • Минимизация page faults

Когда заполняется Ephemeral Segment

Allocation → Gen 0 full → Gen 0 GC → Survivors → Gen 1
                        ↓
                 Gen 1 full → Gen 1 GC → Survivors → Gen 2
                        ↓
                 Ephemeral segment full → Allocate new segment

Практика

Задание 1: Boxing/Unboxing Demo

using System;
        using System.Collections;
        using System.Collections.Generic;
        using System.Diagnostics;

        class BoxingDemo
        {
            static void Main()
            {
                // Boxing через object
                var sw = Stopwatch.StartNew();
                for (int i = 0; i < 10_000_000; i++)
                {
                    object boxed = i;  // Boxing!
                }
                Console.WriteLine($"Boxing: {sw.ElapsedMilliseconds}ms");

                // Без boxing (generics)
                sw.Restart();
                var list = new List<int>(10_000_000);
                for (int i = 0; i < 10_000_000; i++)
                {
                    list.Add(i);  // No boxing
                }
                Console.WriteLine($"No boxing: {sw.ElapsedMilliseconds}ms");
            }
        }

Задание 2: Benchmark Workstation vs Server GC

// Запуск с Workstation GC:
        // dotnet run --configuration Release

        // Запуск с Server GC:
        // DOTNET_gcServer=1 dotnet run --configuration Release

        using System;
        using System.Diagnostics;

        class GCBenchmark
        {
            static void Main()
            {
                Console.WriteLine($"Server GC: {GCSettings.IsServerGC}");
        
                var sw = Stopwatch.StartNew();
                long totalAllocated = 0;
        
                for (int i = 0; i < 1000; i++)
                {
                    var data = new byte[1_000_000];  // 1MB allocations
                    totalAllocated += data.Length;
            
                    if (i % 100 == 0)
                    {
                        Console.WriteLine($"Gen 0: {GC.CollectionCount(0)}");
                        Console.WriteLine($"Gen 1: {GC.CollectionCount(1)}");
                        Console.WriteLine($"Gen 2: {GC.CollectionCount(2)}");
                    }
                }
        
                Console.WriteLine($"Total: {sw.ElapsedMilliseconds}ms");
                Console.WriteLine($"Allocated: {totalAllocated / 1_000_000}MB");
            }
        }

Контрольные вопросы

  1. Почему struct в массиве хранится на куче, а не на стеке?
  2. Что такое ephemeral segment и зачем он нужен?
  3. Когда происходит forced collection Gen 2?
  4. Какой размер объекта попадает в LOH по умолчанию?
  5. Может ли reference type храниться на стеке?

2.2 GC и управление жизненным циклом объектов

IDisposable Pattern

Зачем нужен IDisposable

IDisposable предоставляет детерминированный способ освобождения ресурсов, в отличие от GC, который работает недетерминированно.

Ресурсы, требующие IDisposable:

  • File handles
  • Database connections
  • Network sockets
  • Unmanaged memory
  • GDI+ objects
  • Mutex/Semaphore

Correct Implementation

Полный паттерн IDisposable:

public class ResourceHolder : IDisposable
        {
            // Unmanaged resource
            private IntPtr _nativeHandle;
    
            // Managed disposable resource
            private FileStream? _fileStream;
    
            // Track if already disposed
            private bool _disposed;
    
            public ResourceHolder(string path)
            {
                _fileStream = new FileStream(path, FileMode.Open);
                _nativeHandle = AllocateNativeResource();
            }
    
            // Public IDisposable implementation
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);  // Предотвращаем вызов финализатора
            }
    
            // Protected virtual для наследования
            protected virtual void Dispose(bool disposing)
            {
                if (!_disposed)
                {
                    if (disposing)
                    {
                        // Освобождаем MANAGED ресурсы
                        _fileStream?.Dispose();
                    }
            
                    // Освобождаем UNMANAGED ресурсы
                    FreeNativeResource(_nativeHandle);
                    _nativeHandle = IntPtr.Zero;
            
                    _disposed = true;
                }
            }
    
            // Финализатор — только если есть unmanaged ресурсы!
            ~ResourceHolder()
            {
                Dispose(false);
            }
    
            private static IntPtr AllocateNativeResource() => IntPtr.Zero; // placeholder
            private static void FreeNativeResource(IntPtr handle) { }      // placeholder
        }

Ключевые правила

  1. GC.SuppressFinalize(this) — обязателен в Dispose()
  2. - Предотвращает помещение объекта в finalization queue - Ускоряет GC (объект не требует финализации)
  1. Финализатор ТОЛЬКО для unmanaged ресурсов
  2. - Если нет unmanaged ресурсов — финализатор не нужен - Финализаторы замедляют GC (объект переживает минимум одну коллекцию)
  1. Dispose(bool disposing) — protected virtual
  2. - Позволяет наследникам правильно освобождать ресурсы - disposing == true → вызван из Dispose() → можно трогать managed объекты - disposing == false → вызван из финализатора → managed объекты МОГУТ быть уже собраны

Modern IDisposable (.NET Core 3.0+)

public class ModernResource : IDisposable
        {
            private FileStream? _fileStream;
    
            public void Dispose()
            {
                _fileStream?.Dispose();
                GC.SuppressFinalize(this);
            }
        }

        // Если нет unmanaged ресурсов — финализатор НЕ НУЖЕН!
        // Если есть unmanaged — используйте SafeHandle вместо IntPtr

Nested Disposables

public class CompositeResource : IDisposable
        {
            private readonly IDisposable _resource1;
            private readonly IDisposable _resource2;
            private bool _disposed;
    
            public CompositeResource(IDisposable r1, IDisposable r2)
            {
                _resource1 = r1;
                _resource2 = r2;
            }
    
            public void Dispose()
            {
                if (_disposed) return;
        
                // Dispose в обратном порядке создания
                _resource2.Dispose();
                _resource1.Dispose();
        
                _disposed = true;
                GC.SuppressFinalize(this);
            }
        }

SafeHandle и CriticalFinalizerObject

SafeHandle

Проблема с IntPtr:

// ОПАСНО — race condition при ThreadAbort
        class BadWrapper
        {
            private IntPtr _handle;
    
            ~BadWrapper()
            {
                CloseHandle(_handle);  // Может не выполниться при ThreadAbort!
            }
        }

Решение — SafeHandle:

public sealed class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            public MySafeHandle() : base(true) { }
    
            protected override bool ReleaseHandle()
            {
                // Вызывается даже при ThreadAbort/OutOfMemory
                return CloseHandle(handle);
            }
    
            [DllImport("kernel32.dll")]
            private static extern bool CloseHandle(IntPtr handle);
        }

Преимущества SafeHandle:

  • CriticalFinalizerObject гарантирует выполнение
  • Reference counting предотвращает premature close
  • Constrained Execution Region (CER)

CriticalFinalizerObject

// SafeHandle наследует от CriticalFinalizerObject
        // Это гарантирует:
        // 1. Финализатор выполнится даже при AppDomain unload
        // 2. Финализатор выполнится при ThreadAbort
        // 3. CLR подготовит код финализатора заранее (eager JIT)

Finalizers

Когда использовать

Используйте финализатор ТОЛЬКО когда:

  • Класс напрямую владеет unmanaged ресурсами
  • Нет возможности использовать SafeHandle

НЕ используйте финализатор когда:

  • Освобождаете только managed ресурсы
  • Хотите "гарантировать" очистку (финализатор не гарантирован!)

Почему финализаторы — это плохо

class WithFinalizer
        {
            ~WithFinalizer()
            {
                // Cleanup
            }
        }

Проблемы:

  1. Двойная жизнь объекта:
  2. ``` Allocation → Gen 0 → GC → Finalization Queue → Gen 1 → Finalizer → Gen 2 ``` Объект с финализатором переживает минимум одну дополнительную коллекцию.
  1. Недетерминированность:
  2. - Неизвестно когда выполнится финализатор - Может не выполниться вообще (AppDomain crash)
  1. Порядок не гарантирован:
  2. - Если объект A ссылается на B, финализатор B может выполниться раньше A
  1. Производительность:
  2. - Все объекты с финализаторами попадают в finalization queue - Отдельный поток выполняет финализаторы - Full GC ждет завершения всех финализаторов

Best Practice

// ПЛОХО
        class Bad
        {
            private FileStream _stream;
    
            ~Bad()
            {
                _stream.Dispose();  // Может быть уже null!
            }
        }

        // ХОРОШО — без финализатора
        class Good : IDisposable
        {
            private FileStream? _stream;
    
            public void Dispose()
            {
                _stream?.Dispose();
                GC.SuppressFinalize(this);
            }
        }

        // ХОРОШО — с SafeHandle вместо финализатора
        class Better : IDisposable
        {
            private MySafeHandle? _handle;
    
            public void Dispose()
            {
                _handle?.Dispose();
                GC.SuppressFinalize(this);
            }
        }

WeakReference

Что такое WeakReference

WeakReference позволяет ссылаться на объект, не предотвращая его сборку GC.

var obj = new LargeObject();
        var weakRef = new WeakReference<LargeObject>(obj);

        // Проверяем, жив ли объект
        if (weakRef.TryGetTarget(out var target))
        {
            // Объект жив, используем target
            Console.WriteLine(target.Data);
        }
        else
        {
            // Объект собран GC
            Console.WriteLine("Object was collected");
        }

        // Убираем сильную ссылку
        obj = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();

        // Теперь TryGetTarget вернет false

Когда использовать

Кэширование:

public class ImageCache
        {
            private readonly Dictionary<string, WeakReference<Image>> _cache = new();
    
            public Image GetOrLoad(string path)
            {
                if (_cache.TryGetValue(path, out var weakRef) && 
                    weakRef.TryGetTarget(out var image))
                {
                    return image;  // Cache hit
                }
        
                // Cache miss — загружаем
                var newImage = LoadImage(path);
                _cache[path] = new WeakReference<Image>(newImage);
                return newImage;
            }
        }

Event handler leak prevention:

public class Publisher
        {
            private readonly List<WeakReference<EventHandler>> _handlers = new();
    
            public void Subscribe(EventHandler handler)
            {
                _handlers.Add(new WeakReference<EventHandler>(handler));
            }
    
            public void Publish(object sender, EventArgs e)
            {
                // Очищаем dead references
                _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        
                foreach (var wr in _handlers)
                {
                    if (wr.TryGetTarget(out var handler))
                    {
                        handler(sender, e);
                    }
                }
            }
        }

ConditionalWeakTable

Отличие от WeakReference

ХарактеристикаDictionary + WeakReferenceConditionalWeakTable
Key lifecycleKey может быть собранKey должен быть reference type
Value lifecycleValue может быть собранValue привязан к Key
Thread safetyНетДа
EnumerationДаНет

Использование

// Привязываем метаданные к объектам без изменения их класса
        public static class ObjectMetadata
        {
            private static readonly ConditionalWeakTable<object, Metadata> _table = new();
    
            public static void SetMetadata(object obj, Metadata metadata)
            {
                _table.AddOrUpdate(obj, metadata);
            }
    
            public static Metadata? GetMetadata(object obj)
            {
                return _table.TryGetValue(obj, out var metadata) ? metadata : null;
            }
        }

        // Когда obj будет собран GC, его metadata тоже будет собрана!

Практический пример — Extension Properties

public static class TagExtensions
        {
            private static readonly ConditionalWeakTable<object, Dictionary<string, object>> _tags = new();
    
            public static void SetTag(this object obj, string key, object value)
            {
                var dict = _tags.GetValue(obj, _ => new Dictionary<string, object>());
                dict[key] = value;
            }
    
            public static T? GetTag<T>(this object obj, string key)
            {
                if (_tags.TryGetValue(obj, out var dict) && dict.TryGetValue(key, out var value))
                {
                    return (T)value;
                }
                return default;
            }
        }

GC.Collect() — Когда допустимо вызывать

Почему почти никогда

// ПЛОХО — принудительная коллекция без причины
        void ProcessData()
        {
            var data = LoadLargeData();
            Process(data);
            GC.Collect();  // Зачем? GC сам знает когда собирать!
        }

Проблемы:

  • Нарушает оптимизации GC
  • Может вызвать unnecessary Gen 2 collection
  • Дублирует работу GC

Когда ДОПУСТИМО

  1. После загрузки больших данных:
void LoadConfiguration()
        {
            var tempData = LoadTempFiles();  // Много временных объектов
            Process(tempData);
            // tempData больше не нужен, можно подсказать GC
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
            GC.WaitForPendingFinalizers();
        }
  1. В тестах:
[Test]
        public void Test_NoMemoryLeak()
        {
            var weakRef = CreateObject();
    
            // Принудительная коллекция для теста
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
    
            Assert.IsFalse(weakRef.IsAlive);
        }
  1. При переходе в LowLatency mode:
// Перед входом в critical section
        GC.Collect();  // Очищаем кучу перед low latency period
        GCSettings.LatencyMode = GCLatencyMode.LowLatency;
  1. Server приложения при idle:
// В background task во время низкой нагрузки
        async Task PeriodicCleanupAsync(CancellationToken ct)
        {
            while (!ct.IsCancellationRequested)
            {
                await Task.Delay(TimeSpan.FromMinutes(5), ct);
        
                if (IsIdle())
                {
                    GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
                }
            }
        }

GCCollectionMode

// Default — зависит от версии .NET
        GC.Collect();

        // Forced — выполнить немедленно
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);

        // Optimized — GC решает, оптимально ли сейчас собирать (.NET 4.6+)
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);

        // Aggressive (.NET 10+) — агрессивная очистка
        GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive);

Object Pool

Microsoft.Extensions.ObjectPool

using Microsoft.Extensions.ObjectPool;

        // Создание pool
        var policy = new StringBuilderPooledObjectPolicy();
        var pool = new DefaultObjectPool<StringBuilder>(policy, 100);

        // Использование
        var sb = pool.Get();
        try
        {
            sb.Append("Hello, ");
            sb.Append("World!");
            return sb.ToString();
        }
        finally
        {
            sb.Clear();  // Очищаем перед возвратом
            pool.Return(sb);
        }

Custom Object Pool

public class PooledObject<T> : IDisposable where T : class, new()
        {
            private readonly ObjectPool<T> _pool;
            public T Value { get; }
            private int _disposed;
    
            public PooledObject(ObjectPool<T> pool)
            {
                _pool = pool;
                Value = pool.Get();
            }
    
            public void Dispose()
            {
                if (Interlocked.Exchange(ref _disposed, 1) == 0)
                {
                    _pool.Return(Value);
                }
            }
        }

        // Использование
        using var pooled = new PooledObject<MyExpensiveObject>(myPool);
        pooled.Value.DoWork();
        // Автоматически возвращается в pool при выходе из using

Object Pool с ConditionalWeakTable

public class ContextualObjectPool<T> where T : class, new()
        {
            private readonly Stack<T> _pool = new();
            private readonly ConditionalWeakTable<T, object> _context = new();
            private readonly object _lock = new();
    
            public T Get()
            {
                lock (_lock)
                {
                    return _pool.Count > 0 ? _pool.Pop() : new T();
                }
            }
    
            public void Return(T obj, object contextData)
            {
                _context.AddOrUpdate(obj, contextData);
        
                lock (_lock)
                {
                    _pool.Push(obj);
                }
            }
    
            public object? GetContext(T obj)
            {
                return _context.TryGetValue(obj, out var ctx) ? ctx : null;
            }
        }

Leak Detector на WeakReference

public class MemoryLeakDetector
        {
            private readonly List<WeakReference> _trackedObjects = new();
            private readonly List<string> _descriptions = new();
    
            public void Track(object obj, string description)
            {
                _trackedObjects.Add(new WeakReference(obj));
                _descriptions.Add(description);
            }
    
            public void Report()
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
        
                Console.WriteLine("=== Memory Leak Report ===");
        
                for (int i = 0; i < _trackedObjects.Count; i++)
                {
                    var isAlive = _trackedObjects[i].IsAlive;
                    var status = isAlive ? "LEAKED" : "Collected";
                    Console.WriteLine($"{status}: {_descriptions[i]}");
                }
            }
        }

        // Использование
        void Test()
        {
            var detector = new MemoryLeakDetector();
    
            var obj1 = new object();
            var obj2 = new object();
    
            detector.Track(obj1, "Temporary object");
            detector.Track(obj2, "Cached object");
    
            obj1 = null;  // Должен быть собран
            // obj2 все еще ссылается
    
            detector.Report();
            // LEAKED: Cached object
            // Collected: Temporary object
        }

Практика

Задание 1: Правильный IDisposable Pattern

Реализуйте класс DatabaseConnection с:

  • Managed ресурсом (SqlCommand)
  • Unmanaged ресурсом (native handle через SafeHandle)
  • Поддержкой наследования
  • Nested disposables

Задание 2: Object Pool

Создайте ObjectPool<T> с:

  • Максимальным размером пула
  • Thread-safe операциями
  • Поддержкой ConditionalWeakTable для метаданных
  • Автоматическим возвратом через using

Задание 3: Leak Detector

Напишите утилиту для обнаружения memory leaks:

  • Отслеживание объектов через WeakReference
  • Автоматический report после GC
  • Интеграция с unit tests

Контрольные вопросы

  1. Зачем нужен GC.SuppressFinalize(this) в Dispose()?
  2. Почему финализаторы замедляют GC?
  3. Когда Dispose(false) вызывается?
  4. В чем разница между WeakReference и ConditionalWeakTable?
  5. Можно ли использовать GC.Collect() в production?
  6. Зачем в .NET 5 был введен Pinned Object Heap (POH)?

2.3 Диагностика памяти

Обзор инструментов

┌─────────────────────────────────────────────────────────────┐
        │                    Memory Diagnostics                        │
        ├──────────────────┬──────────────────┬───────────────────────┤
        │  Real-time       │  Post-mortem     │  Profiling            │
        │  (live metrics)  │  (after crash)   │  (detailed analysis)  │
        ├──────────────────┼──────────────────┼───────────────────────┤
        │  dotnet-counters │  dotnet-dump     │  dotnet-gcdump        │
        │  EventCounter    │  SOS commands    │  Visual Studio        │
        │  Prometheus      │  WinDbg          │  JetBrains DotMemory  │
        │  ApplicationIns. │  crash dumps     │  PerfView             │
        └──────────────────┴──────────────────┴───────────────────────┘

dotnet-counters

Установка и базовое использование

# Установка
        dotnet tool install -g dotnet-counters

        # Список доступных процессов
        dotnet-counters ps

        # Мониторинг в реальном времени
        dotnet-counters monitor -p <PID>

        # Мониторинг конкретных счетчиков
        dotnet-counters monitor -p <PID> --counters System.Runtime

Ключевые метрики памяти

СчетчикОписаниеНормальное значение
working-setФизическая память процессаЗависит от приложения
gc-heap-sizeРазмер GC кучиСтабильный в steady state
gen-0-gc-countКоличество Gen 0 коллекцийРастет пропорционально нагрузке
gen-1-gc-countКоличество Gen 1 коллекцийМеньше Gen 0
gen-2-gc-countКоличество Gen 2 коллекцийМинимальное в steady state
allocation-rateСкорость аллокаций (bytes/sec)< 1 MB/sec для оптимизированных
gc-fragmentationФрагментация кучи< 5%
loh-sizeРазмер Large Object HeapСтабильный

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

# Мониторинг с кастомными счетчиками
        dotnet-counters monitor -p 12345 --counters "
          System.Runtime,
          Microsoft.AspNetCore.Hosting
        "

        # Сбор данных для последующего анализа
        dotnet-counters collect -p 12345 --counters System.Runtime -o metrics.json

        # Анализ collected данных
        dotnet-counters report -i metrics.json

EventCounter в коде

using System.Diagnostics.Metrics;

        // Создание Meter
        var meter = new Meter("MyApp.Memory", "1.0.0");

        // Создание counters
        var allocationCounter = meter.CreateCounter<long>(
            "allocations.bytes", 
            unit: "byte", 
            description: "Total bytes allocated");

        var poolSizeGauge = meter.CreateGauge<int>(
            "pool.size", 
            unit: "{objects}", 
            description: "Current pool size");

        // Использование
        void ProcessData(byte[] data)
        {
            allocationCounter.Add(data.Length);
            // ... processing
        }

Integration с Prometheus

using OpenTelemetry.Metrics;
        using OpenTelemetry.Exporter.Prometheus;

        var builder = WebApplication.CreateBuilder(args);

        builder.Services.AddOpenTelemetry()
            .WithMetrics(metrics =>
            {
                metrics.AddMeter("MyApp.Memory");
                metrics.AddPrometheusExporter();
            });

        var app = builder.Build();
        app.MapPrometheusScrapingEndpoint();
        app.Run();

Prometheus query examples:

# Allocation rate за последние 5 минут
        rate(myapp_memory_allocations_bytes_total[5m])

        # GC collections per second
        rate(system_runtime_gen_0_gc_count_total[1m])

        # Heap size trend
        system_runtime_gc_heap_size

dotnet-gcdump

Что такое GC Dump

GC Dump — это снимок managed heap в определенный момент времени. Содержит:

  • Все объекты в куче
  • Ссылки между объектами
  • Размеры объектов
  • Типы объектов

Сбор GC Dump

# Установка
        dotnet tool install -g dotnet-gcdump

        # Сбор dump
        dotnet-gcdump collect -p <PID> -o memory.gcdump

        # Сбор с verbose output
        dotnet-gcdump collect -p <PID> -v -o memory.gcdump

Анализ GC Dump

Вариант 1: Visual Studio

  1. Откройте .gcdump файл в Visual Studio
  2. Перейдите во вкладку "Summary" для обзора
  3. Используйте "Dominators" для поиска корней утечек
  4. Сортируйте по "Retained Size" для поиска больших объектов

Вариант 2: Speedscope (online)

  1. Откройте https://www.speedscope.app
  2. Загрузите .gcdump файл
  3. Анализируйте дерево объектов

Вариант 3: JetBrains DotMemory

  1. Откройте DotMemory
  2. Import .gcdump файл
  3. Используйте сравнение snapshots для поиска утечек

Поиск memory leak

Шаги анализа:
        1. Collect snapshot #1 (начальное состояние)
        2. Выполнить операцию N раз
        3. Collect snapshot #2
        4. Сравнить snapshots
        5. Найти типы с наибольшим ростом
        6. Найти GC roots для этих объектов

Пример анализа:

Snapshot #1 → Snapshot #2 comparison:

        Type                    | #1 Count | #2 Count | Delta
        ------------------------|----------|----------|-------
        MyApp.EventSubscription | 100      | 10,100   | +10,000  ← LEAK!
        System.String           | 5,000    | 5,200    | +200
        System.Byte[]           | 200      | 210      | +10

        GC Root для EventSubscription:
        static field → EventManager._subscribers → List<EventSubscription>

dotnet-dump

Сбор и анализ

# Установка
        dotnet tool install -g dotnet-dump

        # Сбор dump
        dotnet-dump collect -p <PID> --type Full -o dump.dmp

        # Анализ
        dotnet-dump analyze dump.dmp

Основные SOS команды

# Общая информация о GC куче
        eeheap -gc

        # Статистика по типам объектов
        dumpheap -stat

        # Объекты конкретного типа
        dumpheap -type System.String

        # Детали конкретного объекта
        dumpobj <address>

        # Найти корни объекта (почему не собран?)
        gcroot <address>

        # Очередь финализации
        finalizequeue

        # Стек текущего потока
        clrstack

        # Все managed потоки
        clrthreads

        # Объекты на стеке
        dso

        # Async state machines
        dumpasync

        # Информация об исключениях
        pe

Практический пример анализа утечки

# 1. Смотрим общую картину
        > eeheap -gc
        GC Heap Size: 512 MB

        # 2. Находим самые большие типы
        > dumpheap -stat
               MT     Count    TotalSize Class Name
        ...
        00007ff8c0a12340  100000   256000000 MyApp.CachedData
        00007ff8c0a56780   50000   128000000 System.Byte[]

        # 3. Смотрим детали объекта
        > dumpobj 000001a2b0001000
        Name:        MyApp.CachedData
        MethodTable: 00007ff8c0a12340
        Size:        2560 bytes

        # 4. Находим корни
        > gcroot 000001a2b0001000
        Thread 1234:
            000000b3c000f000 00007ff8c0a12340 MyApp.Program.Main()
                ->  000001a2b0001000 MyApp.CachedData

        # 5. Смотрим стек
        > clrstack
        OS Thread Id: 0x1234
        Child SP         IP               Call Site
        000000b3c000f000 00007ff8c0a12340 MyApp.Program.Main()

PerfView

Установка

# Download from GitHub
        # https://github.com/microsoft/perfview/releases

Сбор данных

  1. Запустите PerfView
  2. File → Collect → Collect
  3. Включите "GC Only" для memory profiling
  4. Запустите приложение
  5. Выполните тестовый сценарий
  6. Stop Collection

Анализ

GC Stats:

  • Откройте "GC Stats" в collected data
  • Смотрите "GC Heap Alloc Stacks" для поиска hot paths
  • Анализируйте "GC Events" для понимания коллекций

Memory Allocation Stacks:

GC Heap Alloc Stacks:
        └── MyApp.Processor.HandleMessage()
            └── new byte[1024]        ← 45% allocations
            └── string.Substring()    ← 30% allocations
            └── JsonConvert.Serialize() ← 25% allocations

Visual Studio Diagnostic Tools

Memory Usage Tool

  1. Debug → Start Diagnostic Tools Without Debugging (Alt+F2)
  2. Выберите "Memory Usage"
  3. Запустите приложение
  4. Take Snapshot в ключевых точках
  5. Сравните snapshots

Profiling

  1. Debug → Performance Profiler (Alt+F2)
  2. Выберите ".NET Object Allocation Tracking"
  3. Запустите profiling
  4. Анализируйте allocation tree

Интерпретация результатов

Memory Snapshot Comparison:

        Handle Count | Size (bytes) | Type
        -------------|--------------|------------------
            +10,000  |   +2,560,000 | MyApp.EventSubscription  ← LEAK
               +200  |     +12,800  | System.String
                +10  |      +1,024  | System.Int32

        Root Path для EventSubscription:
        static MyApp.EventManager._handlers
          → Dictionary<string, List<EventHandler>>
            → List<EventHandler>
              → EventHandler.Target → MyApp.Subscriber

Практика

Задание 1: Найти и устранить memory leak

Сломанное приложение:

public class LeakyService
        {
            private static readonly List<object> _cache = new();
    
            public void Process(object data)
            {
                // УТЕЧКА: объекты никогда не удаляются из cache
                _cache.Add(new { Data = data, Timestamp = DateTime.UtcNow });
            }
        }

        public class EventLeak
        {
            private readonly EventSource _source;
    
            public EventLeak(EventSource source)
            {
                _source = source;
                // УТЕЧКА: handler не отписывается
                _source.OnEvent += HandleEvent;
            }
    
            private void HandleEvent(object sender, EventArgs e) { }
        }

Шаги диагностики:

  1. Запустите приложение
  2. Соберите baseline snapshot
  3. Выполните 10,000 операций
  4. Соберите второй snapshot
  5. Найдите типы с наибольшим ростом
  6. Найдите GC roots
  7. Исправьте код

Задание 2: Профилирование высоконагруженного сервиса

// Цель: оптимизировать allocation rate
        public class MessageProcessor
        {
            public void Process(Stream input)
            {
                // ПЛОХО — много аллокаций
                var buffer = new byte[input.Length];
                input.Read(buffer, 0, buffer.Length);
                var json = Encoding.UTF8.GetString(buffer);
                var obj = JsonConvert.DeserializeObject(json);
                var result = ProcessObject(obj);
                var output = JsonConvert.SerializeObject(result);
                return Encoding.UTF8.GetBytes(output);
            }
        }

Оптимизация:

public class OptimizedMessageProcessor
        {
            private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
    
            public void Process(Stream input)
            {
                // ХОРОШО — pooled buffers
                var buffer = _pool.Rent((int)input.Length);
                try
                {
                    input.Read(buffer, 0, (int)input.Length);
                    // ... processing с Span<byte>
                }
                finally
                {
                    _pool.Return(buffer);
                }
            }
        }

Задание 3: Dashboard с Prometheus

// Создайте middleware для memory metrics
        public class MemoryMetricsMiddleware
        {
            private readonly RequestDelegate _next;
            private static readonly Gauge _workingSet = Metrics.CreateGauge(
                "process_working_set_bytes", "Process working set in bytes");
    
            public MemoryMetricsMiddleware(RequestDelegate next)
            {
                _next = next;
            }
    
            public async Task InvokeAsync(HttpContext context)
            {
                _workingSet.Set(Environment.WorkingSet);
                await _next(context);
            }
        }

        // Grafana dashboard panels:
        // 1. Working Set (current memory usage)
        // 2. GC Collections rate (gen 0/1/2 per second)
        // 3. Allocation rate (bytes per second)
        // 4. LOH size trend
        // 5. GC Heap fragmentation %

Контрольные вопросы

  1. Какой инструмент использовать для real-time мониторинга?
  2. Как найти причину memory leak?
  3. Собрать 2+ GC dump с интервалом
  4. Сравнить snapshots
  5. Найти типы с наибольшим ростом
  6. Использовать gcroot для поиска корней
  7. Исправить код (убрать static cache, отписать events)
  8. Что показывает dumpheap -stat?
  9. Когда использовать dotnet-dump vs dotnet-gcdump?
  10. Как интерпретировать allocation-rate?

2.4 Zero-Allocation Patterns

Span — Deep Dive

Что такое Span

Span<T> — это ref struct, предоставляющий type-safe представление contiguous memory region без аллокаций.

// Span<T> может работать с:
        // 1. Массивами
        Span<int> fromArray = new int[] { 1, 2, 3 };

        // 2. Стеком
        Span<byte> fromStack = stackalloc byte[256];

        // 3. Unmanaged памятью
        unsafe
        {
            var ptr = Marshal.AllocHGlobal(100);
            Span<byte> fromUnmanaged = new Span<byte>((void*)ptr, 100);
            Marshal.FreeHGlobal(ptr);
        }

Ключевые свойства

public readonly ref struct Span<T>
        {
            public int Length { get; }
            public ref T this[int index] { get; }
            public bool IsEmpty { get; }
            public ref T GetPinnableReference();
    
            // Slicing — БЕЗ аллокаций!
            public Span<T> Slice(int start);
            public Span<T> Slice(int start, int length);
    
            // Conversion
            public Span<T> this[Range range] { get; }
        }

Slicing без аллокаций

// ПЛОХО — аллокация нового массива
        string text = "Hello, World!";
        string substring = text.Substring(7, 5);  // "World" — NEW STRING

        // ХОРОШО — zero allocation
        string text = "Hello, World!";
        ReadOnlySpan<char> span = text.AsSpan();
        ReadOnlySpan<char> slice = span.Slice(7, 5);  // "World" — NO ALLOCATION

Parsing с Span

// ПЛОХО — много аллокаций
        string ParseCsv(string line)
        {
            var parts = line.Split(',');  // Allocates array + strings
            return parts[1].Trim();       // Another allocation
        }

        // ХОРОШО — zero allocation
        string ParseCsv(ReadOnlySpan<char> line)
        {
            int commaIndex = line.IndexOf(',');
            var value = line.Slice(commaIndex + 1);
    
            // Trim без аллокаций
            int start = 0;
            while (start < value.Length && char.IsWhiteSpace(value[start])) start++;
            int end = value.Length;
            while (end > start && char.IsWhiteSpace(value[end - 1])) end--;
    
            return value.Slice(start, end - start).ToString();
        }

Span ограничения

// Span<T> — ref struct, НЕЛЬЗЯ:
        // ❌ Использовать в полях класса
        class Bad { Span<int> _field; }  // CS8345

        // ❌ Использовать в async методах
        async Task BadAsync() { Span<int> s = stackalloc int[10]; }  // CS8352

        // ❌ Возвращать из метода (если stackalloc)
        Span<int> BadReturn() { return stackalloc int[10]; }  // CS8352

        // ❌ Box в object
        object o = span;  // CS8346

        // ❌ Использовать в интерфейсах
        void Process<T>(T span) where T : ISpan<int> { }  // Не работает

        // ✅ МОЖНО использовать:
        // - В локальных переменных
        // - В параметрах методов
        // - В ref return
        // - В struct полях (если struct тоже ref struct)

Memory

Когда использовать Memory

Memory<T> — это regular struct (не ref struct), можно использовать в async и полях класса.

// ✅ Memory<T> можно хранить в поле
        class BufferManager
        {
            private Memory<byte> _buffer;  // OK
    
            public BufferManager(Memory<byte> buffer)
            {
                _buffer = buffer;
            }
        }

        // ✅ Memory<T> можно использовать в async
        async Task ProcessAsync(Memory<byte> buffer)
        {
            await Task.Delay(100);
            buffer.Span[0] = 42;  // OK
        }

        // Conversion
        Memory<byte> memory = new byte[100];
        Span<byte> span = memory.Span;  // Get Span from Memory

ReadOnlyMemory

// Immutable version — для чтения
        public class MessageReader
        {
            private readonly ReadOnlyMemory<byte> _data;
    
            public MessageReader(ReadOnlyMemory<byte> data)
            {
                _data = data;
            }
    
            public int ReadInt32(int offset)
            {
                return MemoryMarshal.Read<int>(_data.Span.Slice(offset));
            }
        }

ArrayPool

Базовое использование

// Shared pool — для общего использования
        var pool = ArrayPool<byte>.Shared;

        // Rent buffer
        byte[] buffer = pool.Rent(1024);  // Returns at least 1024 bytes
        try
        {
            // Use buffer
            buffer[0] = 42;
        }
        finally
        {
            // ALWAYS return to pool!
            pool.Return(buffer);
        }

Стратегии возврата

// Return с очисткой (безопасно, но медленнее)
        pool.Return(buffer, clearArray: true);

        // Return без очистки (быстрее, но данные остаются)
        pool.Return(buffer, clearArray: false);

        // Когда использовать clearArray:
        // - true: если buffer содержал sensitive data
        // - false: если вы перезаписываете все данные перед использованием

Custom ArrayPool

public class LimitedArrayPool<T> : ArrayPool<T>
        {
            private readonly int _maxArrays;
            private readonly int _arraySize;
            private readonly Queue<T[]> _pool;
            private readonly object _lock = new();
    
            public LimitedArrayPool(int maxArrays, int arraySize)
            {
                _maxArrays = maxArrays;
                _arraySize = arraySize;
                _pool = new Queue<T[]>(maxArrays);
            }
    
            public override T[] Rent(int minimumLength)
            {
                if (minimumLength > _arraySize)
                    return new T[minimumLength];  // Too big for pool
        
                lock (_lock)
                {
                    if (_pool.Count > 0)
                        return _pool.Dequeue();
                }
        
                return new T[_arraySize];
            }
    
            public override void Return(T[] array, bool clearArray = false)
            {
                if (array.Length != _arraySize)
                    return;  // Not from this pool
        
                if (clearArray)
                    Array.Clear(array);
        
                lock (_lock)
                {
                    if (_pool.Count < _maxArrays)
                        _pool.Enqueue(array);
                }
            }
        }

ArrayPool + Span паттерн

public static void ProcessWithPool(ReadOnlySpan<byte> input)
        {
            var pool = ArrayPool<byte>.Shared;
            byte[] buffer = pool.Rent(input.Length * 2);
    
            try
            {
                Span<byte> span = buffer.AsSpan(0, input.Length * 2);
        
                // Process with Span
                input.CopyTo(span);
                Transform(span);
        
                // Use result
                Output(span);
            }
            finally
            {
                pool.Return(buffer);
            }
        }

stackalloc

Базовое использование

// Без unsafe (через Span<T>)
        Span<byte> buffer = stackalloc byte[256];
        Span<int> numbers = stackalloc int[100];

        // С unsafe (прямые pointers)
        unsafe
        {
            byte* ptr = stackalloc byte[256];
            int* nums = stackalloc int[100];
        }

Ограничения

// ❌ Нельзя в цикле (stack overflow risk)
        void Bad()
        {
            for (int i = 0; i < 1000; i++)
            {
                Span<byte> buf = stackalloc byte[1024];  // Stack overflow!
            }
        }

        // ❌ Нельзя большой размер (limit ~1MB)
        Span<byte> huge = stackalloc byte[2_000_000];  // Stack overflow!

        // ❌ Нельзя variable size в некоторых случаях
        Span<byte> dynamic = stackalloc byte[GetSize()];  // OK в .NET Core
        // Но размер должен быть reasonable

        // ✅ Правильное использование
        void Good()
        {
            // Small, fixed-size buffers
            Span<byte> header = stackalloc byte[64];
            Span<int> indices = stackalloc int[32];
    
            // Use buffer
            ReadHeader(header);
            ProcessIndices(indices);
        } // Memory automatically reclaimed

Conditional stackalloc

// Паттерн: stackalloc для small, pool для large
        public static void Process(ReadOnlySpan<byte> input)
        {
            Span<byte> buffer;
            byte[]? arrayPoolBuffer = null;
    
            if (input.Length <= 256)
            {
                buffer = stackalloc byte[256];
            }
            else
            {
                arrayPoolBuffer = ArrayPool<byte>.Shared.Rent(input.Length);
                buffer = arrayPoolBuffer.AsSpan(0, input.Length);
            }
    
            try
            {
                input.CopyTo(buffer);
                Transform(buffer);
                Output(buffer);
            }
            finally
            {
                if (arrayPoolBuffer != null)
                {
                    ArrayPool<byte>.Shared.Return(arrayPoolBuffer);
                }
            }
        }

ref returns, ref locals, in parameters

ref locals

// ref local — ссылка на существующую переменную
        int x = 42;
        ref int y = ref x;
        y = 100;  // x теперь тоже 100

        // ref в массиве
        int[] arr = { 1, 2, 3, 4, 5 };
        ref int first = ref arr[0];
        first = 10;  // arr[0] = 10

        // ref в Span
        Span<int> span = stackalloc int[5];
        ref int item = ref span[2];
        item = 42;  // span[2] = 42

ref returns

// Возврат ссылки на элемент
        public ref int Find(int[] array, Predicate<int> match)
        {
            for (int i = 0; i < array.Length; i++)
            {
                if (match(array[i]))
                    return ref array[i];  // ref return
            }
            throw new NotFoundException();
        }

        // Использование
        int[] numbers = { 1, 2, 3, 4, 5 };
        ref int found = ref Find(numbers, n => n == 3);
        found = 30;  // numbers[2] = 30

in parameters

// in parameter — readonly ref (без копирования)
        struct LargeStruct
        {
            public int A, B, C, D, E, F, G, H;  // 32 bytes
        }

        // ПЛОХО — копирование при каждом вызове
        void Process(LargeStruct s) { }

        // ХОРОШО — передача по ссылке, readonly
        void Process(in LargeStruct s) { }

        // Вызов
        var s = new LargeStruct { A = 1 };
        Process(s);  // No copy!

        // in с Span
        void ProcessData(in ReadOnlySpan<byte> data)
        {
            // data нельзя модифицировать
            // но и не копируется
        }

ref struct

// ref struct — может содержать только ref types
        public ref struct SpanEnumerator<T>
        {
            private readonly Span<T> _span;
            private int _index;
    
            public SpanEnumerator(Span<T> span)
            {
                _span = span;
                _index = -1;
            }
    
            public bool MoveNext() => ++_index < _span.Length;
            public ref T Current => ref _span[_index];
        }

        // Использование
        ref struct Parser
        {
            private ReadOnlySpan<char> _input;
            private int _position;
    
            public Parser(ReadOnlySpan<char> input)
            {
                _input = input;
                _position = 0;
            }
    
            public bool TryParseInt(out int value)
            {
                // Zero-allocation parsing
                var span = _input.Slice(_position);
                int end = span.IndexOfAny(' ', ',', ';');
                if (end == -1) end = span.Length;
        
                bool result = int.TryParse(span.Slice(0, end), out value);
                _position += end;
                return result;
            }
        }

ReadOnlySequence

Что такое ReadOnlySequence

ReadOnlySequence<T> — представляет последовательность данных, которая может быть разбита на multiple segments (buffers).

// Создание из одного buffer
        ReadOnlySequence<byte> single = new ReadOnlySequence<byte>(new byte[] { 1, 2, 3 });

        // Создание из multiple buffers
        var segment1 = new byte[] { 1, 2, 3 };
        var segment2 = new byte[] { 4, 5, 6 };

        var seq = new ReadOnlySequence<byte>(
            new BufferSegment(segment1),
            0,
            new BufferSegment(segment2),
            segment2.Length
        );

Stream processing

public static async Task ProcessStreamAsync(Stream stream)
        {
            var pipe = new Pipe();
    
            // Copy stream to pipe
            _ = stream.CopyToAsync(pipe.Writer.AsStream());
    
            while (true)
            {
                ReadResult result = await pipe.Reader.ReadAsync();
                ReadOnlySequence<byte> buffer = result.Buffer;
        
                try
                {
                    while (TryParseMessage(ref buffer, out var message))
                    {
                        ProcessMessage(message);
                    }
            
                    if (result.IsCompleted)
                        break;
                }
                finally
                {
                    // Advance past consumed data
                    pipe.Reader.AdvanceTo(buffer.Start, buffer.End);
                }
            }
        }

        private static bool TryParseMessage(
            ref ReadOnlySequence<byte> buffer, 
            out ReadOnlySequence<byte> message)
        {
            // Find message delimiter
            SequencePosition? position = buffer.PositionOf((byte)'\n');
    
            if (position == null)
            {
                message = default;
                return false;
            }
    
            // Extract message
            message = buffer.Slice(0, position.Value);
            buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            return true;
        }

Практика

Задание 1: JSON Parser без аллокаций

public ref struct JsonParser
        {
            private ReadOnlySpan<char> _input;
            private int _position;
    
            public JsonParser(ReadOnlySpan<char> input)
            {
                _input = input;
                _position = 0;
            }
    
            public bool TryParseString(out ReadOnlySpan<char> value)
            {
                value = default;
        
                SkipWhitespace();
        
                if (_position >= _input.Length || _input[_position] != '"')
                    return false;
        
                _position++;  // Skip opening quote
        
                int start = _position;
                while (_position < _input.Length && _input[_position] != '"')
                {
                    if (_input[_position] == '\\')
                        _position++;  // Skip escaped char
                    _position++;
                }
        
                if (_position >= _input.Length)
                    return false;
        
                value = _input.Slice(start, _position - start);
                _position++;  // Skip closing quote
                return true;
            }
    
            public bool TryParseInt(out int value)
            {
                value = 0;
                SkipWhitespace();
        
                int start = _position;
                while (_position < _input.Length && char.IsDigit(_input[_position]))
                {
                    _position++;
                }
        
                if (start == _position)
                    return false;
        
                return int.TryParse(_input.Slice(start, _position - start), out value);
            }
    
            private void SkipWhitespace()
            {
                while (_position < _input.Length && char.IsWhiteSpace(_input[_position]))
                {
                    _position++;
                }
            }
        }

        // Использование
        var json = "{\"name\":\"John\",\"age\":30}"u8;
        var parser = new JsonParser(json);

        if (parser.TryParseString(out var key1))
        {
            Console.WriteLine($"Key: {key1}");
        }

Задание 2: High-throughput Message Serializer

public class MessageSerializer
        {
            private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
    
            public static byte[] Serialize(Message message)
            {
                int estimatedSize = 64 + message.Payload.Length;
                byte[] buffer = _pool.Rent(estimatedSize);
        
                try
                {
                    Span<byte> span = buffer.AsSpan();
                    int offset = 0;
            
                    // Write header
                    MemoryMarshal.Write(span.Slice(offset), message.Type);
                    offset += sizeof(MessageType);
            
                    MemoryMarshal.Write(span.Slice(offset), message.Payload.Length);
                    offset += sizeof(int);
            
                    // Write payload
                    message.Payload.CopyTo(span.Slice(offset));
                    offset += message.Payload.Length;
            
                    // Return exact copy
                    byte[] result = new byte[offset];
                    span.Slice(0, offset).CopyTo(result);
                    return result;
                }
                finally
                {
                    _pool.Return(buffer);
                }
            }
    
            public static Message Deserialize(ReadOnlySpan<byte> data)
            {
                int offset = 0;
        
                var type = MemoryMarshal.Read<MessageType>(data.Slice(offset));
                offset += sizeof(MessageType);
        
                int length = MemoryMarshal.Read<int>(data.Slice(offset));
                offset += sizeof(int);
        
                byte[] payload = _pool.Rent(length);
                try
                {
                    data.Slice(offset, length).CopyTo(payload);
                    return new Message(type, payload.AsSpan(0, length).ToArray());
                }
                finally
                {
                    _pool.Return(payload);
                }
            }
        }

Задание 3: Оптимизация StringBuilder через Span

// ПЛОХО — StringBuilder аллоцирует internal buffer
        string BuildString(IEnumerable<int> numbers)
        {
            var sb = new StringBuilder();
            foreach (var n in numbers)
            {
                sb.Append(n).Append(',');
            }
            return sb.ToString();
        }

        // ХОРОШО — Span + stackalloc для small outputs
        string BuildStringOptimized(ReadOnlySpan<int> numbers)
        {
            if (numbers.Length == 0)
                return string.Empty;
    
            // Estimate size: up to 10 chars per number + comma
            int estimatedSize = numbers.Length * 11;
    
            Span<char> buffer;
            char[]? pooledBuffer = null;
    
            if (estimatedSize <= 256)
            {
                buffer = stackalloc char[256];
            }
            else
            {
                pooledBuffer = ArrayPool<char>.Shared.Rent(estimatedSize);
                buffer = pooledBuffer.AsSpan(0, estimatedSize);
            }
    
            try
            {
                int pos = 0;
                foreach (var n in numbers)
                {
                    if (!n.TryFormat(buffer.Slice(pos), out int written, default, CultureInfo.InvariantCulture))
                    {
                        // Buffer too small — switch to pooled
                        if (pooledBuffer == null)
                        {
                            pooledBuffer = ArrayPool<char>.Shared.Rent(estimatedSize * 2);
                            buffer.Slice(0, pos).CopyTo(pooledBuffer);
                            buffer = pooledBuffer.AsSpan(0, estimatedSize * 2);
                        }
                    }
                    pos += written;
                    buffer[pos++] = ',';
                }
        
                return new string(buffer.Slice(0, pos - 1));
            }
            finally
            {
                if (pooledBuffer != null)
                {
                    ArrayPool<char>.Shared.Return(pooledBuffer);
                }
            }
        }

Контрольные вопросы

  1. Почему Span<T> нельзя использовать в async методах?
  2. Когда использовать ArrayPool<T> vs stackalloc?
  3. Что такое ref struct и зачем он нужен?
  4. Как ReadOnlySequence<T> помогает с stream processing?
  5. Что делает in parameter?

2.5 Large Object Heap и оптимизация

LOH Fragmentation — Проблема

Почему LOH фрагментируется

LOH по умолчанию не компактируется (до .NET 4.5.1), потому что:

  1. Копирование больших объектов дорого (memory bandwidth)
  2. Большие объекты часто долгоживущие
  3. Исторически LOH считался "rarely collected"
LOH Layout после нескольких аллокаций/освобождений:

        [100KB][50KB freed][200KB][75KB freed][150KB][300KB freed][50KB]
               ↑              ↑                 ↑
               Fragment       Fragment          Fragment

        Total free: 425KB
        Largest contiguous: 300KB
        New 250KB allocation → FAILS (no contiguous space)!

Демонстрация фрагментации

class LohFragmentationDemo
        {
            static void Main()
            {
                Console.WriteLine("=== LOH Fragmentation Demo ===\n");
        
                // Allocate large objects
                var a = new byte[100_000];  // 100KB
                var b = new byte[100_000];  // 100KB
                var c = new byte[100_000];  // 100KB
                var d = new byte[100_000];  // 100KB
                var e = new byte[100_000];  // 100KB
        
                Console.WriteLine($"LOH Size after allocation: {GC.GetGCMemoryInfo().SizeOfHeapInBytes / 1024}KB");
        
                // Free alternating objects
                b = null;
                d = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
        
                Console.WriteLine($"LOH Size after freeing: {GC.GetGCMemoryInfo().SizeOfHeapInBytes / 1024}KB");
                Console.WriteLine($"LOH Fragmentation: {(double)GC.GetGCMemoryInfo().FragmentationAfterBytes / GC.GetGCMemoryInfo().HeapSizeBytes * 100:F1}%");
        
                // Try to allocate 150KB — может fail из-за fragmentation
                try
                {
                    var f = new byte[150_000];  // Needs contiguous space
                    Console.WriteLine("150KB allocation: SUCCESS");
                }
                catch (OutOfMemoryException)
                {
                    Console.WriteLine("150KB allocation: FAILED (fragmentation)");
                }
            }
        }

GCSettings.LargeObjectHeapCompactionMode

Включение компактизации

// .NET 4.5.1+
        GCSettings.LargeObjectHeapCompactionMode = 
            GCLargeObjectHeapCompactionMode.CompactOnce;

        // CompactOnce — компактизирует при следующей Gen 2 collection
        // и сбрасывается в Default

        // Для постоянной компактизации:
        void EnableLohCompaction()
        {
            GCSettings.LargeObjectHeapCompactionMode = 
                GCLargeObjectHeapCompactionMode.CompactOnce;
            GC.Collect(2, GCCollectionMode.Forced, blocking: true);
        }

Когда использовать компактизацию

public class LohManager
        {
            public static void CompactIfNeeded()
            {
                var info = GC.GetGCMemoryInfo();
        
                // Компактизируем если фрагментация > 10%
                if ((double)info.FragmentationAfterBytes / info.HeapSizeBytes > 0.10)
                {
                    GCSettings.LargeObjectHeapCompactionMode = 
                        GCLargeObjectHeapCompactionMode.CompactOnce;
                    GC.Collect(2, GCCollectionMode.Optimized);
                }
            }
        }

.NET 7+ — Ephemeral Strong Mode

<!-- .csproj -->
        <PropertyGroup>
          <GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
        </PropertyGroup>

Или через runtime config:

{
          "runtimeOptions": {
            "gcEphemeralStrongMode": true
          }
        }

Буферизированные vs Небуферизированные потоки

Проблема больших буферов

// ПЛОХО — каждый FileStream аллоцирует buffer в LOH
        async Task CopyFileAsync(string source, string dest)
        {
            using var input = new FileStream(source, FileMode.Open, 
                bufferSize: 1_000_000);  // 1MB buffer → LOH!
            using var output = new FileStream(dest, FileMode.Create,
                bufferSize: 1_000_000);  // 1MB buffer → LOH!
    
            await input.CopyToAsync(output);
        }

Решение: Pooled buffers

// ХОРОШО — используем ArrayPool
        async Task CopyFileOptimizedAsync(string source, string dest)
        {
            using var input = new FileStream(source, FileMode.Open, 
                bufferSize: 4096, useAsync: true);
            using var output = new FileStream(dest, FileMode.Create,
                bufferSize: 4096, useAsync: true);
    
            var pool = ArrayPool<byte>.Shared;
            byte[] buffer = pool.Rent(64 * 1024);  // 64KB from pool
    
            try
            {
                int bytesRead;
                while ((bytesRead = await input.ReadAsync(buffer, 0, buffer.Length)) > 0)
                {
                    await output.WriteAsync(buffer, 0, bytesRead);
                }
            }
            finally
            {
                pool.Return(buffer);
            }
        }

System.IO.Pipelines

// Pipes автоматически управляют буферами
        async Task ProcessPipelineAsync(PipeReader reader, PipeWriter writer)
        {
            while (true)
            {
                ReadResult result = await reader.ReadAsync();
                ReadOnlySequence<byte> buffer = result.Buffer;
        
                try
                {
                    // Process data without copying
                    foreach (var segment in buffer)
                    {
                        ProcessSegment(segment.Span);
                    }
            
                    // Write output
                    var output = writer.GetMemory(4096);
                    int written = GenerateOutput(output.Span);
                    writer.Advance(written);
            
                    if (result.IsCompleted)
                        break;
                }
                finally
                {
                    reader.AdvanceTo(buffer.End);
                }
            }
    
            await writer.CompleteAsync();
        }

BoundedChannel для Backpressure

Проблема unbounded queues

// ПЛОХО — queue может расти бесконечно
        class BadQueue
        {
            private readonly Queue<Message> _queue = new();
    
            public void Enqueue(Message msg)
            {
                _queue.Enqueue(msg);  // No limit → OOM under load!
            }
        }

BoundedChannel решение

using System.Threading.Channels;

        class GoodQueue
        {
            private readonly Channel<Message> _channel;
    
            public GoodQueue(int capacity = 1000)
            {
                _channel = Channel.CreateBounded<Message>(new BoundedChannelOptions(capacity)
                {
                    FullMode = BoundedChannelFullMode.Wait,  // Backpressure
                    SingleReader = true,
                    SingleWriter = false
                });
            }
    
            public async Task EnqueueAsync(Message msg, CancellationToken ct)
            {
                // Waits if channel is full — backpressure!
                await _channel.Writer.WriteAsync(msg, ct);
            }
    
            public async Task ProcessAsync(CancellationToken ct)
            {
                await foreach (var msg in _channel.Reader.ReadAllAsync(ct))
                {
                    await HandleMessageAsync(msg);
                }
            }
        }

BoundedChannel стратегии

// Wait — блокирует writer (default)
        var waitChannel = Channel.CreateBounded<T>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.Wait
        });

        // DropOldest — удаляет oldest message
        var dropOldest = Channel.CreateBounded<T>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.DropOldest
        });

        // DropNewest — удаляет newest message
        var dropNewest = Channel.CreateBounded<T>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.DropNewest
        });

        // DropWrite — silently drops write
        var dropWrite = Channel.CreateBounded<T>(new BoundedChannelOptions(100)
        {
            FullMode = BoundedChannelFullMode.DropWrite
        });

Практика

Задание 1: Демонстрация LOH Fragmentation

class LohFragmentationBenchmark
        {
            [Benchmark]
            public void WithoutCompaction()
            {
                var objects = new List<byte[]>();
        
                // Allocate
                for (int i = 0; i < 100; i++)
                {
                    objects.Add(new byte[100_000]);
                }
        
                // Free every other
                for (int i = 1; i < objects.Count; i += 2)
                {
                    objects[i] = null;
                }
        
                GC.Collect(2);
        
                // Measure fragmentation
                var info = GC.GetGCMemoryInfo();
                Console.WriteLine($"Fragmentation: {(double)info.FragmentationAfterBytes / info.HeapSizeBytes * 100:F1}%");
            }
    
            [Benchmark]
            public void WithCompaction()
            {
                var objects = new List<byte[]>();
        
                // Allocate
                for (int i = 0; i < 100; i++)
                {
                    objects.Add(new byte[100_000]);
                }
        
                // Free every other
                for (int i = 1; i < objects.Count; i += 2)
                {
                    objects[i] = null;
                }
        
                // Enable compaction
                GCSettings.LargeObjectHeapCompactionMode = 
                    GCLargeObjectHeapCompactionMode.CompactOnce;
                GC.Collect(2);
        
                var info = GC.GetGCMemoryInfo();
                Console.WriteLine($"Fragmentation: {(double)info.FragmentationAfterBytes / info.HeapSizeBytes * 100:F1}%");
            }
        }

Задание 2: Streaming Processor с Bounded Memory

public class BoundedMemoryProcessor
        {
            private readonly Channel<byte[]> _inputChannel;
            private readonly Channel<byte[]> _outputChannel;
            private readonly ArrayPool<byte> _pool;
            private readonly int _maxBufferSize;
    
            public BoundedMemoryProcessor(
                int inputCapacity = 100,
                int outputCapacity = 100,
                int maxBufferSize = 64 * 1024)
            {
                _pool = ArrayPool<byte>.Shared;
                _maxBufferSize = maxBufferSize;
        
                _inputChannel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(inputCapacity)
                {
                    FullMode = BoundedChannelFullMode.Wait
                });
        
                _outputChannel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(outputCapacity)
                {
                    FullMode = BoundedChannelFullMode.Wait
                });
            }
    
            public async Task EnqueueAsync(byte[] data, CancellationToken ct)
            {
                if (data.Length > _maxBufferSize)
                    throw new ArgumentException($"Data too large: {data.Length} > {_maxBufferSize}");
        
                await _inputChannel.Writer.WriteAsync(data, ct);
            }
    
            public async Task ProcessAsync(CancellationToken ct)
            {
                await foreach (var data in _inputChannel.Reader.ReadAllAsync(ct))
                {
                    try
                    {
                        var result = ProcessData(data);
                        await _outputChannel.Writer.WriteAsync(result, ct);
                    }
                    finally
                    {
                        if (data.Length >= 85_000)
                        {
                            _pool.Return(data);
                        }
                    }
                }
            }
    
            public IAsyncEnumerable<byte[]> GetResultsAsync(CancellationToken ct)
            {
                return _outputChannel.Reader.ReadAllAsync(ct);
            }
    
            private byte[] ProcessData(byte[] data)
            {
                // Process without allocating large buffers
                var result = _pool.Rent(data.Length);
                try
                {
                    // Transform data
                    data.AsSpan().CopyTo(result);
                    Transform(result.AsSpan(0, data.Length));
            
                    byte[] output = new byte[data.Length];
                    result.AsSpan(0, data.Length).CopyTo(output);
                    return output;
                }
                finally
                {
                    _pool.Return(result);
                }
            }
    
            private void Transform(Span<byte> data)
            {
                // Example transformation
                for (int i = 0; i < data.Length; i++)
                {
                    data[i] = (byte)(data[i] ^ 0xFF);
                }
            }
        }

Задание 3: Benchmark LOH Compaction

using BenchmarkDotNet.Attributes;
        using BenchmarkDotNet.Running;

        [MemoryDiagnoser]
        public class LohCompactionBenchmark
        {
            private List<byte[]>? _objects;
    
            [GlobalSetup]
            public void Setup()
            {
                _objects = new List<byte[]>(100);
            }
    
            [Benchmark(Baseline = true)]
            public void NoCompaction()
            {
                AllocateAndFree();
                GC.Collect(2);
            }
    
            [Benchmark]
            public void WithCompaction()
            {
                AllocateAndFree();
                GCSettings.LargeObjectHeapCompactionMode = 
                    GCLargeObjectHeapCompactionMode.CompactOnce;
                GC.Collect(2);
            }
    
            private void AllocateAndFree()
            {
                _objects!.Clear();
        
                for (int i = 0; i < 100; i++)
                {
                    _objects.Add(new byte[100_000]);
                }
        
                for (int i = 1; i < _objects.Count; i += 2)
                {
                    _objects[i] = null!;
                }
            }
        }

        // Run
        var summary = BenchmarkRunner.Run<LohCompactionBenchmark>();

Контрольные вопросы

  1. Почему LOH не компактируется по умолчанию?
  2. Как включить компактизацию LOH?
  3. Что такое backpressure и как BoundedChannel помогает?
  4. Какой размер buffer оптимален для FileStream?
  5. Как избежать LOH allocations при работе с большими данными?
  6. Использовать ArrayPool<T> для reusable buffers
  7. Использовать System.IO.Pipelines для stream processing
  8. Использовать Span<T> для slicing без копирования
  9. Использовать BoundedChannel для backpressure

2.6 Native Interop и память

Unsafe Code — Pointers, Fixed Buffers, Stackalloc

Pointers в C#

unsafe
        {
            // Указатель на int
            int value = 42;
            int* ptr = &value;
            Console.WriteLine(*ptr);  // 42
    
            // Pointer arithmetic
            int[] arr = { 1, 2, 3, 4, 5 };
            fixed (int* p = arr)
            {
                Console.WriteLine(*(p + 2));  // 3
            }
    
            // Pointer to struct
            Point point = new Point { X = 10, Y = 20 };
            Point* pPoint = &point;
            pPoint->X = 30;  // point.X = 30
        }

        struct Point
        {
            public int X;
            public int Y;
        }

Fixed Buffers

// Fixed buffer в struct (inline array)
        unsafe struct FixedBuffer
        {
            public fixed byte Data[256];  // Inline 256 bytes
            public int Length;
        }

        // Использование
        unsafe
        {
            var buffer = new FixedBuffer();
            buffer.Length = 10;
    
            fixed (byte* p = buffer.Data)
            {
                for (int i = 0; i < buffer.Length; i++)
                {
                    p[i] = (byte)i;
                }
            }
        }

Stackalloc

// Без unsafe (через Span)
        Span<byte> span = stackalloc byte[256];

        // С unsafe (прямые pointers)
        unsafe
        {
            int* numbers = stackalloc int[100];
            numbers[0] = 42;
    
            byte* buffer = stackalloc byte[1024];
            *buffer = 0xFF;
        }

Span с Unmanaged Memory

Создание Span из native памяти

unsafe
        {
            // Alloc native memory
            IntPtr ptr = Marshal.AllocHGlobal(1024);
    
            try
            {
                // Create Span from native memory
                Span<byte> span = new Span<byte>((void*)ptr, 1024);
        
                // Use span
                span[0] = 42;
                span.Fill(0xFF);
        
                // Read back
                Console.WriteLine(span[0]);
            }
            finally
            {
                Marshal.FreeHGlobal(ptr);
            }
        }

NativeMemory (.NET 6+)

using System.Runtime.InteropServices;

        // Allocation
        unsafe
        {
            byte* ptr = (byte*)NativeMemory.Alloc(1024);
            try
            {
                Span<byte> span = new Span<byte>(ptr, 1024);
                span.Fill(42);
            }
            finally
            {
                NativeMemory.Free(ptr);
            }
        }

        // Aligned allocation
        unsafe
        {
            byte* aligned = (byte*)NativeMemory.AlignedAlloc(1024, 64);  // 64-byte aligned
            try
            {
                // Use for SIMD operations
            }
            finally
            {
                NativeMemory.AlignedFree(aligned);
            }
        }

        // Zero-initialized
        unsafe
        {
            int* zeros = (int*)NativeMemory.AllocZeroed(100, sizeof(int));
            try
            {
                // Already zeroed
            }
            finally
            {
                NativeMemory.Free(zeros);
            }
        }

P/Invoke Marshaling

Базовый P/Invoke

[DllImport("kernel32.dll", SetLastError = true)]
        static extern bool ReadFile(
            SafeFileHandle hFile,
            byte[] lpBuffer,
            int nNumberOfBytesToRead,
            out int lpNumberOfBytesRead,
            IntPtr lpOverlapped);

        // Использование
        byte[] buffer = new byte[1024];
        bool success = ReadFile(handle, buffer, buffer.Length, out int bytesRead, IntPtr.Zero);

Marshaling Problems

// ПЛОХО — marshaling копирует данные
        [DllImport("native.dll")]
        static extern void ProcessData(
            [MarshalAs(UnmanagedType.LPArray)] byte[] data,  // Copy!
            int length);

        // ХОРОШО — без копирования
        unsafe
        {
            [DllImport("native.dll")]
            static extern void ProcessData(byte* data, int length);
    
            void Call(byte[] data)
            {
                fixed (byte* p = data)
                {
                    ProcessData(p, data.Length);  // No copy!
                }
            }
        }

Span с P/Invoke

unsafe class NativeInterop
        {
            [DllImport("native.dll")]
            private static extern int NativeProcess(byte* data, int length);
    
            public static int Process(Span<byte> data)
            {
                fixed (byte* p = &MemoryMarshal.GetReference(data))
                {
                    // Handle empty span
                    byte dummy = 0;
                    byte* ptr = (p != null) ? p : &dummy;
                    return NativeProcess(ptr, data.Length);
                }
            }
        }

String Marshaling

// ПЛОХО — automatic marshaling allocates
        [DllImport("native.dll", CharSet = CharSet.Unicode)]
        static extern void ProcessString(string input);  // Allocates native copy

        // ХОРОШО — manual control
        unsafe
        {
            [DllImport("native.dll")]
            static extern void ProcessString(char* input, int length);
    
            static void Call(ReadOnlySpan<char> input)
            {
                fixed (char* p = input)
                {
                    ProcessString(p, input.Length);
                }
            }
        }

MemoryMarshal и Unsafe Class

MemoryMarshal

using System.Runtime.InteropServices;

        // Cast Span<T> to Span<U> (reinterpret)
        Span<int> ints = stackalloc int[10];
        Span<byte> bytes = MemoryMarshal.AsBytes(ints);  // 40 bytes

        // Cast back
        Span<int> ints2 = MemoryMarshal.Cast<byte, int>(bytes);

        // Get reference to first element
        ref int first = ref MemoryMarshal.GetReference(ints);

        // Create Span from reference
        Span<int> span = MemoryMarshal.CreateSpan(ref first, 10);

        // Read/Write without bounds checking
        int value = MemoryMarshal.Read<int>(bytes.Slice(0));
        MemoryMarshal.Write<int>(bytes.Slice(0), 42);

        // Get array segment
        bool success = MemoryMarshal.TryGetArray<byte>(memory, out ArraySegment<byte> segment);

        // String as Span<char>
        ReadOnlySpan<char> span = "hello".AsSpan();

        // TryRead for safe reading
        if (MemoryMarshal.TryRead<int>(bytes, out int val))
        {
            Console.WriteLine(val);
        }

Unsafe Class

using System.Runtime.CompilerServices;

        // Bit-cast (reinterpret)
        float f = 3.14f;
        int i = Unsafe.As<float, int>(ref f);

        // Ref cast
        object obj = "hello";
        ref string s = ref Unsafe.As<object, string>(ref obj);

        // Pointer operations
        unsafe
        {
            int* ptr = stackalloc int[1];
            *ptr = 42;
    
            // Read/Write through pointer
            int value = Unsafe.Read<int>(ptr);
            Unsafe.Write(ptr, 100);
        }

        // SizeOf
        int size = Unsafe.SizeOf<int>();  // 4

        // Add to pointer (like pointer arithmetic)
        unsafe
        {
            int* arr = stackalloc int[10];
            int* elem3 = Unsafe.Add(arr, 3);  // arr + 3
            int* elem5 = Unsafe.Add(ref *arr, 5);  // arr + 5
        }

        // Copy block
        unsafe
        {
            byte* src = stackalloc byte[100];
            byte* dst = stackalloc byte[100];
            Unsafe.CopyBlock(dst, src, 100);
        }

        // Init block (memset)
        unsafe
        {
            byte* buffer = stackalloc byte[100];
            Unsafe.InitBlock(buffer, 0, 100);  // Zero out
        }

MemoryMarshal + Unsafe Patterns

// Fast struct serialization
        unsafe
        {
            public static byte[] Serialize<T>(T value) where T : unmanaged
            {
                byte[] result = new byte[Unsafe.SizeOf<T>()];
                fixed (byte* p = result)
                {
                    Unsafe.Write(p, value);
                }
                return result;
            }
    
            public static T Deserialize<T>(ReadOnlySpan<byte> data) where T : unmanaged
            {
                if (data.Length < Unsafe.SizeOf<T>())
                    throw new ArgumentException("Data too small");
        
                fixed (byte* p = &MemoryMarshal.GetReference(data))
                {
                    return Unsafe.Read<T>(p);
                }
            }
        }

        // Zero-copy parsing
        public static ReadOnlySpan<byte> ExtractField(ReadOnlySpan<byte> data, int offset, int length)
        {
            if (offset + length > data.Length)
                throw new ArgumentOutOfRangeException();
    
            return data.Slice(offset, length);
        }

Практика

Задание 1: High-Performance Binary Protocol Parser

public ref struct BinaryParser
        {
            private ReadOnlySpan<byte> _data;
            private int _position;
    
            public BinaryParser(ReadOnlySpan<byte> data)
            {
                _data = data;
                _position = 0;
            }
    
            public bool TryReadByte(out byte value)
            {
                if (_position >= _data.Length)
                {
                    value = 0;
                    return false;
                }
        
                value = _data[_position++];
                return true;
            }
    
            public bool TryReadInt32(out int value)
            {
                if (_position + sizeof(int) > _data.Length)
                {
                    value = 0;
                    return false;
                }
        
                value = MemoryMarshal.Read<int>(_data.Slice(_position));
                _position += sizeof(int);
                return true;
            }
    
            public bool TryReadString(out ReadOnlySpan<byte> value)
            {
                value = default;
        
                if (!TryReadInt32(out int length) || length < 0)
                    return false;
        
                if (_position + length > _data.Length)
                    return false;
        
                value = _data.Slice(_position, length);
                _position += length;
                return true;
            }
    
            public unsafe bool TryReadStruct<T>(out T value) where T : unmanaged
            {
                value = default;
        
                int size = Unsafe.SizeOf<T>();
                if (_position + size > _data.Length)
                    return false;
        
                fixed (byte* p = &MemoryMarshal.GetReference(_data.Slice(_position)))
                {
                    value = Unsafe.Read<T>(p);
                }
        
                _position += size;
                return true;
            }
        }

        // Protocol: [type:byte][length:int][payload:bytes]
        public enum MessageType : byte
        {
            Text = 1,
            Binary = 2,
            Command = 3
        }

        public static Message ParseMessage(ReadOnlySpan<byte> data)
        {
            var parser = new BinaryParser(data);
    
            if (!parser.TryReadByte(out byte typeByte))
                throw new InvalidDataException("Missing type");
    
            var type = (MessageType)typeByte;
    
            if (!parser.TryReadInt32(out int length))
                throw new InvalidDataException("Missing length");
    
            if (!parser.TryReadString(out var payload))
                throw new InvalidDataException("Missing payload");
    
            return new Message(type, payload.ToArray());
        }

Задание 2: Оптимизация P/Invoke

// ПЛОХО — marshaling overhead
        class BadNative
        {
            [DllImport("lib.dll")]
            public static extern void ProcessArray(
                [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] 
                double[] data,
                int length);
    
            public static void Call(double[] data)
            {
                ProcessArray(data, data.Length);  // Copies array to native!
            }
        }

        // ХОРОШО — no marshaling
        class GoodNative
        {
            [DllImport("lib.dll")]
            private static extern unsafe void ProcessArrayNative(
                double* data,
                int length);
    
            public static unsafe void Call(Span<double> data)
            {
                fixed (double* p = &MemoryMarshal.GetReference(data))
                {
                    ProcessArrayNative(p, data.Length);  // No copy!
                }
            }
        }

        // BEST — with ArrayPool for large data
        class BestNative
        {
            private static readonly ArrayPool<double> _pool = ArrayPool<double>.Shared;
    
            [DllImport("lib.dll")]
            private static extern unsafe void ProcessArrayNative(
                double* data,
                int length);
    
            public static unsafe void Process(ReadOnlySpan<double> input, Span<double> output)
            {
                if (input.Length != output.Length)
                    throw new ArgumentException("Length mismatch");
        
                // For small data, use stack
                if (input.Length <= 256)
                {
                    Span<double> buffer = stackalloc double[input.Length];
                    input.CopyTo(buffer);
            
                    fixed (double* p = &MemoryMarshal.GetReference(buffer))
                    {
                        ProcessArrayNative(p, buffer.Length);
                    }
            
                    buffer.CopyTo(output);
                }
                else
                {
                    // For large data, use pinned array
                    double[] buffer = _pool.Rent(input.Length);
                    try
                    {
                        input.CopyTo(buffer);
                
                        fixed (double* p = buffer)
                        {
                            ProcessArrayNative(p, input.Length);
                        }
                
                        buffer.AsSpan(0, input.Length).CopyTo(output);
                    }
                    finally
                    {
                        _pool.Return(buffer);
                    }
                }
            }
        }

Задание 3: Ring Buffer на Fixed-Size Buffers

public unsafe class RingBuffer<T> where T : unmanaged
        {
            private readonly T* _buffer;
            private readonly int _capacity;
            private int _head;
            private int _tail;
            private int _count;
    
            public RingBuffer(int capacity)
            {
                _capacity = capacity;
                _buffer = (T*)NativeMemory.Alloc((nuint)(capacity * Unsafe.SizeOf<T>()));
                _head = 0;
                _tail = 0;
                _count = 0;
            }
    
            public void Enqueue(T item)
            {
                if (_count == _capacity)
                    throw new InvalidOperationException("Buffer full");
        
                _buffer[_tail] = item;
                _tail = (_tail + 1) % _capacity;
                _count++;
            }
    
            public T Dequeue()
            {
                if (_count == 0)
                    throw new InvalidOperationException("Buffer empty");
        
                T item = _buffer[_head];
                _head = (_head + 1) % _capacity;
                _count--;
                return item;
            }
    
            public Span<T> AsSpan()
            {
                return new Span<T>(_buffer, _count);
            }
    
            public void Dispose()
            {
                NativeMemory.Free(_buffer);
            }
        }

        // Usage
        using var ringBuffer = new RingBuffer<int>(1024);
        ringBuffer.Enqueue(42);
        ringBuffer.Enqueue(100);

        int value = ringBuffer.Dequeue();  // 42

Контрольные вопросы

  1. Почему fixed нужен при работе с arrays в unsafe code?
  2. Когда использовать MemoryMarshal.AsBytes?
  3. Что делает Unsafe.As<TFrom, TTo>?
  4. Как избежать marshaling overhead в P/Invoke?
  5. Зачем нужен NativeMemory вместо Marshal.AllocHGlobal?

2.7 Продвинутые техники оптимизации памяти

Object Layout и Object Header

Структура объекта в .NET

Каждый объект в .NET имеет overhead:

Object Layout (64-bit):
        ┌─────────────────────┐
        │  Method Table Ptr   │  8 bytes — тип объекта
        ├─────────────────────┤
        │  Sync Block Index   │  8 bytes — для lock/Monitor
        ├─────────────────────┤
        │  Instance Fields    │  N bytes — данные
        ├─────────────────────┤
        │  Padding            │  0-7 bytes — выравнивание
        └─────────────────────┘

        Minimum object size: 24 bytes (64-bit)

Измерение размера объекта

using System.Runtime.CompilerServices;

        // Approximate size
        long GetObjectSize(object obj)
        {
            return GC.GetTotalObjectSize(obj);  // .NET 10+
        }

        // Manual calculation
        struct SmallStruct
        {
            public byte A;    // 1 byte
            public int B;     // 4 bytes
            public byte C;    // 1 byte
        }
        // Size: 12 bytes (1 + 3 padding + 4 + 1 + 3 padding)

        struct OptimizedStruct
        {
            public int B;     // 4 bytes
            public byte A;    // 1 byte
            public byte C;    // 1 byte
        }
        // Size: 8 bytes (4 + 1 + 1 + 2 padding)

StructLayout, Pack, FieldOffset

StructLayout

using System.Runtime.InteropServices;

        // Auto (default) — CLR решает
        [StructLayout(LayoutKind.Auto)]
        struct AutoStruct { }

        // Sequential — поля в порядке объявления
        [StructLayout(LayoutKind.Sequential)]
        struct SequentialStruct
        {
            public int A;
            public byte B;
            public short C;
        }

        // Explicit — ручной контроль
        [StructLayout(LayoutKind.Explicit)]
        struct ExplicitStruct
        {
            [FieldOffset(0)] public int Value;
            [FieldOffset(0)] public byte Byte1;
            [FieldOffset(1)] public byte Byte2;
            [FieldOffset(2)] public byte Byte3;
            [FieldOffset(3)] public byte Byte4;
        }

        // Usage — union-like behavior
        var s = new ExplicitStruct { Value = 0x01020304 };
        Console.WriteLine(s.Byte1);  // 4 (little-endian)
        Console.WriteLine(s.Byte2);  // 3

Pack — выравнивание

// Pack = 1 — no padding
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct PackedStruct
        {
            public byte A;    // 1 byte
            public int B;     // 4 bytes
            public byte C;    // 1 byte
        }
        // Total: 6 bytes (no padding)

        // Pack = 4 (default for most)
        [StructLayout(LayoutKind.Sequential, Pack = 4)]
        struct DefaultStruct
        {
            public byte A;    // 1 byte + 3 padding
            public int B;     // 4 bytes
            public byte C;    // 1 byte + 3 padding
        }
        // Total: 12 bytes

        // Pack = 8
        [StructLayout(LayoutKind.Sequential, Pack = 8)]
        struct AlignedStruct
        {
            public byte A;    // 1 byte + 7 padding
            public long B;    // 8 bytes
            public byte C;    // 1 byte + 7 padding
        }
        // Total: 24 bytes

FieldOffset для оптимизации

// Optimized layout — минимальный padding
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct OptimizedMessage
        {
            public int Id;           // 4 bytes
            public short Type;       // 2 bytes
            public byte Priority;    // 1 byte
            public byte Flags;       // 1 byte
            public long Timestamp;   // 8 bytes
        }
        // Total: 16 bytes

        // Unoptimized — больше padding
        [StructLayout(LayoutKind.Sequential)]
        struct UnoptimizedMessage
        {
            public byte Priority;    // 1 + 7 padding
            public long Timestamp;   // 8
            public int Id;           // 4
            public short Type;       // 2 + 2 padding
            public byte Flags;       // 1 + 3 padding
        }
        // Total: 32 bytes (2x larger!)

readonly struct и Inlining Optimizations

readonly struct

// readonly struct гарантирует immutability
        readonly struct Point
        {
            public int X { get; }
            public int Y { get; }
    
            public Point(int x, int y)
            {
                X = x;
                Y = y;
            }
        }

        // Преимущества:
        // 1. JIT может inline методы агрессивнее
        // 2. Можно использовать с `in` parameters без defensive copy
        // 3. Compiler предотвращает accidental mutation

Defensive Copy Problem

struct MutablePoint
        {
            public int X;
            public int Y;
        }

        class Container
        {
            private MutablePoint _point;
    
            public MutablePoint Point => _point;  // Property returns COPY
    
            public void Test()
            {
                Point.X = 10;  // Modifies COPY, not _point!
            }
        }

        // Solution: readonly struct
        readonly struct ImmutablePoint
        {
            public int X { get; }
            public int Y { get; }
        }

        class Container2
        {
            private ImmutablePoint _point;
    
            public ImmutablePoint Point => _point;  // No defensive copy needed!
        }

ref readonly returns

readonly struct LargeData
        {
            public readonly int A, B, C, D, E, F, G, H;
        }

        class DataStore
        {
            private LargeData _data;
    
            // ref readonly — no copy, no mutation
            public ref readonly LargeData GetData()
            {
                return ref _data;
            }
        }

        // Usage
        var store = new DataStore();
        ref readonly var data = ref store.GetData();
        Console.WriteLine(data.A);  // No copy!
        // data.A = 10;  // Error: readonly

JIT Optimizations

Devirtualization

// JIT может devirtualize calls когда тип известен
        interface IProcessor
        {
            void Process();
        }

        sealed class FastProcessor : IProcessor
        {
            public void Process() { }  // sealed — JIT знает точный тип
        }

        void CallProcessor(IProcessor processor)
        {
            processor.Process();  // Virtual call
    
            if (processor is FastProcessor fast)
            {
                fast.Process();  // Devirtualized — direct call!
            }
        }

Inlining

// JIT автоматически inlines small methods
        int Add(int a, int b) => a + b;  // Will be inlined

        // Force inline
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        int FastAdd(int a, int b) => a + b;

        // Prevent inline (для debugging)
        [MethodImpl(MethodImplOptions.NoInlining)]
        void DebugMethod() { }

        // Inlining conditions:
        // ✅ Small methods (< 32 bytes IL)
        // ✅ Non-virtual methods
        // ✅ Methods without try/catch
        // ✅ Methods without loops (sometimes)
        // ❌ Virtual methods (unless devirtualized)
        // ❌ Methods with EH clauses
        // ❌ Large methods

Escape Analysis

// Escape analysis определяет, может ли объект "сбежать" из метода
        void NoEscape()
        {
            var obj = new object();  // Doesn't escape
            obj.ToString();
        } // obj can be stack-allocated (theoretical)

        void Escapes()
        {
            var obj = new object();
            Store(obj);  // obj escapes — must be heap-allocated
        }

        void Store(object obj) { }

        // .NET JIT currently does NOT do escape analysis for stack allocation
        // But it does use it for:
        // - Register allocation
        // - Dead code elimination
        // - Lock elision

Lock Elision

// JIT может eliminate locks для non-escaping objects
        void LockElisionExample()
        {
            var obj = new object();  // Doesn't escape
    
            lock (obj)  // May be elided by JIT
            {
                // Critical section
            }
        }

Renting vs Allocating Patterns

Object Pooling

public class PooledObject<T> : IDisposable where T : class, new()
        {
            private static readonly ObjectPool<T> _pool = 
                new DefaultObjectPool<T>(new DefaultPooledObjectPolicy<T>());
    
            private T? _value;
            private bool _disposed;
    
            public T Value => _value ??= _pool.Get();
    
            public void Dispose()
            {
                if (!_disposed)
                {
                    if (_value != null)
                    {
                        _pool.Return(_value);
                        _value = null;
                    }
                    _disposed = true;
                }
            }
        }

        // Usage
        using var pooled = new PooledObject<ExpensiveObject>();
        pooled.Value.DoWork();

Renting Pattern для High-Load

public class RentingProcessor
        {
            private static readonly ArrayPool<byte> _bytePool = ArrayPool<byte>.Shared;
            private static readonly ArrayPool<char> _charPool = ArrayPool<char>.Shared;
            private static readonly ArrayPool<int> _intPool = ArrayPool<int>.Shared;
    
            public void Process(Stream input, Stream output)
            {
                byte[] readBuffer = _bytePool.Rent(64 * 1024);
                byte[] writeBuffer = _bytePool.Rent(64 * 1024);
                char[] charBuffer = _charPool.Rent(32 * 1024);
                int[] indexBuffer = _intPool.Rent(1024);
        
                try
                {
                    // Process using rented buffers
                    int bytesRead = input.Read(readBuffer, 0, readBuffer.Length);
            
                    // Transform
                    Transform(readBuffer.AsSpan(0, bytesRead), writeBuffer);
            
                    // Output
                    output.Write(writeBuffer, 0, bytesRead);
                }
                finally
                {
                    _bytePool.Return(readBuffer);
                    _bytePool.Return(writeBuffer);
                    _charPool.Return(charBuffer);
                    _intPool.Return(indexBuffer);
                }
            }
    
            private void Transform(ReadOnlySpan<byte> input, Span<byte> output)
            {
                // Zero-allocation transform
                for (int i = 0; i < input.Length; i++)
                {
                    output[i] = (byte)(input[i] ^ 0xFF);
                }
            }
        }

Slab Allocation

// Slab allocator для fixed-size objects
        public class SlabAllocator<T> where T : class, new()
        {
            private const int SlabSize = 1024;
            private readonly Stack<T> _freeList = new();
            private int _slabIndex;
            private T[]?[] _slabs = new T[16][];
    
            public T Allocate()
            {
                if (_freeList.Count > 0)
                    return _freeList.Pop();
        
                if (_slabIndex >= _slabs.Length)
                    Array.Resize(ref _slabs, _slabs.Length * 2);
        
                if (_slabs[_slabIndex] == null)
                    _slabs[_slabIndex] = new T[SlabSize];
        
                var slab = _slabs[_slabIndex]!;
                int itemIndex = slab.Length - SlabSize + (_slabIndex * SlabSize) % SlabSize;
        
                if (itemIndex >= SlabSize)
                {
                    _slabIndex++;
                    return Allocate();
                }
        
                return slab[itemIndex] ??= new T();
            }
    
            public void Return(T obj)
            {
                _freeList.Push(obj);
            }
        }

Практика

Задание 1: Оптимизация структуры данных

// BEFORE — неоптимальный layout
        [StructLayout(LayoutKind.Auto)]
        struct UnoptimizedRecord
        {
            public bool IsActive;       // 1 byte + 7 padding
            public long Id;             // 8 bytes
            public string Name;         // 8 bytes (reference)
            public short Priority;      // 2 bytes + 6 padding
            public int Score;           // 4 bytes + 4 padding
            public DateTime Created;    // 8 bytes
            public byte Flags;          // 1 byte + 7 padding
        }
        // Total: ~56 bytes + string object

        // AFTER — оптимизированный layout
        [StructLayout(LayoutKind.Sequential, Pack = 8)]
        struct OptimizedRecord
        {
            public long Id;             // 8 bytes
            public DateTime Created;    // 8 bytes
            public string Name;         // 8 bytes (reference)
            public int Score;           // 4 bytes
            public short Priority;      // 2 bytes
            public byte Flags;          // 1 byte
            public bool IsActive;       // 1 byte
        }
        // Total: 40 bytes + string object (30% smaller!)

        // BEST — для serialization
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        struct PackedRecord
        {
            public long Id;             // 8 bytes
            public long CreatedTicks;   // 8 bytes (instead of DateTime)
            public int Score;           // 4 bytes
            public short Priority;      // 2 bytes
            public byte Flags;          // 1 byte
            public bool IsActive;       // 1 byte
        }
        // Total: 24 bytes (no reference overhead!)

Задание 2: Arena Allocator

// Arena allocator для batch processing
        public sealed class ArenaAllocator : IDisposable
        {
            private readonly List<byte[]> _slabs = new();
            private readonly int _slabSize;
            private int _offset;
            private bool _disposed;
    
            public ArenaAllocator(int slabSize = 64 * 1024)
            {
                _slabSize = slabSize;
                _slabs.Add(new byte[slabSize]);
                _offset = 0;
            }
    
            public unsafe T* Allocate<T>() where T : unmanaged
            {
                int size = Unsafe.SizeOf<T>();
        
                if (_offset + size > _slabSize)
                {
                    _slabs.Add(new byte[_slabSize]);
                    _offset = 0;
                }
        
                var slab = _slabs[_slabs.Count - 1];
                fixed (byte* p = slab)
                {
                    T* ptr = (T*)(p + _offset);
                    _offset += size;
                    return ptr;
                }
            }
    
            public unsafe Span<T> AllocateSpan<T>(int count) where T : unmanaged
            {
                int size = count * Unsafe.SizeOf<T>();
        
                if (_offset + size > _slabSize)
                {
                    _slabs.Add(new byte[Math.Max(_slabSize, size)]);
                    _offset = 0;
                }
        
                var slab = _slabs[_slabs.Count - 1];
                fixed (byte* p = slab)
                {
                    Span<T> span = new Span<T>(p + _offset, count);
                    _offset += size;
                    return span;
                }
            }
    
            public void Reset()
            {
                _offset = 0;
                if (_slabs.Count > 1)
                {
                    _slabs.RemoveRange(1, _slabs.Count - 1);
                }
            }
    
            public void Dispose()
            {
                if (!_disposed)
                {
                    _slabs.Clear();
                    _disposed = true;
                }
            }
        }

        // Usage
        using var arena = new ArenaAllocator();

        // Allocate multiple objects without individual allocations
        unsafe
        {
            var point1 = arena.Allocate<Point>();
            point1->X = 10;
            point1->Y = 20;
    
            var point2 = arena.Allocate<Point>();
            point2->X = 30;
            point2->Y = 40;
    
            // Allocate array
            Span<int> numbers = arena.AllocateSpan<int>(100);
            numbers.Fill(42);
        }

        // All memory freed at once when arena is disposed

Задание 3: Escape Analysis через JIT Dumps

// Для анализа escape analysis используйте:
        // DOTNET_JitDisasm=* Program.exe
        // или
        // DOTNET_JitDump=* Program.exe

        class EscapeAnalysisDemo
        {
            // Этот объект НЕ escape — может быть оптимизирован
            static void NoEscape()
            {
                var obj = new object();
                Console.WriteLine(obj.GetHashCode());
            }
    
            // Этот объект ESCAPE — должен быть в куче
            static void Escapes(object container)
            {
                var obj = new object();
                Store(container, obj);
            }
    
            static void Store(object container, object obj) { }
    
            // Lock elision candidate
            static void LockElision()
            {
                var sync = new object();
                lock (sync)  // JIT может eliminate этот lock
                {
                    Console.WriteLine("Inside lock");
                }
            }
        }

        // Запуск с JIT disassembly:
        // DOTNET_TieredCompilation=0 DOTNET_JitDisasm=* dotnet run

Контрольные вопросы

  1. Почему порядок полей в struct влияет на размер?
  2. Что такое defensive copy и как его избежать?
  3. Когда JIT может devirtualize вызов?
  4. Что такое arena allocator и когда его использовать?
  5. Как уменьшить размер объекта в куче?
  6. Использовать struct вместо class для small data
  7. Оптимизировать порядок полей (большие поля primero)
  8. Использовать Pack = 1 для serialization structs
  9. Избегать unnecessary references

2.8 Memory-Constrained Architectures

Serverless / Container Memory Limits

Особенности constrained environments

┌─────────────────────────────────────────────────┐
        │              Memory Constraints                   │
        ├──────────────────┬──────────────────────────────┤
        │  Serverless      │  Containers                  │
        │  (AWS Lambda)    │  (Docker/K8s)                │
        ├──────────────────┼──────────────────────────────┤
        │  128MB - 10GB    │  64MB - N                    │
        │  Cold starts     │  Steady state                  │
        │  Short-lived     │  Long-running                  │
        │  Max duration    │  OOM kills                     │
        └──────────────────┴──────────────────────────────┘

.NET в Serverless

// AWS Lambda — минимизируйте memory footprint
        public class Function
        {
            // ПЛОХО — static cache занимает память
            private static readonly Dictionary<string, object> _cache = new();
    
            // ХОРОШО — без cache, minimal allocations
            public async Task<APIGatewayProxyResponse> FunctionHandler(
                APIGatewayProxyRequest request,
                ILambdaContext context)
            {
                // Use Span<T> for processing
                var body = request.Body;
                ReadOnlySpan<char> span = body.AsSpan();
        
                // Process without allocations
                var result = Process(span);
        
                return new APIGatewayProxyResponse
                {
                    StatusCode = 200,
                    Body = result
                };
            }
        }

Container Memory Limits

# Dockerfile — настройка GC для контейнера
        FROM mcr.microsoft.com/dotnet/aspnet:8.0

        # Set GC mode for containers
        ENV DOTNET_gcServer=0
        ENV DOTNET_GCHeapHardLimit=0x10000000  # 256MB

        COPY . /app
        WORKDIR /app

        ENTRYPOINT ["dotnet", "MyApp.dll"]
# Kubernetes — memory limits
        apiVersion: v1
        kind: Pod
        metadata:
          name: myapp
        spec:
          containers:
          - name: myapp
            image: myapp:latest
            resources:
              limits:
                memory: "256Mi"
              requests:
                memory: "128Mi"
            env:
            - name: DOTNET_gcServer
              value: "0"
            - name: DOTNET_GCHeapHardLimit
              value: "0x10000000"

GC Tuning для Constrained Environments

ServerGC vs WorkstationGC

<!-- .csproj -->
        <PropertyGroup>
          <!-- Для контейнеров < 512MB — Workstation GC -->
          <ServerGarbageCollection>false</ServerGarbageCollection>
  
          <!-- Для контейнеров > 1GB — Server GC -->
          <!-- <ServerGarbageCollection>true</ServerGarbageCollection> -->
  
          <!-- Concurrent GC для low latency -->
          <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
  
          <!-- Retain VM — не возвращать память OS (для steady state) -->
          <RetainVMGarbageCollection>false</RetainVMGarbageCollection>
        </PropertyGroup>

Environment Variables

# Server GC (для multi-core, > 1GB)
        DOTNET_gcServer=1

        # Workstation GC (для single-core, < 512MB)
        DOTNET_gcServer=0

        # Concurrent GC
        DOTNET_gcConcurrent=1

        # Heap hard limit (hex bytes)
        DOTNET_GCHeapHardLimit=0x10000000  # 256MB

        # Heap hard limit percent (of container limit)
        DOTNET_GCHeapHardLimitPercent=80  # 80% of container limit

        # Retain VM (keep memory for reuse)
        DOTNET_GCRetainVM=0  # Return memory to OS

        # LOH compaction
        DOTNET_gcLOHCompaction=1

        # No GC region (для critical sections)
        DOTNET_NoGCRegion=0

HeapAffinity

// HeapAffinity — привязка GC heaps к specific cores
        // Полезно для NUMA systems

        // runtimeconfig.json
        {
          "runtimeOptions": {
            "gcHeapAffinitizeMask": 0x0000000F,  // Cores 0-3
            "gcHeapCount": 4,                     // 4 heaps
            "gcNoGCRegionMode": 0
          }
        }

Ephemeral Strong Mode

Что такое Ephemeral Strong Mode

Ephemeral Strong Mode — режим GC (.NET 7+), который оптимизирует Gen 0/1 коллекции для low-memory environments.

<!-- .csproj -->
        <PropertyGroup>
          <GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
        </PropertyGroup>

Преимущества:

  • Меньше Gen 2 collections
  • Better memory utilization в constrained environments
  • Faster Gen 0/1 collections

Когда использовать

Use Ephemeral Strong Mode when:
        ├── Container memory < 512MB
        ├── High allocation rate
        ├── Short-lived objects dominate
        └── Need to minimize Gen 2 collections

Hybrid GC Modes (.NET 8+)

GC Mode Selection

┌─────────────────────────────────────────────────┐
        │              GC Mode Selection                    │
        ├─────────────────────────────────────────────────┤
        │  Memory < 256MB   → Workstation, Concurrent     │
        │  Memory 256-1GB   → Workstation, Ephemeral Strong│
        │  Memory 1-4GB     → Server, Concurrent          │
        │  Memory > 4GB     → Server, Concurrent, RetainVM│
        └─────────────────────────────────────────────────┘

Dynamic GC Configuration

public static class GcConfigurator
        {
            public static void ConfigureForEnvironment()
            {
                long totalMemory = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
        
                if (totalMemory <= 256 * 1024 * 1024)  // 256MB
                {
                    // Low memory — workstation GC
                    Console.WriteLine("Configuring for low memory");
                    // Set via runtimeconfig.json or env vars
                }
                else if (totalMemory <= 1024 * 1024 * 1024)  // 1GB
                {
                    // Medium memory — workstation with ephemeral strong
                    Console.WriteLine("Configuring for medium memory");
                }
                else
                {
                    // High memory — server GC
                    Console.WriteLine("Configuring for high memory");
                }
            }
        }

Практика

Задание 1: Настройка GC для контейнера 256MB

# Dockerfile
        FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine

        # Memory-optimized GC settings
        ENV DOTNET_gcServer=0
        ENV DOTNET_gcConcurrent=1
        ENV DOTNET_GCHeapHardLimitPercent=80
        ENV DOTNET_GCLOHCompaction=1
        ENV DOTNET_GCHardLimit=0x10000000

        # Disable features we don't need
        ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
        ENV DOTNET_SYSTEM_NET_HTTP_USESOCKETSHTTPHANDLER=0

        COPY . /app
        WORKDIR /app

        ENTRYPOINT ["dotnet", "MyApp.dll"]
// runtimeconfig.template.json
        {
          "runtimeOptions": {
            "gcServer": false,
            "gcConcurrent": true,
            "gcHeapHardLimitPercent": 80,
            "gcLargeObjectHeapCompaction": true,
            "garbageCollectionAdaptationMode": 1
          }
        }

Задание 2: Graceful Degradation при Memory Pressure

public class MemoryAwareService
        {
            private readonly ILogger _logger;
            private readonly int _memoryThreshold;
            private volatile bool _isUnderPressure;
    
            public MemoryAwareService(ILogger logger, int memoryThresholdMB = 200)
            {
                _logger = logger;
                _memoryThreshold = memoryThresholdMB * 1024 * 1024;
        
                // Start memory monitoring
                _ = MonitorMemoryAsync(CancellationToken.None);
            }
    
            public async Task<Response> ProcessRequestAsync(Request request)
            {
                if (_isUnderPressure)
                {
                    _logger.LogWarning("Memory pressure — using degraded mode");
                    return await ProcessDegradedAsync(request);
                }
        
                return await ProcessNormalAsync(request);
            }
    
            private async Task<Response> ProcessNormalAsync(Request request)
            {
                // Full processing with caching
                var cache = GetCache();
                var result = await HeavyProcessing(request, cache);
                return result;
            }
    
            private async Task<Response> ProcessDegradedAsync(Request request)
            {
                // Degraded processing — no cache, minimal allocations
                var result = await LightweightProcessing(request);
                return result;
            }
    
            private async Task MonitorMemoryAsync(CancellationToken ct)
            {
                while (!ct.IsCancellationRequested)
                {
                    long workingSet = Environment.WorkingSet;
            
                    if (workingSet > _memoryThreshold && !_isUnderPressure)
                    {
                        _isUnderPressure = true;
                        _logger.LogWarning($"Memory pressure detected: {workingSet / 1024 / 1024}MB");
                
                        // Trigger GC to try to free memory
                        GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized);
                    }
                    else if (workingSet < _memoryThreshold * 0.7 && _isUnderPressure)
                    {
                        _isUnderPressure = false;
                        _logger.LogInformation($"Memory pressure relieved: {workingSet / 1024 / 1024}MB");
                    }
            
                    await Task.Delay(TimeSpan.FromSeconds(5), ct);
                }
            }
        }

Задание 3: Load Test для Memory Constraints

using System.Diagnostics;

        public class MemoryConstraintLoadTest
        {
            private readonly int _maxMemoryMB;
            private readonly int _durationSeconds;
            private readonly int _requestsPerSecond;
    
            public MemoryConstraintLoadTest(int maxMemoryMB, int durationSeconds, int rps)
            {
                _maxMemoryMB = maxMemoryMB;
                _durationSeconds = durationSeconds;
                _requestsPerSecond = rps;
            }
    
            public async Task<LoadTestResult> RunAsync(Func<Task> requestHandler)
            {
                var cts = new CancellationTokenSource(_durationSeconds * 1000);
                var results = new List<RequestResult>();
                var memorySamples = new List<MemorySample>();
        
                // Memory monitoring task
                var monitorTask = Task.Run(async () =>
                {
                    while (!cts.Token.IsCancellationRequested)
                    {
                        memorySamples.Add(new MemorySample(
                            DateTime.UtcNow,
                            Environment.WorkingSet,
                            GC.GetTotalMemory(false)));
                
                        await Task.Delay(100, cts.Token);
                    }
                });
        
                // Load generation task
                var loadTask = Task.Run(async () =>
                {
                    var interval = TimeSpan.FromSeconds(1.0 / _requestsPerSecond);
                    var nextRequest = DateTime.UtcNow;
            
                    while (!cts.Token.IsCancellationRequested)
                    {
                        var start = DateTime.UtcNow;
                        try
                        {
                            await requestHandler();
                            results.Add(new RequestResult(start, true, null));
                        }
                        catch (Exception ex)
                        {
                            results.Add(new RequestResult(start, false, ex.Message));
                        }
                
                        nextRequest = nextRequest.Add(interval);
                        var delay = nextRequest - DateTime.UtcNow;
                        if (delay > TimeSpan.Zero)
                        {
                            await Task.Delay(delay, cts.Token);
                        }
                    }
                });
        
                await Task.WhenAll(monitorTask, loadTask);
        
                // Analyze results
                var maxMemory = memorySamples.Max(s => s.WorkingSet);
                var successRate = results.Count(r => r.Success) / (double)results.Count;
                var avgLatency = results.Where(r => r.Success)
                    .Average(r => (DateTime.UtcNow - r.Timestamp).TotalMilliseconds);
        
                return new LoadTestResult(
                    TotalRequests: results.Count,
                    SuccessRate: successRate,
                    MaxMemoryMB: maxMemory / 1024 / 1024,
                    AvgLatencyMs: avgLatency,
                    MemorySamples: memorySamples,
                    Passed: maxMemory <= _maxMemoryMB * 1024 * 1024 && successRate > 0.99);
            }
        }

        public record MemorySample(DateTime Timestamp, long WorkingSet, long TotalMemory);
        public record RequestResult(DateTime Timestamp, bool Success, string? Error);
        public record LoadTestResult(
            int TotalRequests,
            double SuccessRate,
            long MaxMemoryMB,
            double AvgLatencyMs,
            List<MemorySample> MemorySamples,
            bool Passed);

        // Usage
        var test = new MemoryConstraintLoadTest(
            maxMemoryMB: 256,
            durationSeconds: 60,
            rps: 1000);

        var result = await test.RunAsync(async () =>
        {
            // Simulate request
            await Task.Delay(1);
        });

        Console.WriteLine($"Passed: {result.Passed}");
        Console.WriteLine($"Max Memory: {result.MaxMemoryMB}MB");
        Console.WriteLine($"Success Rate: {result.SuccessRate:P2}");

Контрольные вопросы

  1. Какой GC mode лучше для контейнера с 256MB RAM?
  2. Что такое Ephemeral Strong Mode?
  3. Как реализовать graceful degradation при memory pressure?
  4. Зачем нужен GCHeapHardLimit?
  5. Как тестировать стабильность под memory constraints?

2.9 Контрольная точка модуля 2

Высоконагруженный Message Processing Pipeline

Описание проекта

Создать message processing pipeline, который обрабатывает сообщения с минимальным использованием памяти и гарантированным bounded memory usage.

Требования

Zero-Allocation Hot Path
// Интерфейс процессора
        public interface IMessageProcessor
        {
            // Hot path — должен быть zero-allocation
            void ProcessMessage(ReadOnlySpan<byte> message, Span<byte> output);
        }

        // Реализация с Span<T> и ArrayPool<T>
        public class OptimizedMessageProcessor : IMessageProcessor
        {
            private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
    
            public void ProcessMessage(ReadOnlySpan<byte> message, Span<byte> output)
            {
                // Zero-allocation processing
                // 1. Parse message header
                // 2. Transform payload
                // 3. Write to output
                // NO new allocations in hot path!
            }
        }
Bounded Memory Usage (< 50MB)
// Pipeline с bounded memory
        public class BoundedPipeline
        {
            private readonly Channel<byte[]> _inputChannel;
            private readonly Channel<byte[]> _outputChannel;
            private readonly int _maxMemoryBytes;
            private volatile int _currentMemoryUsage;
    
            public BoundedPipeline(int maxMemoryMB = 50)
            {
                _maxMemoryBytes = maxMemoryMB * 1024 * 1024;
        
                // Bounded channels для backpressure
                _inputChannel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(1000)
                {
                    FullMode = BoundedChannelFullMode.Wait
                });
        
                _outputChannel = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(1000)
                {
                    FullMode = BoundedChannelFullMode.Wait
                });
            }
    
            public async Task EnqueueAsync(byte[] message, CancellationToken ct)
            {
                // Check memory before accepting
                if (_currentMemoryUsage + message.Length > _maxMemoryBytes)
                {
                    throw new MemoryLimitExceededException(
                        $"Would exceed {_maxMemoryBytes / 1024 / 1024}MB limit");
                }
        
                Interlocked.Add(ref _currentMemoryUsage, message.Length);
                await _inputChannel.Writer.WriteAsync(message, ct);
            }
    
            public async Task ProcessAsync(CancellationToken ct)
            {
                await foreach (var message in _inputChannel.Reader.ReadAllAsync(ct))
                {
                    try
                    {
                        var output = ProcessMessage(message);
                        await _outputChannel.Writer.WriteAsync(output, ct);
                    }
                    finally
                    {
                        // Return memory to pool
                        Interlocked.Add(ref _currentMemoryUsage, -message.Length);
                        if (message.Length >= 85_000)
                        {
                            _pool.Return(message);
                        }
                    }
                }
            }
        }
Custom GC Configuration
// runtimeconfig.template.json
        {
          "runtimeOptions": {
            "gcServer": true,
            "gcConcurrent": true,
            "gcHeapHardLimit": "0x3200000",
            "gcLargeObjectHeapCompaction": true,
            "garbageCollectionAdaptationMode": 1
          }
        }
// GC configuration at startup
        public static class GcSetup
        {
            public static void Configure()
            {
                // Verify GC settings
                Console.WriteLine($"Server GC: {GCSettings.IsServerGC}");
                Console.WriteLine($"Concurrent GC: {GCSettings.LatencyMode}");
        
                // Set LOH compaction
                GCSettings.LargeObjectHeapCompactionMode = 
                    GCLargeObjectHeapCompactionMode.CompactOnce;
            }
        }
Memory Metrics Dashboard
// Memory metrics collector
        public class MemoryMetricsCollector
        {
            private readonly Timer _timer;
            private readonly List<MemorySnapshot> _snapshots = new();
    
            public MemoryMetricsCollector()
            {
                _timer = new Timer(CollectSnapshot, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
            }
    
            private void CollectSnapshot(object? state)
            {
                var info = GC.GetGCMemoryInfo();
                var snapshot = new MemorySnapshot(
                    Timestamp: DateTime.UtcNow,
                    WorkingSet: Environment.WorkingSet,
                    Gen0Collections: GC.CollectionCount(0),
                    Gen1Collections: GC.CollectionCount(1),
                    Gen2Collections: GC.CollectionCount(2),
                    HeapSize: info.HeapSizeBytes,
                    Fragmentation: info.FragmentationAfterBytes,
                    LohSize: info.SizeOfTotalObjectsInLOH);
        
                _snapshots.Add(snapshot);
            }
    
            public IReadOnlyList<MemorySnapshot> GetSnapshots() => _snapshots;
        }

        public record MemorySnapshot(
            DateTime Timestamp,
            long WorkingSet,
            int Gen0Collections,
            int Gen1Collections,
            int Gen2Collections,
            long HeapSize,
            double Fragmentation,
            long LohSize);

Критерии прохождения

Allocation Rate < 1 MB/sec при 10k msg/sec
[Benchmark]
        public void ProcessMessages()
        {
            var processor = new OptimizedMessageProcessor();
            var input = new byte[1024];
            var output = new byte[1024];
    
            for (int i = 0; i < 10_000; i++)
            {
                processor.ProcessMessage(input, output);
            }
        }

        // Target: < 100 KB allocated per 10k messages
        // = < 1 MB/sec allocation rate at 10k msg/sec
No Gen 2 Collections During Steady State
// Monitor during load test
        var gen2Before = GC.CollectionCount(2);

        await RunLoadTestAsync(duration: TimeSpan.FromMinutes(5));

        var gen2After = GC.CollectionCount(2);
        var gen2Collections = gen2After - gen2Before;

        Assert.That(gen2Collections, Is.EqualTo(0), 
            "No Gen 2 collections during steady state");
LOH Fragmentation < 5%
var info = GC.GetGCMemoryInfo();
        var fragmentationPercent = (double)info.FragmentationAfterBytes / info.HeapSizeBytes * 100;

        Assert.That(fragmentationPercent, Is.LessThan(5), 
            "LOH fragmentation must be < 5%");
Все Memory Leaks Обнаружены и Устранены
// Leak detection test
        [Test]
        public void NoMemoryLeaks()
        {
            var weakRefs = new List<WeakReference>();
    
            // Create and process messages
            for (int i = 0; i < 1000; i++)
            {
                var msg = CreateMessage();
                weakRefs.Add(new WeakReference(msg));
        
                ProcessMessage(msg);
                msg = null;
            }
    
            // Force GC
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
    
            // Check for leaks
            var leaked = weakRefs.Count(wr => wr.IsAlive);
            Assert.That(leaked, Is.EqualTo(0), 
                $"Found {leaked} leaked objects");
        }

Архитектура проекта

MessagePipeline/
        ├── src/
        │   ├── Pipeline/
        │   │   ├── BoundedPipeline.cs          # Main pipeline with backpressure
        │   │   ├── IMessageProcessor.cs        # Processor interface
        │   │   ├── OptimizedProcessor.cs       # Zero-allocation implementation
        │   │   └── MemoryMetricsCollector.cs   # Metrics collection
        │   ├── Memory/
        │   │   ├── PooledBuffer.cs             # ArrayPool wrapper
        │   │   ├── MemoryLimiter.cs            # Memory usage tracking
        │   │   └── ArenaAllocator.cs           # Batch allocation
        │   └── Program.cs                      # Entry point
        ├── tests/
        │   ├── PipelineTests.cs                # Functional tests
        │   ├── MemoryTests.cs                  # Memory constraint tests
        │   ├── LeakDetectionTests.cs           # Leak detection
        │   └── BenchmarkTests.cs               # Performance benchmarks
        ├── runtimeconfig.template.json         # GC configuration
        └── MessagePipeline.csproj

Пример реализации

// OptimizedProcessor.cs
        public sealed class OptimizedMessageProcessor : IMessageProcessor, IDisposable
        {
            private static readonly ArrayPool<byte> _pool = ArrayPool<byte>.Shared;
            private readonly byte[] _workingBuffer;
    
            public OptimizedMessageProcessor(int maxMessageSize = 64 * 1024)
            {
                _workingBuffer = _pool.Rent(maxMessageSize);
            }
    
            public void ProcessMessage(ReadOnlySpan<byte> message, Span<byte> output)
            {
                // Use working buffer (pooled, no allocation)
                Span<byte> buffer = _workingBuffer.AsSpan(0, message.Length);
                message.CopyTo(buffer);
        
                // Parse header (zero allocation)
                var header = MemoryMarshal.Read<MessageHeader>(buffer);
        
                // Transform payload (zero allocation)
                TransformPayload(buffer.Slice(Unsafe.SizeOf<MessageHeader>()));
        
                // Write output (zero allocation)
                WriteOutput(buffer, output);
            }
    
            private void TransformPayload(Span<byte> payload)
            {
                // In-place transformation
                for (int i = 0; i < payload.Length; i++)
                {
                    payload[i] = (byte)(payload[i] ^ 0xFF);
                }
            }
    
            private void WriteOutput(ReadOnlySpan<byte> input, Span<byte> output)
            {
                input.CopyTo(output);
            }
    
            public void Dispose()
            {
                _pool.Return(_workingBuffer);
            }
        }

        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct MessageHeader
        {
            public int MessageId;
            public short Type;
            public byte Priority;
            public byte Flags;
        }

Evaluation Rubric

КритерийWeightPass Criteria
Allocation Rate25%< 1 MB/sec at 10k msg/sec
Gen 2 Collections20%0 during steady state (5 min)
LOH Fragmentation15%< 5%
Memory Limit20%< 50MB regardless of load
Leak Detection10%0 leaked objects
Code Quality10%Proper IDisposable, pooling, Span usage

Дополнительные задания (Optional)

  1. Prometheus Integration — экспортировать memory metrics
  2. Graceful Degradation — switch to lightweight mode при memory pressure
  3. Custom ArrayPool — implement specialized pool для message sizes
  4. Native Interop — integrate with native library для crypto operations
  5. Multi-threaded Pipeline — parallel processing с bounded memory

Submission

  1. GitHub repository с полным кодом
  2. BenchmarkDotNet результаты
  3. dotnet-counters output во время load test
  4. dotnet-gcdump analysis (no leaks)
  5. README с архитектурой и design decisions

Контрольные вопросы (Final)

  1. Как доказать что hot path zero-allocation?

Использовать BenchmarkDotNet с MemoryDiagnoser, проверить Allocated column = 0 или negligible.

  1. Почему bounded channels важны для memory constraints?

Предотвращают unbounded queue growth. Без backpressure producer может overwhelm consumer, causing OOM.

  1. Как настроить GC для production environment?

Зависит от memory: < 512MB → Workstation, > 1GB → Server. Всегда включать concurrent GC и LOH compaction.

  1. Что делать если Gen 2 collections происходят во время steady state?
  1. Проверить memory leaks (static caches, event handlers)
  2. Уменьшить allocation rate (Span, pooling)
  3. Увеличить heap size (если возможно)
  4. Настроить GC parameters
  1. Как доказать отсутствие memory leaks?
  1. WeakReference tracking tests
  2. GC dump comparison (before/after load)
  3. dotnet-counters monitoring (stable heap size)
  4. Profiler analysis (DotMemory, PerfView)