Информация об изменениях

Сообщение Re: крепостные объекты от 07.03.2024 11:34

Изменено 07.03.2024 11:53 Sinclair

Re: крепостные объекты
Здравствуйте, Qulac, Вы писали:

Q>В net можно прикрепить объект к памяти запретив сборщику мусора его перемещение.

можно.
Q>Пишут, что это может быть полезно для увеличения производительности приложения.
Кто это пишет? Пожалуйста, если вы хотите обсудить некоторое утверждение, которое сделано кем-то, приведите ссылку на это утверждение. Без контекста понять, о чём именно идёт речь, бывает практически невозможно.
Q>А при каких сценариях работы с объектами это лучше использовать?
Например, вы делаете вызов в натив.
Натив ничего не знает про GC и типы дотнета. У вас есть два способа работать с нативом:
1. Выполнить выделение неуправляемой памяти; скопировать нужные вам данные в эту память при помощи GC-aware инструментов; выполнить вызов; освободить занимаемую память.
2. Передать в натив адрес управляемого объекта, полагаясь на побитовую совместимость используемых вами типов данных.

Второй подход, очевидно, выгоднее. Вместо того, чтобы копировать 500 мегабайт из дотнетового byte[] в неуправляемый буфер, вы сразу передаёте адрес нулевого элемента этого массива.
CLR гарантирует вам, что байты byte[] лежат подряд, поэтому никаких проблем на принимающей стороне возникнуть не должно.
Но! Нативный метод (вроде WriteFile) может работать долго. В это время запросто может проснуться GC и решить поперемещать ваши данные. GC работает в предположении, что он знает размещение всех ссылок на все живые объекты.
Поэтому он спокойно перемещает данные.
Даже если у вас работает цикл вроде
for(var i=0; i<arr.Length; i++)
  s += arr[i];

то посреди цикла GC может передвинуть arr в другое место в памяти — ну и что, на следующей итерации arr прибавит смещение к новому адресу и ничего не сломается.
А вот про нативный код GC ничего не знает; переместив массив, он не может "залезть внутрь" нативного кода и подправить там ему указатель.
Поэтому перед вызовом нужно как-то сообщить GC, что двигать конкретно вот эти данные нельзя.
Это и делается при помощи "прикрепления" объекта.
Два основных способа:
1. Если вам действительно нужно держать данные неподвижно только на время одного вызова, то в C# есть специальное ключевое слово fixed. См. напр. здесь:
            fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
            {
                if (Interop.Kernel32.WriteFile(handle, pinned, buffer.Length, out int numBytesWritten, &overlapped) != 0)
                {
                    Debug.Assert(numBytesWritten == buffer.Length);
                    return;
                }

                int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
                throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
            }

Такой код не просто позволяет получить [i]указатель
на управляемый объект; он ещё и сообщает GC, что двигать эти данные нельзя до окончания блока, обозначенного ключевым словом fixed.
Если же вам нужно, чтобы какой-то кусок данных располагался неподвижно за пределами времени жизни конкретного фрейма стека (т.е. после выхода из вашей функции), то придётся велосипедить вручную.
Например, с асинхронным IO использовать простой fixed не получится — функция WriteAsync возвращает управление до того, как операционная система закончит работать с буфером.
Поэтому в дотнетовом IO асинхронная версия значительно сложнее.
Вот то место, где пришпиливается буфер:
            internal NativeOverlapped* PrepareForOperation(ReadOnlyMemory<byte> memory, long fileOffset, OSFileStreamStrategy? strategy = null)
            {
                Debug.Assert(strategy is null || strategy is AsyncWindowsFileStreamStrategy, $"Strategy was expected to be null or async, got {strategy}.");

                _result = 0;
                _strategy = strategy;
                _bufferSize = memory.Length;
                _memoryHandle = memory.Pin();
                _overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped);
                if (_fileHandle.CanSeek)
                {
                    _overlapped->OffsetLow = (int)fileOffset;
                    _overlapped->OffsetHigh = (int)(fileOffset >> 32);
                }
                return _overlapped;
            }

Под капотом у ReadOnlyMemory<byte>.Pin() используется GCHandle.Alloc(..., GCHandleType.Pinned).

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

Ускорение происходит не за счёт самого пришпиливания как такового (как раз наоборот — GC не может свободно распоряжаться пришпиленной памятью, поэтому ему придётся делать всякие приседания, увеличивая расход памяти и время сборки мусора), а за счёт того, что можно отказаться от лишних копирований.
Re: крепостные объекты
Здравствуйте, Qulac, Вы писали:

Q>В net можно прикрепить объект к памяти запретив сборщику мусора его перемещение.

можно.
Q>Пишут, что это может быть полезно для увеличения производительности приложения.
Кто это пишет? Пожалуйста, если вы хотите обсудить некоторое утверждение, которое сделано кем-то, приведите ссылку на это утверждение. Без контекста понять, о чём именно идёт речь, бывает практически невозможно.
Q>А при каких сценариях работы с объектами это лучше использовать?
Например, вы делаете вызов в натив.
Натив ничего не знает про GC и типы дотнета. У вас есть два способа работать с нативом:
1. Выполнить выделение неуправляемой памяти; скопировать нужные вам данные в эту память при помощи GC-aware инструментов; выполнить вызов; освободить занимаемую память.
2. Передать в натив адрес управляемого объекта, полагаясь на побитовую совместимость используемых вами типов данных.

Второй подход, очевидно, выгоднее. Вместо того, чтобы копировать 500 мегабайт из дотнетового byte[] в неуправляемый буфер, вы сразу передаёте адрес нулевого элемента этого массива.
CLR гарантирует вам, что байты byte[] лежат подряд, поэтому никаких проблем на принимающей стороне возникнуть не должно.
Но! Нативный метод (вроде WriteFile) может работать долго. В это время запросто может проснуться GC и решить поперемещать ваши данные. GC работает в предположении, что он знает размещение всех ссылок на все живые объекты.
Поэтому он спокойно перемещает данные.
Даже если у вас работает цикл вроде
for(var i=0; i<arr.Length; i++)
  s += arr[i];

то посреди цикла GC может передвинуть arr в другое место в памяти — ну и что, на следующей итерации arr[i] прибавит смещение к новому адресу и ничего не сломается.
А вот про нативный код GC ничего не знает; переместив массив, он не может "залезть внутрь" нативного кода и подправить там ему указатель.
Поэтому перед вызовом нужно как-то сообщить GC, что двигать конкретно вот эти данные нельзя.
Это и делается при помощи "прикрепления" объекта.
Два основных способа:
1. Если вам действительно нужно держать данные неподвижно только на время одного вызова, то в C# есть специальное ключевое слово fixed. См. напр. здесь:
            fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
            {
                if (Interop.Kernel32.WriteFile(handle, pinned, buffer.Length, out int numBytesWritten, &overlapped) != 0)
                {
                    Debug.Assert(numBytesWritten == buffer.Length);
                    return;
                }

                int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle);
                throw Win32Marshal.GetExceptionForWin32Error(errorCode, handle.Path);
            }

Такой код не просто позволяет получить указатель на управляемый объект; он ещё и сообщает GC, что двигать эти данные нельзя до окончания блока, обозначенного ключевым словом fixed.
Если же вам нужно, чтобы какой-то кусок данных располагался неподвижно за пределами времени жизни конкретного фрейма стека (т.е. после выхода из вашей функции), то придётся велосипедить вручную.
Например, с асинхронным IO использовать простой fixed не получится — функция WriteAsync возвращает управление до того, как операционная система закончит работать с буфером.
Поэтому в дотнетовом IO асинхронная версия значительно сложнее.
Вот то место, где пришпиливается буфер:
            internal NativeOverlapped* PrepareForOperation(ReadOnlyMemory<byte> memory, long fileOffset, OSFileStreamStrategy? strategy = null)
            {
                Debug.Assert(strategy is null || strategy is AsyncWindowsFileStreamStrategy, $"Strategy was expected to be null or async, got {strategy}.");

                _result = 0;
                _strategy = strategy;
                _bufferSize = memory.Length;
                _memoryHandle = memory.Pin();
                _overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped);
                if (_fileHandle.CanSeek)
                {
                    _overlapped->OffsetLow = (int)fileOffset;
                    _overlapped->OffsetHigh = (int)(fileOffset >> 32);
                }
                return _overlapped;
            }

Под капотом у ReadOnlyMemory<byte>.Pin() используется GCHandle.Alloc(..., GCHandleType.Pinned).

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

Ускорение происходит не за счёт самого пришпиливания как такового (как раз наоборот — GC не может свободно распоряжаться пришпиленной памятью, поэтому ему придётся делать всякие приседания, увеличивая расход памяти и время сборки мусора), а за счёт того, что можно отказаться от лишних копирований.