Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stack Overflow when using curried interface member, no stack overflow with tupled version #87393

Closed
dawedawe opened this issue Jun 12, 2023 · 11 comments · Fixed by #87395
Closed
Assignees
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Milestone

Comments

@dawedawe
Copy link

Description

The following program causes a stack overflow in release build. If you follow the comments and use the tupled version of ApplyEval the stack overflow doesn't happen and the program runs till exit.
As far as I understand the dump of the IL, there are tail. prefixes in both versions but it seems that the runtime doesn't obey them for the curried version.
In the diff between the curried and the tupled IL, I see a big differences in the stack of
.override method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>
Maybe that's a hint that can help?
Please tell me if I can help in any way or if this should be raised in another repository.

namespace Bar

type Foo<'a> =
    | Pure of 'a
    | Apply of ApplyCrate<'a>

// This curried version of "ApplyEval" causes a stack overflow in Release compilation
and ApplyEval<'a, 'ret> = abstract Eval<'b,'c,'d> : 'b Foo -> 'c Foo -> 'd Foo -> ('b -> 'c -> 'd -> 'a) Foo -> 'ret

// This tupled version of "ApplyEval" does not cause a stack overflow in Release compilation
// and ApplyEval<'a, 'ret> = abstract Eval<'b,'c,'d> : ('b Foo * 'c Foo * 'd Foo * ('b -> 'c -> 'd -> 'a) Foo) -> 'ret

and ApplyCrate<'a> = abstract Apply : ApplyEval<'a, 'ret> -> 'ret

module Foo =
    let apply2 (b : Foo<'b>) (c : Foo<'c>) (d : Foo<'d>) (f : Foo<'b -> 'c -> 'd -> 'a>) : Foo<'a> =
        { new ApplyCrate<'a> with
            member _.Apply<'ret> (eval : ApplyEval<'a,'ret>) =
                // eval.Eval(b, c, d, f)    // uncomment for tupled version
                eval.Eval b c d f        // uncomment for curried version
        }
        |> Apply

    let lift a = Pure a

    let rec evaluateCps<'a, 'b> (f : 'a Foo) (cont : 'a -> 'b) : 'b =
        match f with
        | Pure a -> cont a
        | Apply crate ->
            crate.Apply
                { new ApplyEval<_,_> with
                    // member _.Eval((b, c, d, f)) =    // uncomment for tupled version
                    member _.Eval b c d f =           // uncomment for curried version
                        evaluateCps f (fun f ->
                            evaluateCps b (fun b ->
                                evaluateCps c (fun c ->
                                    evaluateCps d (fun d -> cont (f b c d))
                                )
                            )
                        )
                }

module TailRecBug =

    let overflowtest () =
        let rec makeChain (height : int) (k : int Foo -> 'r) : 'r =
            if height <= 0 then
                k (Foo.lift 0)
            else
                makeChain (height - 1)
                    (fun b ->
                        let c = Foo.lift height
                        let f = Foo.lift (fun a b c -> a + b + c)
                        Foo.apply2 b c c f  |> k)

        let chain = makeChain 10000 id
        Foo.evaluateCps chain id

module Main =

    [<EntryPoint>]
    let main _argv =
        let r = TailRecBug.overflowtest()
        printfn "%A" r
        0

IL of curried version


    .method private hidebysig newslot virtual final 
            instance !b  'Bar.ApplyEval<\'a, \'b>.Eval'<c,d,e>(class Bar.Foo`1<!!c> b,
                                                               class Bar.Foo`1<!!d> c,
                                                               class Bar.Foo`1<!!e> d,
                                                               class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>> f) cil managed
    {
      .override  method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>(class Bar.Foo`1<!!c>,
                                                                       class Bar.Foo`1<!!d>,
                                                                       class Bar.Foo`1<!!e>,
                                                                       class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!0>>>>)
      // Code size       24 (0x18)
      .maxstack  9
      IL_0000:  ldarg.s    f
      IL_0002:  ldarg.0
      IL_0003:  ldfld      class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1> class Bar.Foo/evaluateCps@36<!a,!b>::cont
      IL_0008:  ldarg.1
      IL_0009:  ldarg.2
      IL_000a:  ldarg.3
      IL_000b:  newobj     instance void class Bar.Foo/'evaluateCps@39-1'<!a,!b,!!c,!!d,!!e>::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1>,
                                                                                                    class Bar.Foo`1<!2>,
                                                                                                    class Bar.Foo`1<!3>,
                                                                                                    class Bar.Foo`1<!4>)
      IL_0010:  tail.
      IL_0012:  call       !!1 Bar.Foo::evaluateCps<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!1,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!2,!a>>>,!b>(class Bar.Foo`1<!!0>,
                                                                                                                                                                                                                                            class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,!!1>)
      IL_0017:  ret
    } // end of method evaluateCps@36::'Bar.ApplyEval<\'a, \'b>.Eval'

IL of tupled version

    .method private hidebysig newslot virtual final 
            instance !b  'Bar.ApplyEval<\'a, \'b>.Eval'<c,d,e>(class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>> _arg1) cil managed
    {
      .override  method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>(class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!0>>>>>)
      // Code size       51 (0x33)
      .maxstack  9
      .locals init (class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>> V_0,
               class Bar.Foo`1<!!e> V_1,
               class Bar.Foo`1<!!d> V_2,
               class Bar.Foo`1<!!c> V_3)
      IL_0000:  ldarg.1
      IL_0001:  call       instance !3 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item4()
      IL_0006:  stloc.0
      IL_0007:  ldarg.1
      IL_0008:  call       instance !2 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item3()
      IL_000d:  stloc.1
      IL_000e:  ldarg.1
      IL_000f:  call       instance !1 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item2()
      IL_0014:  stloc.2
      IL_0015:  ldarg.1
      IL_0016:  call       instance !0 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item1()
      IL_001b:  stloc.3
      IL_001c:  ldloc.0
      IL_001d:  ldarg.0
      IL_001e:  ldfld      class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1> class Bar.Foo/evaluateCps@36<!a,!b>::cont
      IL_0023:  ldloc.1
      IL_0024:  ldloc.2
      IL_0025:  ldloc.3
      IL_0026:  newobj     instance void class Bar.Foo/'evaluateCps@39-1'<!a,!b,!!c,!!d,!!e>::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1>,
                                                                                                    class Bar.Foo`1<!4>,
                                                                                                    class Bar.Foo`1<!3>,
                                                                                                    class Bar.Foo`1<!2>)
      IL_002b:  tail.
      IL_002d:  call       !!1 Bar.Foo::evaluateCps<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!1,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!2,!a>>>,!b>(class Bar.Foo`1<!!0>,
                                                                                                                                                                                                                                            class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,!!1>)
      IL_0032:  ret
    } // end of method evaluateCps@36::'Bar.ApplyEval<\'a, \'b>.Eval'

stackoverflow.zip

Reproduction Steps

  • compile the provided .fs file in Release mode

  • run the exe

  • stackoverflow happens

  • follow the comments to switch to the tupled version

  • compile the provided .fs file in Release mode

  • run the exe

  • stackoverflow doesn't happen

Expected behavior

The runtime obeys the tail. prefix for both version and no stack overflow happens in Release mode

Actual behavior

A stack overflow happens:

Stack overflow.
Repeat 5349 times:
--------------------------------
   at Bar.Foo+apply2@17[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Bar.ApplyCrate<'a>.Apply[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Bar.ApplyEval`2<Int32,Int32>)
   at System.Runtime.CompilerServices.RuntimeHelpers.DispatchTailCalls(IntPtr, Void (IntPtr, Byte ByRef, System.Runtime.CompilerServices.PortableTailCallFrame*), Byte ByRef)
--------------------------------
   at Bar.Foo+apply2@17[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Bar.ApplyCrate<'a>.Apply[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Bar.ApplyEval`2<Int32,Int32>)
   at Bar.Main.main(System.String[])

Regression?

As far as I know, this is not a regression.

Known Workarounds

Use the tupled version

Configuration

.NET 7 on x64 Win11

Other information

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Jun 12, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jun 12, 2023
@jakobbotsch jakobbotsch added area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Jun 12, 2023
@jakobbotsch jakobbotsch self-assigned this Jun 12, 2023
@jakobbotsch jakobbotsch removed the untriaged New issue has not been triaged by the area owner label Jun 12, 2023
@ghost
Copy link

ghost commented Jun 12, 2023

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

The following program causes a stack overflow in release build. If you follow the comments and use the tupled version of ApplyEval the stack overflow doesn't happen and the program runs till exit.
As far as I understand the dump of the IL, there are tail. prefixes in both versions but it seems that the runtime doesn't obey them for the curried version.
In the diff between the curried and the tupled IL, I see a big differences in the stack of
.override method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>
Maybe that's a hint that can help?
Please tell me if I can help in any way or if this should be raised in another repository.

namespace Bar

type Foo<'a> =
    | Pure of 'a
    | Apply of ApplyCrate<'a>

// This curried version of "ApplyEval" causes a stack overflow in Release compilation
and ApplyEval<'a, 'ret> = abstract Eval<'b,'c,'d> : 'b Foo -> 'c Foo -> 'd Foo -> ('b -> 'c -> 'd -> 'a) Foo -> 'ret

// This tupled version of "ApplyEval" does not cause a stack overflow in Release compilation
// and ApplyEval<'a, 'ret> = abstract Eval<'b,'c,'d> : ('b Foo * 'c Foo * 'd Foo * ('b -> 'c -> 'd -> 'a) Foo) -> 'ret

and ApplyCrate<'a> = abstract Apply : ApplyEval<'a, 'ret> -> 'ret

module Foo =
    let apply2 (b : Foo<'b>) (c : Foo<'c>) (d : Foo<'d>) (f : Foo<'b -> 'c -> 'd -> 'a>) : Foo<'a> =
        { new ApplyCrate<'a> with
            member _.Apply<'ret> (eval : ApplyEval<'a,'ret>) =
                // eval.Eval(b, c, d, f)    // uncomment for tupled version
                eval.Eval b c d f        // uncomment for curried version
        }
        |> Apply

    let lift a = Pure a

    let rec evaluateCps<'a, 'b> (f : 'a Foo) (cont : 'a -> 'b) : 'b =
        match f with
        | Pure a -> cont a
        | Apply crate ->
            crate.Apply
                { new ApplyEval<_,_> with
                    // member _.Eval((b, c, d, f)) =    // uncomment for tupled version
                    member _.Eval b c d f =           // uncomment for curried version
                        evaluateCps f (fun f ->
                            evaluateCps b (fun b ->
                                evaluateCps c (fun c ->
                                    evaluateCps d (fun d -> cont (f b c d))
                                )
                            )
                        )
                }

module TailRecBug =

    let overflowtest () =
        let rec makeChain (height : int) (k : int Foo -> 'r) : 'r =
            if height <= 0 then
                k (Foo.lift 0)
            else
                makeChain (height - 1)
                    (fun b ->
                        let c = Foo.lift height
                        let f = Foo.lift (fun a b c -> a + b + c)
                        Foo.apply2 b c c f  |> k)

        let chain = makeChain 10000 id
        Foo.evaluateCps chain id

module Main =

    [<EntryPoint>]
    let main _argv =
        let r = TailRecBug.overflowtest()
        printfn "%A" r
        0

IL of curried version


    .method private hidebysig newslot virtual final 
            instance !b  'Bar.ApplyEval<\'a, \'b>.Eval'<c,d,e>(class Bar.Foo`1<!!c> b,
                                                               class Bar.Foo`1<!!d> c,
                                                               class Bar.Foo`1<!!e> d,
                                                               class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>> f) cil managed
    {
      .override  method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>(class Bar.Foo`1<!!c>,
                                                                       class Bar.Foo`1<!!d>,
                                                                       class Bar.Foo`1<!!e>,
                                                                       class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!0>>>>)
      // Code size       24 (0x18)
      .maxstack  9
      IL_0000:  ldarg.s    f
      IL_0002:  ldarg.0
      IL_0003:  ldfld      class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1> class Bar.Foo/evaluateCps@36<!a,!b>::cont
      IL_0008:  ldarg.1
      IL_0009:  ldarg.2
      IL_000a:  ldarg.3
      IL_000b:  newobj     instance void class Bar.Foo/'evaluateCps@39-1'<!a,!b,!!c,!!d,!!e>::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1>,
                                                                                                    class Bar.Foo`1<!2>,
                                                                                                    class Bar.Foo`1<!3>,
                                                                                                    class Bar.Foo`1<!4>)
      IL_0010:  tail.
      IL_0012:  call       !!1 Bar.Foo::evaluateCps<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!1,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!2,!a>>>,!b>(class Bar.Foo`1<!!0>,
                                                                                                                                                                                                                                            class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,!!1>)
      IL_0017:  ret
    } // end of method evaluateCps@36::'Bar.ApplyEval<\'a, \'b>.Eval'

IL of tupled version

    .method private hidebysig newslot virtual final 
            instance !b  'Bar.ApplyEval<\'a, \'b>.Eval'<c,d,e>(class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>> _arg1) cil managed
    {
      .override  method instance !1 class Bar.ApplyEval`2<!a,!b>::Eval<[3]>(class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!0>>>>>)
      // Code size       51 (0x33)
      .maxstack  9
      .locals init (class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>> V_0,
               class Bar.Foo`1<!!e> V_1,
               class Bar.Foo`1<!!d> V_2,
               class Bar.Foo`1<!!c> V_3)
      IL_0000:  ldarg.1
      IL_0001:  call       instance !3 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item4()
      IL_0006:  stloc.0
      IL_0007:  ldarg.1
      IL_0008:  call       instance !2 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item3()
      IL_000d:  stloc.1
      IL_000e:  ldarg.1
      IL_000f:  call       instance !1 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item2()
      IL_0014:  stloc.2
      IL_0015:  ldarg.1
      IL_0016:  call       instance !0 class [System.Runtime]System.Tuple`4<class Bar.Foo`1<!!c>,class Bar.Foo`1<!!d>,class Bar.Foo`1<!!e>,class Bar.Foo`1<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!c,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!d,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!e,!a>>>>>::get_Item1()
      IL_001b:  stloc.3
      IL_001c:  ldloc.0
      IL_001d:  ldarg.0
      IL_001e:  ldfld      class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1> class Bar.Foo/evaluateCps@36<!a,!b>::cont
      IL_0023:  ldloc.1
      IL_0024:  ldloc.2
      IL_0025:  ldloc.3
      IL_0026:  newobj     instance void class Bar.Foo/'evaluateCps@39-1'<!a,!b,!!c,!!d,!!e>::.ctor(class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!0,!1>,
                                                                                                    class Bar.Foo`1<!4>,
                                                                                                    class Bar.Foo`1<!3>,
                                                                                                    class Bar.Foo`1<!2>)
      IL_002b:  tail.
      IL_002d:  call       !!1 Bar.Foo::evaluateCps<class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!1,class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!2,!a>>>,!b>(class Bar.Foo`1<!!0>,
                                                                                                                                                                                                                                            class [FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2<!!0,!!1>)
      IL_0032:  ret
    } // end of method evaluateCps@36::'Bar.ApplyEval<\'a, \'b>.Eval'

stackoverflow.zip

Reproduction Steps

  • compile the provided .fs file in Release mode

  • run the exe

  • stackoverflow happens

  • follow the comments to switch to the tupled version

  • compile the provided .fs file in Release mode

  • run the exe

  • stackoverflow doesn't happen

Expected behavior

The runtime obeys the tail. prefix for both version and no stack overflow happens in Release mode

Actual behavior

A stack overflow happens:

Stack overflow.
Repeat 5349 times:
--------------------------------
   at Bar.Foo+apply2@17[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Bar.ApplyCrate<'a>.Apply[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Bar.ApplyEval`2<Int32,Int32>)
   at System.Runtime.CompilerServices.RuntimeHelpers.DispatchTailCalls(IntPtr, Void (IntPtr, Byte ByRef, System.Runtime.CompilerServices.PortableTailCallFrame*), Byte ByRef)
--------------------------------
   at Bar.Foo+apply2@17[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Bar.ApplyCrate<'a>.Apply[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Bar.ApplyEval`2<Int32,Int32>)
   at Bar.Main.main(System.String[])

Regression?

As far as I know, this is not a regression.

Known Workarounds

Use the tupled version

Configuration

.NET 7 on x64 Win11

Other information

No response

Author: dawedawe
Assignees: jakobbotsch
Labels:

area-CodeGen-coreclr, untriaged

Milestone: -

@jakobbotsch
Copy link
Member

Thanks for the report. The problem here is that the JIT gets confused because internally, the call to eval.Eval b c d f is expanded into two separate calls, where the first call computes the target address.
The JIT sets things up under the expectation that the first call is the one dispatching to user code, but in reality the second call does that.

@jakobbotsch jakobbotsch added this to the 8.0.0 milestone Jun 12, 2023
@dawedawe
Copy link
Author

Wow, thanks for the super fast feedback. Much appreciated.
Do you think a fix can make it into 8.0.0?

jakobbotsch added a commit to jakobbotsch/runtime that referenced this issue Jun 12, 2023
…esolving

In the helper-based tailcall mechanism it is possible that we expand the
target call into two actual calls: first, a call to
CORINFO_HELP_VIRTUAL_FUNC_PTR to compute the target, and second a call
to that target. We were not taking into account that the return address
needed for the tailcall mechanism needs to be from the second call.

In this particular case the runtime does not request the JIT to pass the
target; that means we end up resolving the target from both the caller
and from the CallTailCallTarget stub. Ideally the JIT would be able to
eliminate the CORINFO_HELP_VIRTUAL_FUNC_PTR call in the caller since it
turns out to be unused, but that requires changes in DCE (and is
somewhat non-trivial, as we have to preserve a null-check).

A simpler way to improve the case is to just change the runtime to
always request the target from the JIT for GVMs, which means the
CallTailCallTarget stub no longer needs to resolve it. That also has the
effect of fixing the original issue, but I have left the original fix in
as well.

Fix dotnet#87393
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jun 12, 2023
@jakobbotsch
Copy link
Member

@dawedawe Yes, this should make .NET 8. #87395 has the fix.

@dawedawe
Copy link
Author

Awesome, thank you so much!

jakobbotsch added a commit that referenced this issue Jun 12, 2023
…esolving (#87395)

In the helper-based tailcall mechanism it is possible that we expand the
target call into two actual calls: first, a call to
CORINFO_HELP_VIRTUAL_FUNC_PTR to compute the target, and second a call
to that target. We were not taking into account that the return address
needed for the tailcall mechanism needs to be from the second call.

In this particular case the runtime does not request the JIT to pass the
target; that means we end up resolving the target from both the caller
and from the CallTailCallTarget stub. Ideally the JIT would be able to
eliminate the CORINFO_HELP_VIRTUAL_FUNC_PTR call in the caller since it
turns out to be unused, but that requires changes in DCE (and is
somewhat non-trivial, as we have to preserve a null-check).

A simpler way to improve the case is to just change the runtime to
always request the target from the JIT for GVMs, which means the
CallTailCallTarget stub no longer needs to resolve it. That also has the
effect of fixing the original issue, but I have left the original fix in
as well.

Fix #87393
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jun 12, 2023
@dawedawe
Copy link
Author

Hey @jakobbotsch, any chance to get this backported to .NET 7? That would help a very big F# customer ;)

Also, is it possible to predict when the nightlies will have it?
It seems to not be in dotnet-sdk-8.0.100-preview.6.23313.16-win-x64 yet.

@jakobbotsch
Copy link
Member

Hey @jakobbotsch, any chance to get this backported to .NET 7? That would help a very big F# customer ;)

Yes, we can backport this if the workaround is not sufficient for your situation. Is it the case? cc @JulieLeeMSFT

Note that the workaround may actually have better performance characteristics. It trades off an allocation for being able to make a more efficient tailcall (basically a single jmp instruction in the generated assembly code). The curried version does not require a tuple allocation but goes through a slower tailcalling mechanism.

Also, is it possible to predict when the nightlies will have it? It seems to not be in dotnet-sdk-8.0.100-preview.6.23313.16-win-x64 yet.

I can see that the current nightly is built from 54dab73 (you can click the blue version badge in the table in dotnet/installer to get this). Presumably the next build should have the fix, but I'm not sure when it will be available.

@dawedawe
Copy link
Author

Hey @jakobbotsch, any chance to get this backported to .NET 7? That would help a very big F# customer ;)

Yes, we can backport this if the workaround is not sufficient for your situation. Is it the case? cc @JulieLeeMSFT

I think, it would be good to have in 7 to lower the risk of running into it, as one might not see the StackOverflow during development and moving production code to a new 7 release is done quicker than to 8.

@dawedawe
Copy link
Author

Alright, I can confirm it's fixed in dotnet-sdk-8.0.100-preview.6.23315.27-win-x64.
Thanks again for the great response.

@jakobbotsch
Copy link
Member

Glad to hear it.

I think, it would be good to have in 7 to lower the risk of running into it, as one might not see the StackOverflow during development and moving production code to a new 7 release is done quicker than to 8.

We typically only backport fixes when there is a concrete user scenario, not to minimize potential risk (since the fix itself comes with risk, the bar for servicing older releases is higher). In this case the behavior is quite old (from .NET core 3.1, I believe) and requires a specific set of circumstances to trigger:

  1. The tail call must be to a generic virtual method
  2. The generic type arguments must all be value types

Based on this another workaround would be to store the target into a delegate and then do the call through that. It has the benefit that it does not require changing the shape of the API. E.g. add the following function:

[<MethodImpl(MethodImplOptions.NoInlining)>]
let applyEval<'a, 'b, 'c, 'd, 'e> (fn : 'a -> 'b -> 'c -> 'd -> 'e) b c d f =
    fn b c d f

and change eval.Eval b c d f to applyEval eval.Eval b c d f.

With that said, if the workaround is not sufficient (or hard to apply) then I would be happy to backport the fix -- please let me know!

@dawedawe
Copy link
Author

I talked with the affected party, it's enough to have this in .NET 8 :)

@ghost ghost locked as resolved and limited conversation to collaborators Jul 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants