Stack Exchange
Инспекция класса для работы с HeapAlloc
Нашел в спецификации C# интересный материал A.8 Dynamic memory allocation.
Немного переделал его для работы с x64:
    internal unsafe class Memory
    {
        // Heap API flags
        private const uint HEAP_ZERO_MEMORY = 0x00000008;

        // Heap API functions
        [DllImport("Kernel32")]
        private static extern IntPtr GetProcessHeap();
        [DllImport("Kernel32")]
        private static extern int HeapSize(IntPtr hHeap, uint dwFlags, IntPtr lpMem);
        [DllImport("Kernel32")]
        private static extern IntPtr HeapAlloc(IntPtr hHeap, uint dwFlags, UIntPtr dwBytes);
        [DllImport("Kernel32")]
        private static extern IntPtr HeapReAlloc(IntPtr hHeap, uint dwFlags, IntPtr lpMem, UIntPtr dwBytes);
        [DllImport("Kernel32")]
        private static extern bool HeapFree(IntPtr hHeap, uint flags, IntPtr lpMem);

        // Handle for the process heap. This handle is used in all calls to the
        // HeapXXX APIs in the methods below.
        private static IntPtr ph = GetProcessHeap();

        // Private instance constructor to prevent instantiation.
        private Memory()
        {
        }

        // Allocates a memory block of the given size. The allocated memory is
        // automatically initialized to zero.
        public static void* Alloc(uint size)
        {
            void* result = HeapAlloc(ph, HEAP_ZERO_MEMORY, new UIntPtr(size)).ToPointer();
            if (result == null) throw new OutOfMemoryException();
            return result;
        }

        // Copies count bytes from src to dst. The source and destination
        // blocks are permitted to overlap.
        public static void Copy(void* src, void* dst, int count)
        {
            byte* ps = (byte*)src;
            byte* pd = (byte*)dst;
            if (ps > pd)
            {
                for (; count != 0; count--) *pd++ = *ps++;
            }
            else if (ps < pd)
            {
                for (ps += count, pd += count; count != 0; count--) *--pd = *--ps;
            }
        }

        // Frees a memory block.
        public static void Free(void* block)
        {
            if (!HeapFree(ph, 0, new IntPtr(block))) throw new InvalidOperationException();
        }

        // Re-allocates a memory block. If the reallocation request is for a
        // larger size, the additional region of memory is automatically
        // initialized to zero.
        public static void* ReAlloc(void* block, uint size)
        {
            void* result = HeapReAlloc(ph, HEAP_ZERO_MEMORY, new IntPtr(block), new UIntPtr(size)).ToPointer();
            if (result == null) throw new OutOfMemoryException();
            return result;
        }

        // Returns the size of a memory block.
        public static int SizeOf(void* block)
        {
            int result = HeapSize(ph, 0, new IntPtr(block));
            if (result == -1) throw new InvalidOperationException();
            return result;
        }
    }

Я тот еще нативщик, поэтому прошу опытных людей провести испекцию кода, который преимущественно будет являться заменой стандартного байтового массива (byte[]) в многопоточном приложении.
p.s. по предварительным данным HeapAlloc работает быстрее стандартной stackalloc. Правда профессиональные бенчмарки не проводил, использовал stopwatch.

Нативное выделение памяти для начала негативно влияет на сборщик мусора. Зачем вам нативная память в C#? Мне кажется, вы пытаетесь экономить на гвоздях.

@VladD, скорость выделения, скорость освобождения получаются даже быстрее чем stackalloc. Нужно это для производительности.

@VladD, можно поподробнее как именно нативное выделение влияет на коллектор?

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

Ну, области нативной памяти становятся недоступны коллектору, и ему приходится в своих расчётах всё время эти дыры учитывать.

Расскажите лучше вашу настоящую задачу. Обыкновенное выделение памяти через new должно быть достаточно быстрым: это Interlocked.Increment стек-пойнтера плюс обнуление памяти. Если вы хотите и на обнулении памяти сэкономить, заведите пул кусков памяти и используйте их повторно.

@VladD, но, посудите сами, что "обойти дыру", что "втыкать" при каждом завершении метода удаляя управляемый буффер - не одно и тоже ли по перформансу?

@VladD, настоящая задача - увеличение производительности приложения. Оптимизировать стандартными средствами уже некуда (если не считать микрооптимизации, но это отдельная тема). Грубо говоря сейчас втыкает GC выделяя и удаляя много объектов Gen0. А когда GC "втыкает" - он тупо приостанавливает все потоки. Поэтому решил уйти на более низкий уровень, самостоятельно когда нужно выделять и освобождать память.

Мне не верится, что вы сможете работать с памятью скорее, чем GC, он всё-таки сложная и умная штука. Возможно, вам нужно просто уменьшить количество мелких временных объектов. Да, и может быть, какие-то из них реально являются структурами.

@VladD, по поводу пула кусков памяти - Вы имеете ввиду какой то кэширующий пул буфферов, который сохраняет в себя одинаковые данные, доступные всем потокам? Я слышал нечто подобное, но не вдавался в подробности.

Ага, именно так, пул буферов, как бы собственный аллокатор, если хотите.

раз у вас дошло до оптимизации GC, в плоть до отказа от него, то скорее всего вам нужно бороться не с GC, а с memory-трафиком, т.е. избавляться от создания временных объектов в больших количествах, а по максимуму использовать их повторно, вроде интернирования строк, и все в таком духе.

@rdorn, VladD - не всегда это возможно. Мало очень статических данных, в основном они динамически меняются. Да и не поможет это сильно, издержки на синхронизации для кэширующего пула, увеличение размера потребляемой ОЗУ (ведь пул будет активным все время работы приложения). Я попробую, но многого от этого не стоит ожидать. Буду пробовать и то и то. Так что вопрос открыт, корректный ли код в вопросе? Может что-то подправить?

По самой идее

по предварительным данным HeapAlloc работает быстрее стандартной stackalloc

Это странно. Как правило, managed-память выделяется быстрее нативной, поскольку для её выделения требуется лишь сдвинуть указатель, а для выделения нативной идёт поиск достаточного свободного куска по связному списку. Со временем появляется фрагментация и искать в списке становится всё дольше. Кроме одного случая.
У тебя постоянно происходит выделение памяти на короткоживущие объекты одинакового размера. В таком случае в нативной памяти постоянно занимаются и освобождаются куски одинакового размера и они довольно быстро попадаются в списке свободных кусков.
А вот в случае managed-памяти освобождения внутри не будет, поэтому придётся постоянно выполнять дефрагментацию памяти, что занимает значительное время.
Такую проблему следует решать по-другому:

Попробовать заменить класс на структуру, чтобы использовать память в стеке.
Попробовать сделать переиспользуемый пулл объектов класса на достаточное количество элементов. Он уползёт куда-нибудь в долгоживущие объекты и перестанет мешаться при распределении памяти.

По коду:

internal unsafe class Memory


Этот класс вообще не требует unsafe-кода.

[DllImport("Kernel32")]
private static extern int HeapSize(IntPtr hHeap, uint dwFlags, IntPtr lpMem);


Он не int. Должно быть IntPtr с кастом в long, чтобы соблюдать битность. Но вообще он size_t: https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa366706(v=vs.85).aspx

void* result = (...).ToPointer();
if (result == null) throw new ...;
return result;


IntPtr result = ...;
if (result == IntPtr.Zero) throw new ...;
return result;


public static void Copy(void* src, void* dst, int count)


Традиционно это называется не Copy, а Move. Т. е. при копировании блоки пересекаться не должны, а при перемещении могут. И для этого уже есть готовая функция: RtlMoveMemory.

throw new OutOfMemoryException();
throw new InvalidOperationException();


Ну хоть какое-нибудь описание в исключения?

throw new OutOfMemoryException();


Не уверен, можно ли запросить ещё памяти, если кончился хип.
Мне почему-то кажется, что да.

Кстати, да, HeapAlloc -- это системный вызов, непонятно как он может быть быстрее нормальной аллокации на стеке. Думаю, профилирование покажет, что медленнее, причём намного.

@VladD, дефрагментация сожрёт разницу. Т. е. если ты выделил 8 байт, выделил ещё 8 managed-байт, освободил первые 8 байт и повторил это миллион раз - вероятно, тебе один и тот же кусок отдадут в unmanaged. А вот если бы они шли из той же кучи, то и managed, то там бы постоянно вертелась дефрагментация памяти, поскольку часть объектов выживает. Думаю, за счёт этого действительно может быть выигрыш, но фиксить-то надо по-другому :)

Нипанятна! Откуда возьмётся фрагментация, если stackalloc? Всё в стеке, при окончании работы функции фрейм умирает.

@VladD, ой. Я думал про managed heap. А вообще, ведь у объекта в стеке будет лишь ссылка, а сам-то объект в куче.
