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

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

Open
MangelMaxime opened this issue Jun 1, 2022 · 4 comments
Labels
Milestone

Comments

@MangelMaxime
Copy link

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

dsyme commented Jun 1, 2022

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

dsyme commented Jun 1, 2022

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

dsyme commented Jun 1, 2022

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

@MangelMaxime
Copy link
Author

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.

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

No branches or pull requests

4 participants