fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Non-inline SRTP usage

Open gimbling-away opened this issue 1 year ago • 9 comments

I propose we allow non-inline SRTP functions

The existing way of approaching this problem in F# is ... Functions that use SRTPs are forced to be inline, which is not ideal

  • As it can lead to larger binaries with larger functions being unnecessarily inlined
  • Functional control flow based on recursion becomes impossible to do

Pros and Cons

The advantages of making this adjustment to F# are Smaller binaries, recursive SRTP usage

The disadvantages of making this adjustment to F# are None, as of now.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): M

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick these items by placing a cross in the box:

  • [X] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • [X] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
  • [X] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
  • [X] I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • [X] This is not a breaking change to the F# language design
  • [ ] I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

gimbling-away avatar Mar 14 '24 10:03 gimbling-away

~~But...SRTP is statically resolved, hence has to be inlined.~~ See some discussions below

vzarytovskii avatar Mar 14 '24 10:03 vzarytovskii

But...SRTP is statically resolved, hence has to be inlined.

Or I should say, that the way they designed and functioning, they require to be inlined now. I would say it's one of the those things which was decided when designing F# 1.0.

I'm not entirely sure how can traits be expressed in runtime for them to universally work without inlining.

Or a bunch of functions will have to be codegen'd and statically dispatched on the callsite.

vzarytovskii avatar Mar 14 '24 10:03 vzarytovskii

But...SRTP is statically resolved, hence has to be inlined.

I can't seem to connect the dots here, can the compiler not generate different copies of the function for different TPs? That's how many languages do it (For ex. Rust)

gimbling-away avatar Mar 14 '24 10:03 gimbling-away

But...SRTP is statically resolved, hence has to be inlined.

I can't seem to connect the dots here, can the compiler not generate different copies of the function for different TPs? That's how many languages do it (For ex. Rust)

Replied just before you posted it. This will work for sure. However, I can see some issues with pickled data compatibility (since new compiler will have to suppor both, as well as generate both traits, so old compilers know about it as well).

vzarytovskii avatar Mar 14 '24 10:03 vzarytovskii

But...SRTP is statically resolved, hence has to be inlined.

I can't seem to connect the dots here, can the compiler not generate different copies of the function for different TPs? That's how many languages do it (For ex. Rust)

Are you sure it is worth it? And are you sure it’s difficulty is M?

vshapenko avatar Mar 14 '24 10:03 vshapenko

Are you sure it is worth it? And are you sure it’s difficulty is M?

ppprobably not? (with @vzarytovskii's mention of the compiler needing to support two variants of trait data) — I haven't worked with FSC before, so had no idea. Could bump it up to L/XL perhaps?

gimbling-away avatar Mar 14 '24 10:03 gimbling-away

@vshapenko the idea is interesting and worth existing here.

One aspect is code size, and enabling features of SRTP, without forcing inline.

Other compilers (C++ and Rust, I'd gather) handle this, so there must be good reasons.

Is it high importance for F#, today? not for me.

smoothdeveloper avatar Mar 14 '24 10:03 smoothdeveloper

Could bump it up to L/XL perhaps?

I think it's fine to leave it

vzarytovskii avatar Mar 14 '24 10:03 vzarytovskii

I think the actual problem here is in the "S" of SRTP. The use of inline is just there to statically resolve the parameters during compile time (static).

To make that dynamic, are you suggestion to let SRTP work like dynamic method calls, as in C#? Something like Foo?doSomething(), where doSomething is in this case in the SRTP signature?

Because, you know, one of the most powerful reasons that SRTP works the way it does is that during compile time it can guarantee that the method is there, and furthermore, it will embed that method.

Or am I misreading this and do you still want static resolve (during compile time), but not embedded on the call site, instead just like a function call with parameters? (your mention of "smaller binaries", which may not be a given, btw, suggests this).

Example:

let foo () =
    let inline f a b = a + b // SRTP
    let inline g a b = a + b // SRTP
    let x = f 10 20
    let y = g 60.0 70.0
    x * int y

Currently, this looks like this after compilation (note that you also get const folding):

.method public static
	int32 foo () cil managed
{
	.maxstack 4
	.locals init (
		[0] float64 y
	)

    IL_0000: nop
    IL_0001: ldc.r8 60
    IL_000a: ldc.r8 70
    IL_0013: add      ; inlined g 60 70
    IL_0014: stloc.0
    IL_0015: ldc.i4.s 30 ; inlined and const-folded f 10 20
    IL_0017: ldloc.0
    IL_0018: conv.i4 ; cast
    IL_0019: mul ; multiply
    IL_001a: ret
}

Removing inline to mimic the behavior of your suggestion (but keeping the SRTP semantics of f)

.method public static 
	int32 foo () cil managed 
{
	.maxstack 4
	.locals init (
		[0] int32 x,
		[1] float64 y
	)

    IL_0000: ldc.i4.s 10
    IL_0002: ldc.i4.s 20
    IL_0004: call int32 Tests::f(int32, int32) ; f 10 20 (no inlining)
    IL_0009: stloc.0
    IL_000a: ldc.r8 60
    IL_0013: ldc.r8 70
    IL_001c: call float64 Tests::g(float64, float64) ; g 60.0 70.0 (no inlining)
    IL_0021: stloc.1
    IL_0022: ldloc.0
    IL_0023: ldloc.1
    IL_0024: conv.i4 ; cast
    IL_0025: mul ; multiply
    IL_0026: ret
}

To get the second output, I had to compile it in debug mode, as the F# optimizations will inline it anyway, which begs the question how much you would really gain here: it may result in larger IL code, larger overhead and/or slightly longer JIT compile times, and possibly slower execution, OR, in certain cases, the result may be exactly the same due to existing optimizations.


There's one more thing to consider. You may have noticed that I wrote two functions f and g that do the same thing. That was on purpose. With inline you get auto-generalization and you can use a single function with these arguments. Without it, you lose that ability, and it will bind to the first type used, hence the two functions.

Which suggests to me that we may keep the keyword inline for keeping the SRTP semantics the same, but add perhaps an attribute [<DoNotInline>] or NoInlining(true) to the function, but that'll look rather silly and confusing...

abelbraaksma avatar May 05 '24 22:05 abelbraaksma