fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
345 stars 21 forks source link

Non-inline SRTP usage #1353

Open gimbling-away opened 7 months ago

gimbling-away commented 7 months ago

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

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:

Please tick all that apply:

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.

vzarytovskii commented 7 months ago

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

vzarytovskii commented 7 months ago

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.

gimbling-away commented 7 months ago

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)

vzarytovskii commented 7 months ago

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).

vshapenko commented 7 months ago

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?

gimbling-away commented 7 months ago

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?

smoothdeveloper commented 7 months ago

@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.

vzarytovskii commented 7 months ago

Could bump it up to L/XL perhaps?

I think it's fine to leave it

abelbraaksma commented 6 months ago

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...