F# NativePtr.stackalloc Unexpected Stack Overflow

2019-07-13 19:57发布

Still doing my F# performance testing and trying to get stack based arrays working. For some more background see here: f# NativePtr.stackalloc in Struct Constructor.

As I understand it, each function call should get its own frame in the stack. This memory is then freed upon return by moving the stack pointer back. However the below causes a stack overflow error - not sure why as the stackalloc is performed inside a function.

Interestingly this only happens in Release mode, not Debug mode.

I believe the standard stack size in dotnet is 1MB and I haven't adjusted mine. I would expect an allocation of 8192 ints (32768 bytes) not to blow the stack.

#nowarn "9"

module File1 =

    open Microsoft.FSharp.NativeInterop
    open System
    open System.Diagnostics    

    let test () =
        let stackAlloc x =
            let mutable ints:nativeptr<int> = NativePtr.stackalloc x
            ()

        let size = 8192            
        let reps = 10000
        let clock = Stopwatch()
        clock.Start()
        for i = 1 to reps do            
            stackAlloc size
        let elapsed = clock.Elapsed.TotalMilliseconds
        let description = "NativePtr.stackalloc"
        Console.WriteLine("{0} ({1} ints, {2} reps): {3:#,##0.####}ms", description, size, reps, elapsed)

    [<EntryPoint>]
    let main argv = 
        printfn "%A" argv
        test ()
        Console.ReadKey() |> ignore
        0

UPDATE After decompiling with ILSpy as suggested by Fyodor Soikin, we can see that inlining has taken place during optimisation. Kinda cool, and kinda scary!

using Microsoft.FSharp.Core;
using System;
using System.Diagnostics;
using System.IO;

[CompilationMapping(SourceConstructFlags.Module)]
public static class File1
{
    public unsafe static void test()
    {
        Stopwatch clock = new Stopwatch();
        clock.Start();
        for (int i = 1; i < 10001; i++)
        {
            IntPtr intPtr = stackalloc byte[8192 * sizeof(int)];
        }
        double elapsed = clock.Elapsed.TotalMilliseconds;
        Console.WriteLine("{0} ({1} ints, {2} reps): {3:#,##0.####}ms", "NativePtr.stackalloc", 8192, 10000, elapsed);
    }

    [EntryPoint]
    public static int main(string[] argv)
    {
        PrintfFormat<FSharpFunc<string[], Unit>, TextWriter, Unit, Unit> format = new PrintfFormat<FSharpFunc<string[], Unit>, TextWriter, Unit, Unit, string[]>("%A");
        PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<string[], Unit>>(Console.Out, format).Invoke(argv);
        File1.File1.test();
        ConsoleKeyInfo consoleKeyInfo = Console.ReadKey();
        return 0;
    }
}

Further to this, the following may be of interest:

http://www.hanselman.com/blog/ReleaseISNOTDebug64bitOptimizationsAndCMethodInliningInReleaseBuildCallStacks.aspx

Also optimization can be tweaked using attributes:

https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.methodimploptions(v=vs.110).aspx?cs-save-lang=1&cs-lang=fsharp#code-snippet-1

1条回答
贼婆χ
2楼-- · 2019-07-13 20:24

This would happen if your stackAlloc function was inlined, thus causing stackalloc to happen within the test's frame. This also explains why it would only happen in Release: inlining is a kind of optimization that would be performed much less aggressively in Debug than Release.

To confirm this, I would try looking at your resulting code with ILSpy.

Why do you need to use stack-allocated arrays in the first place? This looks exactly like the kind of thing that Donald Knuth warned us about. :-)

查看更多
登录 后发表回答