Pull to refresh

Comments 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>. А вот через последний и можно реализовать нативные буферы. Но в целом, это натягивание совы на глобус.

Всё-таки тема конкретных кейсов для POH не раскрыта.

Sign up to leave a comment.

Articles