Ответ 1
Случай, когда stackalloc быстрее:
private static volatile int _dummy; // just to avoid any optimisations
// that have us measuring the wrong
// thing. Especially since the difference
// is more noticable in a release build
// (also more noticable on a multi-core
// machine than single- or dual-core).
static void Main(string[] args)
{
System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch();
Thread[] threads = new Thread[20];
sw1.Start();
for(int t = 0; t != 20; ++t)
{
threads[t] = new Thread(DoSA);
threads[t].Start();
}
for(int t = 0; t != 20; ++t)
threads[t].Join();
Console.WriteLine(sw1.ElapsedTicks);
System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
threads = new Thread[20];
sw2.Start();
for(int t = 0; t != 20; ++t)
{
threads[t] = new Thread(DoHA);
threads[t].Start();
}
for(int t = 0; t != 20; ++t)
threads[t].Join();
Console.WriteLine(sw2.ElapsedTicks);
Console.Read();
}
private static void DoSA()
{
Random rnd = new Random(1);
for(int i = 0; i != 100000; ++i)
StackAllocation(rnd);
}
static unsafe void StackAllocation(Random rnd)
{
int size = rnd.Next(1024, 131072);
int* p = stackalloc int[size];
_dummy = *(p + rnd.Next(0, size));
}
private static void DoHA()
{
Random rnd = new Random(1);
for(int i = 0; i != 100000; ++i)
HeapAllocation(rnd);
}
static void HeapAllocation(Random rnd)
{
int size = rnd.Next(1024, 131072);
int[] a = new int[size];
_dummy = a[rnd.Next(0, size)];
}
Важные отличия между этим кодом и тем в вопросе:
-
У нас есть несколько потоков. С распределением стека они выделяют в свой собственный стек. С распределением кучи они выделяют из кучи, совместно используемой другими потоками.
-
Более крупные размеры выделены.
-
Различные размеры, назначенные каждый раз (хотя я засеял случайный генератор, чтобы сделать тесты более детерминированными). Это делает фрагментацию кучи более вероятной, делая распределение кучи менее эффективным, чем с одинаковыми распределениями каждый раз.
Кроме того, также стоит отметить, что stackalloc
часто используется в качестве альтернативы использованию fixed
для привязки массива к куче. Привязка массивов является плохим для производительности кучи (не только для этого кода, но и для других потоков, использующих одну и ту же кучу), поэтому влияние производительности будет еще больше, если заявленная память будет использоваться в течение разумного промежутка времени.
В то время как мой код демонстрирует случай, когда stackalloc
дает преимущество в производительности, в вопросе, вероятно, ближе к большинству случаев, когда кто-то может "оптимизировать" его, используя его. Надеемся, что два фрагмента кода показывают, что целая stackalloc
может дать толчок, это также может сильно повредить производительность.
Как правило, вы даже не должны рассматривать stackalloc
, если вам не понадобится использовать фиксированную память для взаимодействия с неуправляемым кодом в любом случае, и его следует рассматривать как альтернативу fixed
, а не альтернативой распределению общей кучи, Использование в этом случае по-прежнему требует осторожности, предусмотрительности перед началом работы и профилирования после того, как вы закончите.
Использование в других случаях может принести пользу, но оно должно быть далеко от списка улучшений производительности, которые вы пытались выполнить.
Edit:
Чтобы ответить на часть 1 вопроса. Stackalloc концептуально много, как вы описываете. Он получает кусок памяти стека, а затем возвращает указатель на этот кусок. Он не проверяет, что память поместится как таковая, но если она попытается получить память в конце стека, которая защищена .NET при создании потока, то это приведет к тому, что ОС вернет исключение в runtime, который затем превращается в исключение, управляемое .NET. То же самое происходит, если вы просто выделите один байт в методе с бесконечной рекурсией - если только вызов не был оптимизирован, чтобы избежать распределения стека (иногда это возможно), тогда один байт в конечном итоге будет содержать достаточно, чтобы вызвать исключение.