fsharp
fsharp copied to clipboard
Difference of compilation time between 13 000 "static member" and 13 000 "let binding"
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
- Delete
bin
andobj
folders - Run
dotnet build
- Delete
-
Force save
- Add a space to the
Types.fs
file - Save the file
- 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 - Add a space to the
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
I took a look. Methodology:
-
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
-
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
-
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>>,!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.
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
.
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.)
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.