Комментарии 10
Хотелось бы понять, есть ли у POH преимущество? Я вот набросал простенький бенчмарк с выделением-освобождением памяти и получил следующее:
| Method | BlockSize | Mean | Error | StdDev |
|-------------- |---------- |----------:|---------:|---------:|
| MarshalAlloc | 32 | 34.78 us | 0.056 us | 0.046 us |
| PinnedAlloc | 32 | 129.22 us | 0.565 us | 0.529 us |
| GCHandleAlloc | 32 | 111.00 us | 0.394 us | 0.368 us |
| MarshalAlloc | 64 | 35.38 us | 0.057 us | 0.053 us |
| PinnedAlloc | 64 | 130.89 us | 0.768 us | 0.718 us |
| GCHandleAlloc | 64 | 113.60 us | 0.357 us | 0.334 us |
| MarshalAlloc | 128 | 54.59 us | 0.415 us | 0.388 us |
| PinnedAlloc | 128 | 139.64 us | 2.678 us | 2.865 us |
| GCHandleAlloc | 128 | 119.83 us | 1.366 us | 1.140 us |
| MarshalAlloc | 256 | 54.71 us | 1.020 us | 0.954 us |
| PinnedAlloc | 256 | 145.44 us | 2.837 us | 3.036 us |
| GCHandleAlloc | 256 | 127.97 us | 0.454 us | 0.354 us |
| MarshalAlloc | 512 | 54.50 us | 0.132 us | 0.117 us |
| PinnedAlloc | 512 | 160.99 us | 3.141 us | 3.739 us |
| GCHandleAlloc | 512 | 148.42 us | 2.279 us | 2.239 us |
| MarshalAlloc | 1024 | 76.02 us | 0.481 us | 0.427 us |
| PinnedAlloc | 1024 | 203.36 us | 0.596 us | 0.497 us |
| GCHandleAlloc | 1024 | 191.79 us | 3.412 us | 4.555 us |
Код
[Benchmark]
public void MarshalAlloc()
{
for (int i = 0; i < 1024; i++)
ptrs[i] = Marshal.AllocHGlobal(BlockSize);
for (int i = 0; i < 1024; i++)
Marshal.FreeHGlobal(ptrs[i]);
}
[Benchmark]
public unsafe void PinnedAlloc()
{
for (int i = 0; i < 1024; i++)
{
arr[i] = GC.AllocateUninitializedArray<byte>(BlockSize, pinned: true);
fixed (byte* ptr = arr[i])
ptrs[i] = new IntPtr(ptr);
}
for (int i = 0; i < 1024; i++)
{
arr[i] = null;
ptrs[i] = IntPtr.Zero;
}
}
[Benchmark]
public void GCHandleAlloc()
{
for (int i = 0; i < 1024; i++)
{
arr[i] = new byte[BlockSize];
gchs[i] = GCHandle.Alloc(arr[i], GCHandleType.Pinned);
}
for (int i = 0; i < 1024; i++)
{
arr[i] = null;
gchs[i].Free();
}
}
Понятное дело, что здесь не рассматривались аспекты фрагментирования кучи, сложности с ручным освобождением памяти, но вот преимущество у нативного выделения в разы мне кажется немного странным.
Также с POH есть второй неочевидный момент: передача указателя на буфер в вызов, инициирующий асинхронную операцию. То есть по выходу из функции буфер должен продолжать жить. Но если мы не знаем, был ли выделен в POH, то мы не имеем права использовать fixed
, а должны использовать медленный GCHandle
, что сводит преимущество POH на нет.
Не совсем понятно по тексту статьи (мне). Мы fixed если используем - элемент не в POH будет, а в SOH/LOG?
Понятное дело, что здесь не рассматривались аспекты фрагментирования кучиВы сами же отвечаете на свой вопрос. Эти аспекты, которые здесь не рассматривались — и есть предназначение POH. Короче говоря, бенчмарк не говорит ничего о преимуществах/недостатках POH.
преимущество у нативного выделения в разы мне кажется немного страннымОно потому и быстрее, что нативное, не? )
Также с POH есть второй неочевидный момент: передача указателя на буфер в вызов, инициирующий асинхронную операцию.Так в асинхронных операциях нельзя использовать указатели. Либо я не понял, что имеется в виду, скиньте код.
Оно потому и быстрее, что нативное, не? )
Не. Выделение управляемой памяти в GC-языках обычно более быстрое. Тут львиная доля времени уходит на fixed/GCHandle.Alloc
, а не на выделение.
Так в асинхронных операциях нельзя использовать указатели. Либо я не понял, что имеется в виду, скиньте код.
Я имею в виду нативные вызовы типа WSASend
, куда вы передаёте указатель на буфер, а данные туда пишутся в фоне.
Вы можете вызывать эту функцию явно, написав свою библиотеку для работы с сокетами, а можете неявно, просто используя асинхронные методы работы с библиотечным Socket
. И единственный способ зафиксировать буфер здесь — это GCHandle.Alloc
, а не fixed
.
Резюме: я бы рассматривал POH как альтернативу выделения неуправляемой памяти, но с поддержкой GC.
Не. Выделение управляемой памяти в GC-языках обычно более быстрое. Тут львиная доля времени уходит на fixed/GCHandle.Alloc, а не на выделение.Да? Как-то контринтуитивно. Подскажете, где можно почитать про это?
Вы можете вызывать эту функцию явно, написав свою библиотеку для работы с сокетамиСилюсь понять, для чего может быть нужна своя библиотека для работы с сокетами — и не получается.
И единственный способ зафиксировать буфер здесь — это GCHandle.Alloc, а не fixed.А зачем вообще тут использовать именно неуправляемый буфер, а не, скажем, Memory{T}?
Да? Как-то контринтуитивно. Подскажете, где можно почитать про это?
В учебниках. Это основы.
А если не верите — напишите бенчмарк и убедитесь в этом самостоятельно. Я вот прямо сейчас проверил: у меня на небольших объектах (64 байта) скорость выделения управляемой памяти оказалась в 3 раза выше, чем неуправляемой (выделение + удаление).
Силюсь понять, для чего может быть нужна своя библиотека для работы с сокетами — и не получается.
Ну, например, для работы с io_uring вместо epoll.
И причём, тут, собственно, своя библиотека? Можно подумать, когда вы используете системную библиотеку, под капотом оно работает как-то по-другому.
А зачем вообще тут использовать именно неуправляемый буфер, а не, скажем, Memory{T}?
Потому что RTFM. Это просто обёртка над управляемым массивом.
В учебниках. Это основы.Так вы просветите меня, основ не знающего — в каких именно учебниках? Желательно с ссылками на главы.
Я вот прямо сейчас проверил: у меня на небольших объектах (64 байта) скорость выделения управляемой памяти оказалась в 3 раза выше, чем неуправляемой (выделение + удаление).Так вы про выделение говорите или про выделение + удаление? Как замеряли удаление в управляемой памяти?
Ну, например, для работы с io_uring вместо epoll.Так это же работа с файлами. Я спрашивал про сокеты.
И причём, тут, собственно, своя библиотека?Так это же вы написали про свою библиотеку)
Потому что RTFM. Это просто обёртка над управляемым массивом.Memory — это «не обёртка над управляемым массивом». Так что, действительно, RTFM.
Так вы просветите меня, основ не знающего — в каких именно учебниках? Желательно с ссылками на главы.
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals
Так вы про выделение говорите или про выделение + удаление? Как замеряли удаление в управляемой памяти?
Читайте основы. Управляемую память не нужно освобождать. Это сделает сборщик мусора в фоновом потоке. Да, возможны ситуации, когда он может не успевать, но это редкий сценарий.
Так это же работа с файлами. Я спрашивал про сокеты.
man io_uring
man socket
Так это же вы написали про свою библиотеку)
Нет. Вы просто не дочитали фразу до конца: своя библиотека или библиотечный Socket. А под капотом всё все равно сводится к вызову API операционной системы.
Memory — это «не обёртка над управляемым массивом».
Да, ошибся. Аргументом Memory<T>
может быть не только массив:
https://github.com/dotnet/corert/blob/master/src/System.Private.CoreLib/shared/System/Memory.cs#L174
но и ещё строка и MemoryManager<T>
. А вот через последний и можно реализовать нативные буферы. Но в целом, это натягивание совы на глобус.
Я думаю, можно посмотреть у Майкрософт, как они заменили GCHandle.Alloc
на GC.AllocateUninitializedArray
: https://github.com/dotnet/aspnetcore/pull/21614/files
Всё-таки тема конкретных кейсов для POH не раскрыта.
Pinned Object Heap в .NET 5