fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

Difference of compilation time between 13 000 "static member" and 13 000 "let binding"

Open MangelMaxime opened this issue 2 years ago • 4 comments

Hello,

while working on a binding for Fable I encounter a case with a huge number elements to map, around 13 000.

Depending on how I represents those elements the compilation time (when running dotnet build) can be really different:

  • ~20sec for static member
  • ~2min30 for let

I measured the timing in two ways:

  • Force clean

    1. Delete bin and obj folders
    2. Run dotnet build
  • Force save

    1. Add a space to the Types.fs file
    2. Save the file
    3. Run dotnet build

    If I don't update the Types.fs file, then the compilation is almost instant probably because of/thanks to cache mechanism

LetBindings

namespace Glutinum.IconifyIcons.Mdi

open Fable.Core

module mdi =

    [<Import("default", "@iconify-icons/mdi/123-off")>]
    let _123Off : string = jsNative

    [<Import("default", "@iconify-icons/mdi/123")>]
    let _123 : string = jsNative

    [<Import("default", "@iconify-icons/mdi/1password")>]
    let _1password : string = jsNative

    // 13 000 more let bindings...

Force save

  • 00:03:28.23
  • 00:02:35.74
  • 00:02:39.66
  • 00:02:31.04
  • 00:02:28.58

Force clean

  • 00:02:59.43
  • 00:01:50.69

Static members

namespace Glutinum.strings.Mdi

open Fable.Core

[<Erase>]
type mdi =

    [<Import("default", "@iconify-icons/mdi/123-off")>]
    static member inline _123Off : string =
        jsNative

    [<Import("default", "@iconify-icons/mdi/123")>]
    static member inline _123 : string =
        jsNative

    [<Import("default", "@iconify-icons/mdi/1password")>]
    static member inline _1password : string =
        jsNative

    // 13 000 more static members...

Force clean

  • 00:00:17.95
  • 00:00:17.35

Force save

  • 00:01:09.75
  • 00:01:13.68
  • 00:00:18.41
  • 00:00:26.16
  • 00:00:28.37

I don't know why the first 2 times it was that slow, perhaps I had something running on my computer taking too much ressources. I left then in the timing because I didn't want to hide this situation

Repro steps

Here is a zip file containing two projects using both of the representation with 13 000 elements in them.

reproduction-fsharp-compilation-diff-method-let.zip

Expected behavior

Compilation time between static member or let binding should not be that different? As fast as possible compilation time for both representation.

Related information

Provide any related information (optional):

  • Operating system: Windows 11
  • .NET Runtime kind (.NET Core, .NET Framework, Mono): .NET Core 6.0.203
  • Editing Tools (e.g. Visual Studio Version, Visual Studio): VSCode Ionide v6.0.5

MangelMaxime avatar Jun 01 '22 08:06 MangelMaxime

I took a look. Methodology:

  1. Collect arguments via

    cd reproduction-fsharp-compilation-diff-method-let\reproduction-fsharp-compilation-diff-method-let\LetBindings
    dotnet build -v n > args.txt
    

    then hand edit args.txt to contain only the arguments to the compilation

  2. Use PerfView to collect data (took about 4 minutes, should have reduced the input size)

    \bin\PerfView.exe  C:\GitHub\dsyme\fsharp\artifacts\bin\fsc\Release\net6.0\fsc.exe @args.txt
    
  3. Look at data in PerfView

    \bin\PerfView.exe PerfViewData.etl.zip   
    

There's a complaint about 64-bit stacks from PerfView

Highly suspicious on inclusive list:

fsharp.compiler.service!FSharp.Compiler.AbstractIL.ILBinaryWriter+Codebuf+findRoots@2170[System.__Canon].Invoke(class Microsoft.FSharp.Collections.FSharpList`1&gt;&gt;,!0)</TD></TR></TABLE>

Matching entry on exclusive list:

fsharp.compiler.service!FSharp.Compiler.AbstractIL.ILBinaryWriter+Codebuf+tryspec_inside_tryspec@2196.Invoke(class ILExceptionSpec,class ILExceptionSpec)</TD></TR></TABLE>

To me this indicates an algorithmic problem where the let bindings are generating long module initialization code that is later being processed in a quadratic or sub-optimal way.

dsyme avatar Jun 01 '22 09:06 dsyme

So note that the two inputs are fairly different - the member code has no file-level initialization, while the let code does. The second seems to hit a choke point fairly late in findRoots.

dsyme avatar Jun 01 '22 09:06 dsyme

Note also a lot of exception handlers are being emitted in the initialization code, which may not be entirely obvious, because of this:

    let inline jsNative<'T> : 'T =
        // try/catch is just for padding so it doesn't get optimized
        try failwith "You've hit dummy code used for Fable bindings. This probably means you're compiling Fable code to .NET by mistake, please check."
        with ex -> raise ex

This doesn't mean the compiler perf isn't fixable, but does help to explain why the two aren't easily comparable.

(This is also almost certainly not correct in any case - the initialization code will fail if ever triggered, which is unlikely to be as desired - though maybe Fable compilation ensures it never is.)

dsyme avatar Jun 01 '22 09:06 dsyme

Note also a lot of exception handlers are being emitted in the initialization code, which may not be entirely obvious, because of this:

Indeed, I didn't thought about the fact that jsNative is inlined and could have impact depending on where it is applied to.

(This is also almost certainly not correct in any case - the initialization code will fail if ever triggered, which is unlikely to be as desired - though maybe Fable compilation ensures it never is.)

In Fable, using jsNative allows us to provide code without real implementation. Which is really useful for bindings.

For example, this code:

[<Erase>]
type mdi =

    [<Import("default", "@iconify-icons/mdi/123-off")>]
    static member inline _123Off : string =
        jsNative

let myIcon = mdi._123Off

produce

import $003123_off from "@iconify-icons/mdi/123-off";

export const myIcon = $003123_off;

which is what is expected from JavaScript of view.

Using the exception in jsNative allows us to explain to the user what is happening if he didn't generate the binding correctly or use a Fable code in a .NET project.

MangelMaxime avatar Jun 01 '22 12:06 MangelMaxime