Размер стека в Mono

Я написал крошечный рекурсивный бит кода F #, чтобы узнать, сколько уровней рекурсии я могу поместить в стек под .NET/Mono. Он просто печатает глубину рекурсии всякий раз, когда она является точной мощностью 2, поэтому я обнаруживаю максимальную глубину с точностью до множителя 2.

Я запускаю код в потоке с определенным объемом пространства стека с помощью System.Threading.Thread (ThreadStart, int). В .Net, по-видимому, требуется примерно 100 байт на уровень рекурсии, и я могу получить около 16 миллионов уровней в стеке 2G. Использование памяти во многом аналогично использованию Mono, однако я могу получить только около 30 тысяч уровней. Увеличение значения размера стека, прошедшего до Thread, прошло примерно около 600000, не увеличивает глубину рекурсии.

ulimit сообщает, что размер размера стека равен 1G.

Очевидным объяснением является то, что Mono не будет подчиняться второму аргументу Thread, если он слишком велик. Кто-нибудь, пожалуйста, знает, как убедить Mono выделить большой стек?

Код тривиален, но он ниже на всякий случай, если кто-то заботится:

let rec f i =
    if popcount i = 1 then // population count is one on exact powers of 2
        printf "Got up to %d\n" i
        stdout.Flush ()
    if i = 1000000000 then 0 else 1 + f (i+1)

Ответ 1

Вариант 1: изменение размера моноблока

Очевидным объяснением является то, что Mono не будет подчиняться второму аргументу Thread, если он слишком велик. Кто-нибудь, пожалуйста, знает, как убедить Mono выделить большой стек?

Вы правы, что Mono ограничивает размер стека, даже если вы передаете большое значение. Например, на моей 64-разрядной тестовой машине Cent OS максимальный размер стека, который выделяет Mono, составляет 2 мегабайта. исходный файл Mono С# Thread.cs показывает нам, что происходит, когда вы создаете поток Mono:

public Thread (ThreadStart start, int maxStackSize)
{
    if (start == null)
        throw new ArgumentNullException ("start");

    threadstart = start;
    Internal.stack_size = CheckStackSize (maxStackSize);
}

static int CheckStackSize (int maxStackSize)
{
    if (maxStackSize < 0)
        throw new ArgumentOutOfRangeException ("less than zero", "maxStackSize");

    if (maxStackSize < 131072) // make sure stack is at least 128k big
        return 131072;

    int page_size = Environment.GetPageSize ();

    if ((maxStackSize % page_size) != 0) // round up to a divisible of page size
        maxStackSize = (maxStackSize / (page_size - 1)) * page_size;

    int default_stack_size = (IntPtr.Size / 4) * 1024 * 1024; // from wthreads.c
    if (maxStackSize > default_stack_size)
        return default_stack_size;

    return maxStackSize; 
}

Приведенный выше код ставит жесткие ограничения на размер стека.

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

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

Ответ 2

Вариант 2: F # Хвост вызовов

Один из вариантов заключается в том, чтобы переписать ваш метод так, чтобы вы использовали рекурсивный tail calls. Из предыдущей ссылки (Wikipedia):

Рекурсивная функция хвоста является частным случаем рекурсии, в которой последняя команда, выполняемая в методе, является рекурсивным вызовом. F # и многие другие функциональные языки могут оптимизировать хвостовые рекурсивные функции; поскольку после рекурсивного вызова никакой дополнительной работы не требуется, чтобы функция не запоминала, откуда она появилась, и, следовательно, нет причин выделять дополнительную память в стеке.

F # оптимизирует хвосто-рекурсивные функции, сообщая CLR, чтобы удалить текущий стек стека перед выполнением целевой функции. В результате рекурсивные функции хвоста могут повторно обрабатываться без потери пространства стека.

Существует предостережение - Mono не полностью поддерживает хвостовые рекурсивные вызовы - вам нужно сначала протестировать среду .NET, а затем посмотреть, будет ли код работать в Mono.

Вот ваша функция примера, переписанная с использованием хвостового рекурсивного вызова - она ​​работает как в .NET(с использованием Visual Studio 2010), так и в Mono (Mono версии 3.2.0, F # 3.0):

let f i =
    let rec loop acc counter =
        // display progress every 10k iterations
        let j = counter % 10000
        if j = 0 then
            printf "Got up to %d\n" counter
            stdout.Flush ()

        if counter < 1000000000 then
            // tail recursive
            loop (acc + 1) (counter + 1)
        else
            acc

    loop 0 i

Для входного значения 1:

let result = f 1
printf "result %d\n" result

Выход:

Got up to 10000
Got up to 20000
Got up to 30000
...
Got up to 999980000
Got up to 999990000
Got up to 1000000000
result 999999999

Для входного значения 999,999,998:

let result = f 999999998
printf "result %d\n" result

Выход:

Got up to 1000000000
result 2

В приведенном выше коде используются две переменные для отслеживания прогресса:

acc - аккумулятор, в котором хранится результат вычисления

counter - это просто число рекурсивных вызовов

Почему ваш примерный код не является хвостом рекурсивным?

Перефразируя Как записать хвостохранилища в разделе статьи Wikipedia, мы можем переписать эту строку вашего кода:

if i = 1000000000 then 0 else 1 + f (i+1)

а

if i = 1000000000 then 0
else
    let intermediate = f (i+1) // recursion
    let result = 1 + intermediate // additional operations
    result

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

Ресурсы

Ответ 3

Вариант 3: сделать его итерационным

Один из вариантов - переписать свой метод так, чтобы он не был рекурсивным, но итеративным. То есть вы меняете метод так, чтобы это был цикл, который вычисляет результаты:

let f i =
    let mutable acc = 0
    for counter in i .. 1000000000 do
        // display progress every 10k iterations
        let j = counter % 10000
        if j = 0 then
            printf "Got up to %d\n" counter
            stdout.Flush ()

        if counter < 1000000000 then
            acc <- acc + 1
    acc

Для входного значения 1:

let result = f 1
printf "result %d\n" result

Выход:

Got up to 10000
Got up to 20000
Got up to 30000
...
Got up to 999980000
Got up to 999990000
Got up to 1000000000
result 999999999

Для входного значения 999,999,998:

let result = f 999999998
printf "result %d\n" result

Выход:

Got up to 1000000000
result 2

В приведенном выше коде используются две переменные для отслеживания прогресса:

acc - накопитель, в котором хранится результат вычисления

counter - это просто число итеративных вызовов (количество циклов)

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

Ответ 4

Этот код обходит монолитный предел. Это полезно для dfs в конкурентном программировании.

        public static void IncreaseStack(ThreadStart action, int stackSize = 16000000)
        {
            var thread = new Thread(action, stackSize);
#if __MonoCS__
        const BindingFlags bf = BindingFlags.NonPublic | BindingFlags.Instance;
        var it = typeof(Thread).GetField("internal_thread", bf).GetValue(thread);
        it.GetType().GetField("stack_size", bf).SetValue(it, stackSize);
#endif
            thread.Start();
            thread.Join();
        }