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

Allow InlineIfLambda to be used on arbitrary parameters

Open En3Tho opened this issue 4 years ago • 10 comments

While looking at this (https://gist.github.com/mrange/fbefd946dba6725a0b727b7d3fd81d6f) and my own delegate-allowing pipe experiment I've stumbled upon a limitation: InlineIfLambda doesn't support non-function parameters. E.g. |> can't be generic enough to inline lambdas too (if parameter appears to be lambda).

I propose we remove this limitation but add a warning instead (InlineIfLambda attribute used on non-lambda parameter (e.g. 'a) that could be suppressed.

so this:

let inline (|>) ([<InlineIfLambda>] arg) ([<InlineIfLambda>] func) = func arg

should be allowed even if arg is arbitrary 'a and in case if arg is a lambda, it should be inlined.

Please tick this 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] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [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.

Please tick all that apply:

  • [x] This is not a breaking change to the F# language design
  • [x] 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.

En3Tho avatar Dec 16 '21 09:12 En3Tho

@dsyme can this be allowed?

En3Tho avatar Dec 17 '21 11:12 En3Tho

I don't understand why we would want this. If the argument has variable type, then it can't be applied as a function within its scope. So it can't be inlined.

Arbitrary expressions can't be inlined because of side-effect order.

dsyme avatar Dec 17 '21 12:12 dsyme

For example (|>) or (>>) operators could benefit from this because there is no guarantee that next value will be function. Could be, could be not.

I would also like it for my custom Pipe and Combine operators that enable Structural based Invoke pattern too: https://github.com/En3Tho/FSharpExtensions/blob/main/FSharpExtensions/En3Tho.FSharp.Extensions/Experimental.fs

Now they match core operators in the way they behave and inline (means no lambda inlining) but I would really like to have this option.

En3Tho avatar Dec 17 '21 16:12 En3Tho

The idea is to leave behavior as it is, but just to allow this attribute on any parameter and if compiler can detect that there is a function type/lambda actually, then it can be inlined

It's not about inlining generic expressions or whatever, it's more about widening attribute usage so it could be passed down further or work both with 'a as actual 'a (not a lambda so not inlinable) and 'a as 'a -> 'b (now it's a lambda so it's actually inlinable).

En3Tho avatar Dec 19 '21 10:12 En3Tho

@dsyme I don't know if you've read my last 2 messages but can you reconsider this? Or should I close this suggestion?

En3Tho avatar May 20 '22 05:05 En3Tho

Please leave it open, as I'd like to go back and look at your performance results in your original issue and understand why lifting the restriction works for you.

InlineIfLambda has limitations as it can't apply to local "let" values, and I guess it also has restrictions when using it with generic functions.

dsyme avatar May 20 '22 07:05 dsyme

(One way forward for you could be to mod the compiler and double check lifting the restriction is sufficient for you,, if you haven't done that already)

dsyme avatar May 20 '22 07:05 dsyme

@dsyme Thank you for answer. Benchmarks are in that gist, I doubt numbers could have changed since. I will try to mod compiler and see if it that helps. It's just that I don't have too much time lately :(

En3Tho avatar May 20 '22 12:05 En3Tho

I don't know if I did everything right. But I changed line in copiler then built it then used msbuild path-to-benchmarks-project.

Pipe with just inlineIfLambda with on arbutrary 'a parameter works exactly the same as custom one from PushStream where they are specifically typed. This is neat.

I couldn't get my custom operator to work as fast as inlined pipe to matter what I did. Either I don't know some tricks or inlining check just happens before the actual type is really known and placed so It assumes it's not a lambda. Although everything builds and overloads are picked up properly.

So the result's I've got are below:


BenchmarkDotNet=v0.12.1, OS=Windows 10.0.22000
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
.NET Core SDK=6.0.300-preview.22154.4
  [Host]     : .NET Core 6.0.5 (CoreCLR 6.0.522.21309, CoreFX 6.0.522.21309), X64 RyuJIT DEBUG
  DefaultJob : .NET Core 6.0.5 (CoreCLR 6.0.522.21309, CoreFX 6.0.522.21309), X64 RyuJIT


Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated Code Size
RunOriginal 28.833 μs 0.1048 μs 0.0981 μs - - - 168 B 163 B
RunInlinedPipe 3.290 μs 0.0094 μs 0.0088 μs - - - - 38 B
RunPipeFromPushStream 3.286 μs 0.0111 μs 0.0104 μs - - - - 38 B
RunCustomPipe 28.853 μs 0.0987 μs 0.0923 μs - - - 168 B 163 B

Benchmark code is taken from link in inital message. Here is how pipe operators are defined:

module InlinedPipe =
    let inline (|>) ([<InlineIfLambda>] x) ([<InlineIfLambda>] f) = f x

module PipeFromPushStream =
    let inline (|>) ([<InlineIfLambda>] v : _ -> _) ([<InlineIfLambda>] f : _ -> _) = f v

module CustomOperator =

    [<AutoOpen>]
    module Pipe1 =

        type T = T with
            static member inline ($) (_, [<InlineIfLambda>] invokable: 'a -> 'b) = fun ([<InlineIfLambda>] value) -> invokable value
            static member inline ($) (_, [<InlineIfLambda>] invokable) = fun ([<InlineIfLambda>] value) -> (^a: (member Invoke: ^b -> ^c) invokable, value)

        let inline (|>) ([<InlineIfLambda>] value) ([<InlineIfLambda>] invokable) =
            (T $ invokable) (value)

I think this kind of stuff might be even helpful in FSC/FCS as there are lots of piping going on. I guess some of the stuff can be inlined for better compiler speed. Also, it can open up usage of such PushStreams or other inline-based mechanism to make allocation-free (or less) collection operations

En3Tho avatar May 23 '22 07:05 En3Tho

@dsyme Can you please give some tips on how to make this custom operator pipe inlineable?

I just like the fact that you can it with actions and funcs and so on. It's not like it's used with those that often but still it's quite cool :)

Also, >>/<< operators work with them too, it's funny.

En3Tho avatar May 23 '22 20:05 En3Tho