Какова опасность при создании потока с размером стека размером 50x по умолчанию?

В настоящее время я работаю над очень критичной для производительности программой, и один путь, который я решил изучить, что может помочь уменьшить потребление ресурсов, увеличил размер стека моих рабочих потоков, чтобы я мог перемещать большую часть данных (float[]s), которые Я буду подключаться к стеку (используя 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;
    }
}

Ответ 1

Сравнивая тестовый код с Сэмом, я решил, что мы оба правы!
Однако о разных вещах:

  • Доступ к памяти (чтение и запись) выполняется так же быстро, где бы он ни находился - стек, глобальный или куча.
  • Выделение, однако, это самый быстрый из стека и самый медленный в куче.

Это выглядит следующим образом: stack < < t21 < heap. (время размещения)
Технически, распределение стеков на самом деле не является распределением, среда выполнения просто гарантирует, что часть массива (фрейм?) Зарезервирована для массива.

Я настоятельно советую быть осторожным с этим. Я рекомендую следующее:

  • Когда вам нужно часто создавать массивы, которые никогда не покидают функцию (например, передавая ее ссылку), использование стека будет огромным улучшением.
  • Если вы можете перерабатывать массив, делайте это каждый раз, когда сможете! Куча - лучшее место для хранения долгосрочных объектов. (загрязняющая глобальная память не хороша, кадры стека могут исчезнуть)

(Примечание: 1. применяется только к типам значений, типы ссылок будут выделены в куче, а преимущество будет уменьшено до 0)

Чтобы ответить на вопрос: я вообще не сталкивался с какой-либо проблемой с помощью теста большого стека. Я считаю, что единственными возможными проблемами являются переполнение стека, если вы не будете осторожны с вашими вызовами функций и исчерпаете память при создании потока (-ов), если система работает на низком уровне.

Ниже приведен мой первоначальный ответ. Это неверно, и тесты неверны. Он поддерживается только для справки.


Мой тест показывает, что выделенная память в стеке и глобальная память по крайней мере на 15% медленнее, чем (занимает 120% времени) памяти, выделенной кучей, для использования в массивах!

Это мой тестовый код, и это пример вывода:

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row value to the column's.

Я тестировал на Windows 8.1 Pro (с обновлением 1), используя i7 4700 MQ, в .NET 4.5.1
Я тестировал как с x86, так и x64, и результаты идентичны.

Изменить. Я увеличил размер стека всех потоков 201 МБ, размер выборки до 50 миллионов и уменьшил итерации до 5.
Результаты те же, что и выше:

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row value to the column's.

Хотя, кажется, стек фактически замедляется.

Ответ 2

Я обнаружил увеличение скорости обработки на 530%!

Это, безусловно, самая большая опасность, которую я бы сказал. Там что-то серьезно не так с вашим эталоном, код, который ведет себя непредсказуемо, обычно имеет неприятную ошибку, скрытую где-то.

Очень сложно загружать много пространства стека в .NET-программе, за исключением чрезмерной рекурсии. Размер рамки стека управляемых методов устанавливается в виде камня. Просто сумма аргументов метода и локальных переменных в методе. Минус тех, которые могут быть сохранены в регистре CPU, вы можете игнорировать это, поскольку их так мало.

Увеличение размера стека не приведет к чему-либо, вы просто зарезервируете кучу адресного пространства, которое никогда не будет использоваться. Нет никакого механизма, который мог бы объяснить прирост перпендикуляра, не используя память, конечно.

Это не похоже на родную программу, особенно написанную на C, она также может зарезервировать пространство для массивов в фрейме стека. Основной вектор атаки вредоносного ПО за переполнение стекового буфера. Возможно, в С# вам нужно использовать ключевое слово stackalloc. Если вы это делаете, то очевидной опасностью является необходимость писать небезопасный код, подверженный таким атакам, а также случайное повреждение фреймов стека. Очень сложно диагностировать ошибки. В последнее время наблюдается контрмера против этого, я думаю, начиная с .NET 4.0, где дрожание генерирует код, чтобы помещать "cookie" в фрейм стека и проверяет, остается ли он в неизменном виде, когда метод возвращается. Мгновенный сбой на рабочем столе без какого-либо способа перехватить или сообщить о неудаче, если это произойдет. Это... опасно для психического состояния пользователя.

Основной поток вашей программы, который запускается операционной системой, по умолчанию будет иметь стек 1 МБ, 4 МБ, когда вы скомпилируете свою целевую программу x64. Для этого требуется запустить Editbin.exe с параметром /STACK в событии post build. Обычно вы можете запросить до 500 МБ, прежде чем ваша программа начнет работать при работе в 32-битном режиме. Потоки могут тоже, намного проще, опасная зона обычно колеблется около 90 МБ для 32-битной программы. Триггер, когда ваша программа работает в течение длительного времени, а адресное пространство фрагментировано из предыдущих распределений. Для того, чтобы получить этот режим сбоя, для использования этого режима сбоя должно быть уже достаточно высокого уровня.

Тройка-проверьте свой код, там что-то не так. Вы не можете получить ускорение x5 с большим стеком, если вы явно не напишете свой код, чтобы воспользоваться им. Который всегда требует небезопасного кода. Использование указателей в С# всегда имеет умение создавать более быстрый код, он не подвергается проверкам границ массива.

Ответ 3

У меня было бы резервирование там, что я просто не знал бы, как его предсказать - разрешения, GC (который должен сканировать стек) и т.д. - все может быть затронуто. Я бы очень хотел использовать неуправляемую память:

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}

Ответ 4

Одна вещь, которая может пойти не так, заключается в том, что вы не можете получить разрешение на это. Если не работает в режиме полного доверия, Framework просто проигнорирует запрос на больший размер стека (см. MSDN на Thread Constructor (ParameterizedThreadStart, Int32))

Вместо увеличения размера системного стека до таких огромных чисел я бы предложил переписать код так, чтобы он использовал итерацию и реализацию ручного стека в куче.

Ответ 5

Высокопроизводительные массивы могут быть доступны так же, как обычный С#, но это может стать началом проблемы. Рассмотрим следующий код:

float[] someArray = new float[100]
someArray[200] = 10.0;

Вы ожидаете исключение из привязки, и это совершенно разумно, потому что вы пытаетесь получить доступ к элементу 200, но максимальное допустимое значение равно 99. Если вы перейдете к маршруту stackalloc, то не будет объекта, обернутого вокруг вашего массива, связанного проверьте, и следующее не будет отображаться:

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

Вы выделяете достаточно памяти для хранения 100 поплавков, и вы устанавливаете местоположение памяти sizeof (float), которое начинается с местоположения, начатого в этой памяти + 200 * sizeof (float) для хранения вашего значения с плавающей точкой 10. Неудивительно, что эта память находится за пределами выделенной памяти для поплавков, и никто не знает, что может быть сохранено в этом адресе. Если вам повезет, вы могли бы использовать некоторую в настоящее время неиспользуемую память, но в то же время, вероятно, вы можете переписать некоторое место, которое использовалось для хранения других переменных. Обобщение: непредсказуемое поведение во время выполнения.

Ответ 6

Языки Microbenchmarking с JIT и GC, такие как Java или С#, могут быть немного сложными, поэтому обычно рекомендуется использовать существующую инфраструктуру. Java предлагает mhf или Caliper, которые превосходны, к сожалению, насколько мне известно. С# doesn Не предлагайте ничего подобного. Джон Скит написал этот здесь, о котором я буду вслепую предполагать, заботится о самых важных вещах (Джон знает, что он делает в этой области, да и не беспокойтесь, я действительно проверял). Я немного изменил время, потому что 30 секунд на тест после разминки было слишком много для моего терпения (5 секунд должно было быть сделано).

Итак, сначала результаты .NET 4.5.1 под Windows 7 x64 - числа обозначают итерации, которые он мог бы запустить за 5 секунд, поэтому лучше.

x64 JIT:

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (да, это все еще печально):

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

Это дает гораздо более разумное ускорение не более 14% (и большая часть накладных расходов обусловлена ​​тем, что GC должен работать, считая это наихудшим сценарием реалистично). Результаты x86 интересны, хотя - не совсем понятно, что происходит там.

и здесь код:

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}

Ответ 7

Поскольку разница в производительности слишком велика, проблема едва ли связана с распределением. Вероятно, это вызвано доступом к массиву.

Я разобрал тело цикла функций:

TestMethod1:

IL_0011:  ldloc.0 
IL_0012:  ldloc.1 
IL_0013:  ldc.i4.4 
IL_0014:  mul 
IL_0015:  add 
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1 
IL_001d:  ldc.i4.1 
IL_001e:  add 
IL_001f:  stloc.1 
IL_0020:  ldloc.1 
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2:

IL_0012:  ldloc.0 
IL_0013:  ldloc.1 
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1 
IL_001b:  ldc.i4.1 
IL_001c:  add 
IL_001d:  stloc.1 
IL_001e:  ldloc.1 
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

Мы можем проверить использование инструкции и, что более важно, исключение, которое они выбрали в спецификация ECMA:

stind.r4: Store value of type float32 into memory at address

Исключения, которые он выбрал:

System.NullReferenceException

и

stelem.r4: Replace array element at index with the float32 value on the stack.

Исключение составляет:

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

Как вы можете видеть, stelem работает больше в проверке диапазона массивов и проверке типов. Поскольку тело цикла ничего не делает (только присваивать значение), накладные расходы на проверку превышают время вычисления. Вот почему производительность отличается на 530%.

И это также отвечает на ваши вопросы: опасность заключается в отсутствии диапазона массивов и проверки типов. Это небезопасно (как указано в объявлении функции, D).

Ответ 8

EDIT: (небольшое изменение кода и измерение приводит к большому изменению результата)

Сначала я запустил оптимизированный код в отладчике (F5), но это было неправильно. Он должен запускаться без отладчика (Ctrl + F5). Во-вторых, код может быть тщательно оптимизирован, поэтому мы должны усложнять его, чтобы оптимизатор не смешивался с нашим измерением. Я сделал все методы, возвращая последний элемент в массиве, и массив заполняется по-разному. Также в OP TestMethod2 есть дополнительный ноль, который всегда делает это в десять раз медленнее.

Я попробовал другие методы, помимо двух, которые вы предоставили. Метод 3 имеет тот же код, что и ваш метод 2, но функция объявлена ​​ unsafe. Метод 4 использует доступ указателя к регулярно создаваемому массиву. Метод 5 использует доступ указателя к неуправляемой памяти, как описано Марк Гравелл. Все пять методов запускаются в очень похожие моменты. M5 является самым быстрым (а M1 - вторым). Разница между самым быстрым и самым медленным составляет около 5%, что меня не волнует.

    public static unsafe float TestMethod3()
    {
        float[] samples = new float[5000000];

        for (var ii = 0; ii < 5000000; ii++)
        {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }

        return samples[5000000 - 1];
    }

    public static unsafe float TestMethod4()
    {
        float[] prev = new float[5000000];
        fixed (float* samples = &prev[0])
        {
            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
    }

    public static unsafe float TestMethod5()
    {
        var ptr = Marshal.AllocHGlobal(5000000 * sizeof(float));
        try
        {
            float* samples = (float*)ptr;

            for (var ii = 0; ii < 5000000; ii++)
            {
                samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
            }

            return samples[5000000 - 1];
        }
        finally
        {
            Marshal.FreeHGlobal(ptr);
        }
    }