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

Non-inline SRTP usage #1353

Open
5 of 6 tasks
gimbling-away opened this issue Mar 14, 2024 · 9 comments
Open
5 of 6 tasks

Non-inline SRTP usage #1353

gimbling-away opened this issue Mar 14, 2024 · 9 comments

Comments

@gimbling-away
Copy link

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:

  • This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
  • 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
  • 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
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate

Please tick all that apply:

  • 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 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.

@vzarytovskii
Copy link

vzarytovskii commented Mar 14, 2024

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

@vzarytovskii
Copy link

vzarytovskii commented Mar 14, 2024

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
Copy link
Author

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
Copy link

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
Copy link

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
Copy link
Author

gimbling-away commented Mar 14, 2024

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
Copy link
Contributor

@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
Copy link

vzarytovskii commented Mar 14, 2024

Could bump it up to L/XL perhaps?

I think it's fine to leave it

@abelbraaksma
Copy link
Member

abelbraaksma commented May 5, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants