Skip to content

inline functions causes different behaviours in both its implementation and caller #18757

@RickyYCheng

Description

@RickyYCheng
let [<TailCall>] inline Y ([<InlineIfLambda>] f) x = 
    let rec y x = f y x
    y x

type Vertex = (struct(int * int))

type [<Struct>] Graph = 
    {
        Grid: byte[,]
        Rows: int
        Cols: int
        Shift: struct(int * int)
        ObstacleID: byte
        PlatformID: byte
    }

type [<Struct>] Agent = 
    {
        MaxJumpHeight: uint
        AABBLeft: uint
        AABBTop: uint
        AABBRight: uint
    }

let inline isGrounding 
    ({Grid = grid; Rows = rows; Cols = cols; Shift = shift; ObstacleID = obstacleID; PlatformID = platformID} as graph: Graph) 
    ({MaxJumpHeight = maxJumpHeight; AABBLeft = absL; AABBTop = absT; AABBRight = absR} as agent: Agent) 
    ((x, y): Vertex) = 
    
    if y + 1 >= rows then false
    else 
        let left = - int absL
        // let top = - int absT
        let right = int absR
        
        // let mutable i = left
        // let mutable found = false
        // while i <= right && x + i < cols && not found do
        //     if x + i < 0 then 
        //         i <- -x
        //     elif grid[y + 1, x + i] = obstacleID || grid[y + 1, x + i] = platformID then 
        //         found <- true
        //     else 
        //         i <- i + 1
        // found
        let rec loop i = 
            if x + i < 0 then loop -x
            elif i > right || x + i >= cols then false
            elif grid[y + 1, x + i] = obstacleID || grid[y + 1, x + i] = platformID then true
            else loop (i + 1)
        loop left // or use Y-combinators

let foo() = 
    let graph: Graph = 
        {
            Grid = array2D [|
                [|0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy; 255uy|]
                [|0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy; 0uy|]
            |]

            Rows = 9
            Cols = 16
            Shift = struct(0, 0)
            ObstacleID = 0uy
            PlatformID = 1uy
        }

    let agent: Agent = 
        { 
            MaxJumpHeight = 3u
            AABBLeft = 1u
            AABBTop = 1u
            AABBRight = 1u 
        }
    isGrounding graph agent (0, 0)

dotnet 8/9
Then we could check compiled code:

  1. (Caller) For the isGrounding's implementation in function foo(), everything is ok.
public static bool foo() {
    // xxx
    return loop@46-1(platformID@, obstacleID@, grid@, cols@, item, item2, num, i);
}

Static func so it is ok

  1. (Self) For the isGrounding, heap allocations is created.
    public static bool isGrounding(Graph graph, Agent agent, ValueTuple<int, int> _arg1)
    {
        int rows@ = graph.Rows@;
        byte platformID@ = graph.PlatformID@;
        byte obstacleID@ = graph.ObstacleID@;
        byte[,] grid@ = graph.Grid@;
        int cols@ = graph.Cols@;
        uint aABBRight@ = agent.AABBRight@;
        uint aABBLeft@ = agent.AABBLeft@;
        int item = _arg1.Item2;
        int item2 = _arg1.Item1;
        if (item + 1 >= rows@)
        {
            return false;
        }
        int func = (int)(0 - aABBLeft@);
        int right = (int)aABBRight@;
        FSharpFunc<int, bool> fSharpFunc = new loop@47(platformID@, obstacleID@, grid@, cols@, item, item2, right);
        return fSharpFunc.Invoke(func);
    }

There are some heap allocations here since closure captured a lot of variables.

Expected behavior

Produce similar msil code. No allocations is needed.

Actual behavior

Create closures and heap allocations.

Known workarounds

Use while or no-inlining.

Related information

Provide any related information (optional):

  • Windows 11
  • .Net 8 / .Net 9
  • MSBuild, SharpLab, ILSPY

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions