Практическое использование ключевого слова stackalloc
Кто-нибудь когда-либо использовал stackalloc
во время программирования на C#? Я знаю, что делает, но единственный раз, когда он появляется в моем коде, случайно, потому что Intellisense предлагает это, когда я начинаю печатать static
, например.
Хотя это не связано со сценариями использования stackalloc
Я на самом деле выполняю значительное количество старых взаимодействий в своих приложениях, поэтому время от времени я могу прибегнуть к использованию unsafe
код. Но тем не менее я обычно нахожу способы избежать unsafe
полностью.
А поскольку размер стека для одного потока в.Net составляет ~1 МБ (поправьте меня, если я ошибаюсь), я еще более зарезервирован для использования stackalloc
,
Существуют ли практические случаи, когда можно было бы сказать: "Это именно тот объем данных и обработки, которые я могу использовать небезопасно и использовать" stackalloc
"?
6 ответов
Единственная причина использовать stackalloc
это производительность (для вычислений или взаимодействия). Используя stackalloc
вместо выделенного массива кучи вы создаете меньшее давление GC (GC должен работать меньше), вам не нужно закреплять массивы, он быстрее выделяется, чем массив кучи, и он автоматически освобождается при выходе из метода (выделенные кучи массивы освобождаются только при запуске GC). Также с помощью stackalloc
вместо встроенного распределителя (такого как malloc или эквивалент.Net) вы также получаете скорость и автоматическое освобождение при выходе из области видимости.
Производительность мудрая, если вы используете stackalloc
Вы значительно увеличиваете вероятность попадания кэша в процессор из-за локальности данных.
Я использовал stackalloc для выделения буферов для работы DSP в реальном времени. Это был очень специфический случай, когда производительность должна была быть максимально последовательной. Обратите внимание, что есть разница между согласованностью и общей пропускной способностью - в этом случае меня не беспокоило слишком медленное распределение кучи, просто недетерминированность сборки мусора в этой точке программы. Я бы не использовал его в 99% случаев.
Stackalloc инициализация пролетов. В предыдущих версиях C# результат stackalloc мог быть сохранен только в локальной переменной указателя. Начиная с C# 7.2, stackalloc теперь может использоваться как часть выражения и может предназначаться для диапазона, и это может быть сделано без использования ключевого слова unsafe. Таким образом, вместо записи
Span<byte> bytes;
unsafe
{
byte* tmp = stackalloc byte[length];
bytes = new Span<byte>(tmp, length);
}
Вы можете написать просто:
Span<byte> bytes = stackalloc byte[length];
Это также чрезвычайно полезно в ситуациях, когда вам нужно некоторое пространство для выполнения операции, но вы хотите избежать выделения памяти кучи для относительно небольших размеров.
Span<byte> bytes = length <= 128 ? stackalloc byte[length] : new byte[length];
... // Code that operates on the Span<byte>
stackalloc
относится только к небезопасному коду. Для управляемого кода вы не можете решить, где разместить данные. Типы значений распределяются в стеке по умолчанию (если они не являются частью ссылочного типа, в этом случае они размещаются в куче). Ссылочные типы размещаются в куче.
Размер стека по умолчанию для простого ванильного приложения.NET составляет 1 МБ, но вы можете изменить это в PE-заголовке. Если вы запускаете потоки явно, вы также можете установить другой размер через перегрузку конструктора. Для приложений ASP.NET размер стека по умолчанию составляет всего 256 КБ, что следует учитывать при переключении между двумя средами.
Поздний ответ, но я считаю, что все еще полезно.
Я пришел к этому вопросу, и мне все еще было любопытно увидеть разницу в производительности, поэтому я создал следующий тест (используя пакет BenchmarkDotNet NuGet):
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[RankColumn]
public class Benchmark1
{
//private MemoryStream ms = new MemoryStream();
static void FakeRead(byte[] buffer, int start, int length)
{
for (int i = start; i < length; i++)
buffer[i] = (byte) (i % 250);
}
static void FakeRead(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte) (i % 250);
}
[Benchmark]
public void AllocatingOnHeap()
{
var buffer = new byte[1024];
FakeRead(buffer, 0, buffer.Length);
}
[Benchmark]
public void ConvertingToSpan()
{
var buffer = new Span<byte>(new byte[1024]);
FakeRead(buffer);
}
[Benchmark]
public void UsingStackAlloc()
{
Span<byte> buffer = stackalloc byte[1024];
FakeRead(buffer);
}
}
И вот где результаты
| Method | Mean | Error | StdDev | Rank | Gen 0 | Allocated |
|----------------- |---------:|---------:|---------:|-----:|-------:|----------:|
| UsingStackAlloc | 704.9 ns | 13.81 ns | 12.91 ns | 1 | - | - |
| ConvertingToSpan | 755.8 ns | 5.77 ns | 5.40 ns | 2 | 0.0124 | 1,048 B |
| AllocatingOnHeap | 839.3 ns | 4.52 ns | 4.23 ns | 3 | 0.0124 | 1,048 B |
Этот тест показывает, что использование
stackalloc
является самым быстрым решением, а также не использует распределения! Если вам интересно, как использовать пакет NuGet BenchmarkDotNet, посмотрите это видео.
На этот вопрос есть несколько отличных ответов, но я просто хочу отметить, что
Stackalloc также можно использовать для вызова собственных API
Многие собственные функции требуют от вызывающей стороны выделения буфера для получения возвращаемого результата. Например, функция CfGetPlaceholderInfo вcfapi.h
имеет следующую подпись.
HRESULT CfGetPlaceholderInfo(
HANDLE FileHandle,
CF_PLACEHOLDER_INFO_CLASS InfoClass,
PVOID InfoBuffer,
DWORD InfoBufferLength,
PDWORD ReturnedLength);
Чтобы вызвать его на C# через взаимодействие,
[DllImport("Cfapi.dll")]
public static unsafe extern HResult CfGetPlaceholderInfo(IntPtr fileHandle, uint infoClass, void* infoBuffer, uint infoBufferLength, out uint returnedLength);
Вы можете использовать stackalloc.
byte* buffer = stackalloc byte[1024];
CfGetPlaceholderInfo(fileHandle, 0, buffer, 1024, out var returnedLength);