Skip to content

Optimization inlining applied inconsistently with piping #12416

Open
@mrange

Description

@mrange

Repro steps

I have a simple F# program. I expect in all cases the lambdas should be inlined but they don't seem to be for reasons that I can't make sense of.

// Simple PushStream
type 'T PushStream = ('T -> bool) -> bool

let inline ofArray (vs : _ array) : _ PushStream = fun ([<InlineIfLambda>] r) ->
  let mutable i = 0
  while i < vs.Length && r vs.[i] do
    i <- i + 1
  i = vs.Length

let inline fold ([<InlineIfLambda>] f) z ([<InlineIfLambda>] ps : _ PushStream) =
  let mutable s = z
  let _ = ps (fun v -> s <- f s v; true)
  s

// It turns out that if we pipe using |> the F# compiler don't inlines
//  the lambdas as we like it to
//  So define a more restrictive version of |> that applies function f to a function v
//  As both f and v are restricted to lambas we can apply InlineIfLambda
let inline (|>>) ([<InlineIfLambda>] v : _ -> _) ([<InlineIfLambda>] f : _ -> _) = f v

let values = [|0..10000|]

let thisIsInlined1 () = ofArray values |>> fold (+) 0
let thisIsInlined2 () = 
  let vs = [|0..10000|]
  ofArray vs |>> fold (+) 0

let thisIsNotInlined1 () = ofArray [|0..10000|] |>> fold (+) 0

type Test() =
  let _values = [|0..10000|]
  static let _svalues = [|0..10000|]

  let array vs = 
    ofArray vs |>> fold (+) 0

  member x.thisIsInlined2 () = ofArray values |>> fold (+) 0
  member x.thisIsInlined3 () = array _values

  member x.thisIsNotInlined2 () = ofArray _values |>> fold (+) 0
  member x.thisIsNotInlined3 () = ofArray _svalues |>> fold (+) 0

Expected behavior

All thisIs* methods should have similar inlined version of the data pipeline.

Actual behavior

All thisIsInlined* methods have similar inlined version of the data pipeline.
All thisIsNotInlined* methods have non-inlined code which is how it looks like if we didn't specify InlineIfLambda

Example decompiled:

	public static int thisIsInlined2()
	{
		int[] vs = SeqModule.ToArray<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(0, 1, 10000)));
		int num = 0;
		int num2 = 0;
		for (;;)
		{
			bool flag;
			if (num2 < vs.Length)
			{
				int num3 = vs[num2];
				num += num3;
				flag = true;
			}
			else
			{
				flag = false;
			}
			if (!flag)
			{
				break;
			}
			num2++;
		}
		bool flag2 = num2 == vs.Length;
		return num;
	}
	
	public static int thisIsNotInlined1()
	{
		int[] vs = SeqModule.ToArray<int>(Operators.CreateSequence<int>(Operators.OperatorIntrinsics.RangeInt32(0, 1, 10000)));
		FSharpFunc<FSharpFunc<int, bool>, bool> fsharpFunc = new Program.thisIsNotInlined1@27(vs);
		FSharpRef<int> fsharpRef = new FSharpRef<int>(0);
		fsharpFunc.Invoke(new Program.thisIsNotInlined1@27-1(fsharpRef));
		return fsharpRef.contents;
	}

But the only difference is storing the generated source array in a variable or not:

let thisIsInlined2 () = 
  let vs = [|0..10000|]
  ofArray vs |>> fold (+) 0

let thisIsNotInlined1 () = ofArray [|0..10000|] |>> fold (+) 0

Perhaps there are good reasons for this but I don't understand them.

Known workarounds

Before invoking functions that uses InlineIfLambda make sure the input args to them (except the lambdas) are stored in intermediate bindings.

Related information

  • Operating system : Windows 10
  • .NET Runtime kind : .NET 6 : 6.0.100
  • Editing Tools: Visual Studio 2022

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions