В настоящее время я работаю над очень важной для производительности программой, и одним из путей, который я решил изучить, который может помочь снизить потребление ресурсов, было увеличение размера стека моих рабочих потоков, чтобы я мог перемещать большинство данных, к float[]
которым я буду получать доступ стек (использование stackalloc
).
Я читал, что размер стека по умолчанию для потока равен 1 МБ, поэтому для перемещения всех моих float[]
файлов мне потребуется расширить стек примерно в 50 раз (до 50 МБ ~).
Я понимаю, что это обычно считается «небезопасным» и не рекомендуется, но после сопоставления моего текущего кода с этим методом я обнаружил увеличение скорости обработки на 530% ! Так что я не могу просто пройти мимо этого варианта без дальнейшего расследования, что приводит меня к моему вопросу; Каковы опасности, связанные с увеличением стека до такого большого размера (что может пойти не так), и какие меры предосторожности я должен предпринять, чтобы минимизировать такие опасности?
Мой тестовый код,
public static unsafe void TestMethod1()
{
float* samples = stackalloc float[12500000];
for (var ii = 0; ii < 12500000; ii++)
{
samples[ii] = 32768;
}
}
public static void TestMethod2()
{
var samples = new float[12500000];
for (var i = 0; i < 12500000; i++)
{
samples[i] = 32768;
}
}
источник
Marshal.AllocHGlobal
(не забывайте и об этомFreeHGlobal
) выделять данные за пределами управляемой памяти? Затем наведите указатель на afloat*
, и вы должны быть отсортированы.Ответы:
Сравнив тестовый код с Сэмом, я понял, что мы оба правы!
Впрочем, о разных вещах:
Это выглядит следующим образом :
stack
<global
<heap
. (время выделения)Технически, выделение стека на самом деле не является выделением, среда выполнения просто гарантирует, что часть стека (фрейм?) зарезервирована для массива.
Я настоятельно советую быть осторожным с этим, хотя.
Я рекомендую следующее:
( Примечание : 1. применяется только к типам значений; ссылочные типы будут выделяться в куче, а преимущество будет уменьшено до 0)
Чтобы ответить на сам вопрос: я не столкнулся с какой-либо проблемой при тестировании большого стека.
Я считаю, что единственно возможные проблемы - это переполнение стека, если вы не будете осторожны с вызовами функций и нехваткой памяти при создании потока (-ов), если в системе мало работы.
Раздел ниже мой первоначальный ответ. Это неправильно и тесты не верны. Он хранится только для справки.
Мой тест показывает, что память, выделенная для стека, и глобальная память по крайней мере на 15% медленнее, чем (занимает 120% времени) память, выделенная в куче, для использования в массивах!
Это мой тестовый код и пример вывода:
Я тестировал на Windows 8.1 Pro (с обновлением 1), используя i7 4700 MQ, под .NET 4.5.1.
Я тестировал и на x86, и на x64, и результаты были идентичными.
Изменить : Я увеличил размер стека всех потоков 201 МБ, размер выборки до 50 миллионов и уменьшил итерации до 5.
Результаты такие же, как указано выше :
Хотя кажется, что стек на самом деле становится медленнее .
источник
Это, безусловно, самая большая опасность, я бы сказал. Что-то серьезно не так с вашим тестом, в коде, который ведет себя так непредсказуемо, обычно где-то скрыта неприятная ошибка.
Очень и очень трудно потреблять много стекового пространства в программе .NET, за исключением чрезмерной рекурсии. Размер стека каркаса управляемых методов задан в камне. Просто сумма аргументов метода и локальных переменных в методе. За исключением тех, которые могут быть сохранены в регистре процессора, вы можете игнорировать это, так как их так мало.
Увеличение размера стека ничего не даст, вы просто зарезервируете кучу адресного пространства, которое никогда не будет использовано. Нет механизма, который мог бы объяснить увеличение производительности от использования памяти, конечно.
В отличие от нативной программы, особенно написанной на C, она также может зарезервировать место для массивов в кадре стека. Основной вектор атаки вредоносного ПО за переполнением стекового буфера. Возможно, и в C #, вам придется использовать
stackalloc
ключевое слово. Если вы делаете это, то очевидной опасностью является необходимость написания небезопасного кода, подверженного таким атакам, а также случайного повреждения фрейма стека. Очень сложно диагностировать ошибки. В последующих джиттерах есть контрмеры против этого, я думаю, начиная с .NET 4.0, где джиттер генерирует код для помещения «cookie» в фрейм стека и проверяет, остается ли он неповрежденным, когда метод возвращается. Мгновенный сбой на рабочем столе без какого-либо способа перехватить или сообщить о неудаче, если это произойдет. Это ... опасно для психического состояния пользователя.Основной поток вашей программы, запущенный операционной системой, будет иметь размер стека 1 МБ по умолчанию, 4 МБ при компиляции вашей программы для x64. Увеличение, которое требует запуска Editbin.exe с параметром / STACK в событии после сборки. Обычно вы можете запросить до 500 МБ, прежде чем ваша программа будет иметь проблемы с запуском при работе в 32-битном режиме. Потоки тоже могут, конечно, намного проще, опасная зона обычно составляет около 90 МБ для 32-битной программы. Срабатывает, когда ваша программа долгое время работала, а адресное пространство было фрагментировано из предыдущих выделений. Общее использование адресного пространства уже должно быть высоким, более одного гигабайта, чтобы получить этот режим отказа.
Тройной проверь свой код, там что-то не так. Вы не можете получить ускорение в 5 раз с большим стеком, если не напишите явно свой код, чтобы воспользоваться им. Который всегда требует небезопасного кода. Использование указателей в C # всегда имеет ловкость для создания более быстрого кода, он не подвергается проверкам границ массива.
источник
float[]
кfloat*
. Большой стек был просто, как это было достигнуто. Ускорение в 5 раз в некоторых случаях вполне оправдано для этого изменения.У меня была бы оговорка, что я просто не знаю, как это предсказать - разрешения, GC (который должен сканировать стек) и т. Д. - все это может быть затронуто. Я бы очень хотел использовать неуправляемую память вместо этого:
источник
stackalloc
не подлежит сборке мусора.stackalloc
- ему нужно прыгнуть, и вы надеетесь, что он сделает это без особых усилий - но я пытаюсь подчеркнуть, что он вводит ненужные осложнения / проблемы. ИМО,stackalloc
отлично подходит в качестве рабочего буфера, но для выделенного рабочего пространства более вероятно, что он просто выделит где-то чанк-память, а не будет злоупотреблять / сбивать с толку стек,Одна вещь, которая может пойти не так, это то, что вы можете не получить разрешение на это. Если не работать в режиме полного доверия, Framework просто проигнорирует запрос на больший размер стека (см. MSDN на
Thread Constructor (ParameterizedThreadStart, Int32)
)Вместо того, чтобы увеличивать размер системного стека до таких огромных чисел, я бы предложил переписать ваш код, чтобы он использовал итерацию и ручную реализацию стека в куче.
источник
Массивы с высокой производительностью могут быть доступны так же, как и обычный C #, но это может стать началом проблемы: Рассмотрим следующий код:
Вы ожидаете исключение вне границы, и это полностью имеет смысл, потому что вы пытаетесь получить доступ к элементу 200, но максимально допустимое значение равно 99. Если вы идете по маршруту stackalloc, тогда не будет никакого объекта, обернутого вокруг вашего массива для проверки привязки, и следующее не будет показывать никаких исключений:
Выше вы выделяете достаточно памяти для хранения 100 float, и вы устанавливаете размер памяти (float), который начинается с места, начатого в этой памяти, + 200 * sizeof (float) для хранения вашего значения float 10. Неудивительно, что эта память находится за пределами выделенная память для поплавков, и никто не будет знать, что может быть сохранено в этом адресе. Если вам повезет, вы могли использовать некоторую неиспользуемую в данный момент память, но в то же время, скорее всего, вы могли бы перезаписать какое-то место, которое использовалось для хранения других переменных. Подводя итог: непредсказуемое поведение во время выполнения.
источник
stackalloc
, в этом случае мы говорим иfloat*
т. д. - которые не имеют одинаковые проверки. Это вызваноunsafe
очень веской причиной. Лично я очень рад использовать,unsafe
когда есть веская причина, но Сократ делает некоторые разумные замечания.Языки микробенчмаркинга с JIT и GC, такие как Java или C #, могут быть немного сложными, поэтому, как правило, хорошей идеей является использование существующего фреймворка - Java предлагает mhf или Caliper, которые превосходны, к сожалению, насколько мне известно, C # не предлагает все, что приближается к тем. Джон Скит написал это здесь, и я буду слепо полагать, что он позаботится о самых важных вещах (Джон знает, что он делает в этой области; да, нет никаких забот, которые я действительно проверял). Я немного подправил время, потому что 30 секунд на тест после прогрева было слишком много для моего терпения (5 секунд должно быть).
Итак, сначала результаты .NET 4.5.1 под Windows 7 x64 - числа обозначают итерации, которые он может выполнить за 5 секунд, поэтому чем выше, тем лучше.
x64 JIT:
x86 JIT (да, это все еще грустно):
Это дает гораздо более разумное ускорение не более 14% (и большая часть накладных расходов связана с тем, что GC должен работать, реально считайте, что это наихудший сценарий). Результаты x86 интересны - не совсем понятно, что там происходит.
и вот код:
источник
12500000
качестве размера я на самом деле получаю исключение stackoverflow. Но в основном речь шла о том, чтобы отвергнуть основную предпосылку, что использование кода, выделенного из стека, на несколько порядков быстрее. В противном случае мы делаем здесь наименьшее количество работы, и разница уже составляет всего около 10-15% - на практике она будет еще ниже ... это, на мой взгляд, определенно меняет всю дискуссию.Поскольку разница в производительности слишком велика, проблема едва связана с распределением. Вероятно, это вызвано доступом к массиву.
Я разобрал тело цикла функций:
TestMethod1:
TestMethod2:
Мы можем проверить использование инструкции и, что более важно, исключение, которое они выдают в спецификации ECMA :
Исключения это бросает:
И
Исключение это бросает:
Как видите,
stelem
больше работает в проверке диапазона массива и проверке типа. Поскольку тело цикла мало что делает (присваивает только значение), накладные расходы на проверку доминируют во времени вычислений. Вот почему производительность отличается на 530%.И это также отвечает на ваши вопросы: опасность заключается в отсутствии проверки диапазона и типа массива. Это небезопасно (как указано в объявлении функции; D).
источник
РЕДАКТИРОВАТЬ: (небольшое изменение в коде и в измерении приводит к значительным изменениям в результате)
Сначала я запустил оптимизированный код в отладчике (F5), но это было неправильно. Он должен быть запущен без отладчика (Ctrl + F5). Во-вторых, код может быть полностью оптимизирован, поэтому мы должны усложнить его, чтобы оптимизатор не мешал нашим измерениям. Я сделал все методы, возвращающие последний элемент в массиве, и массив заполняется по-разному. Также в ОП есть дополнительный ноль,
TestMethod2
который всегда делает его в десять раз медленнее.Я попробовал некоторые другие методы, в дополнение к двум, которые вы предоставили. Метод 3 имеет тот же код, что и ваш метод 2, но функция объявлена
unsafe
. Метод 4 использует указатель доступа к регулярно создаваемому массиву. Метод 5 использует указатель доступа к неуправляемой памяти, как описано Марком Гравеллом. Все пять методов работают в очень похожее время. M5 - самый быстрый (а M1 - второй). Разница между самым быстрым и самым медленным составляет около 5%, что меня не волнует.источник
TestMethod4
противTestMethod1
гораздо лучшего сравненияstackalloc
.