I faced something a little bit unexpected the other day in F# (+ .NET Core 3.1) about a let
binding initialization (variable) which was not always occurring depending on which configuration the program compiler: debug or release.
Ok the problem went something along those lines (I purposefully simplified the code, and the behaviour can still be reproduced), I created a project that was a console one with a single file, below:
Program.fs
:
open System
open ClassLibrary1
open Flurl.Http
[<RequireQualifiedAccess>]
module Console =
let private init =
printfn "Console: A"
FlurlHttp.Configure(fun settings ->
printfn "Console: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Console: C"
[<EntryPoint>]
let main _ =
Console.doStuff()
Library.doStuff()
0
The ClassLibrary1
namespace is actually a library project referenced to the console project.
That library project is also made of a single file:
Library.fs
:
namespace ClassLibrary1
open System
open Flurl.Http
[<RequireQualifiedAccess>]
module Library =
let private init =
printfn "Library: A"
FlurlHttp.Configure(fun settings ->
printfn "Library: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Library: C"
The difference when running the console project
Release output:
Console: A
Console: B
Console: C
Library: C
Debug output:
Console: A
Console: B
Console: C
Library: A
Library: B
Library: C
That was a bit disturbing, my colleague and I spent a fair amount of time trying to figure out what was going on.
So I would just like to confirm the compiler optimization rules in this context.
My understanding atm is:
I would like to know if my understanding is correct or no.
[EDIT]
Bent Tranberg suggested my post to be a duplicated of: Module values in F# don't get initialized. Why?
So I checked the answers given in that post:
Brian pointed me to this part of the spec, which indicates that this is the expected behavior.
It looks like one workaround would be to provide an explicit entry point, like this:
[<EntryPoint>] let main _ = 0
So I did add an entry point in the library project
Library.fs
module ClassLibrary1
open System
open Flurl.Http
[<RequireQualifiedAccess>]
module Library =
let private init =
printfn "Library: A"
FlurlHttp.Configure(fun settings ->
printfn "Library: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Library: C"
[<EntryPoint>]
let callMe _ =
Library.doStuff ()
0
and changed the executable program as follows:
open System
open ClassLibrary1
open Flurl.Http
[<RequireQualifiedAccess>]
module Console =
let private init =
printfn "Console: A"
FlurlHttp.Configure(fun settings ->
printfn "Console: B"
settings.AfterCall <- Unchecked.defaultof<Action<FlurlCall>>)
let doStuff () =
init
printfn "Console: C"
[<EntryPoint>]
let main _ =
Console.doStuff()
callMe [||] |> ignore
0
And same thing happened, just as before.
I even changed the Library project type to an executable project and nothing has changed either...
This one took some digging. This is the result of two different issues:
Startup code
This is because of how fsc
chooses to generate the IL for modules. All the initialization code for a module is bundled into a separate class in the StartupCode$
namespace.
So the static constructor for the module actually exists in a another class named <StartupCode$Assembly>.$ClassLibrary1
. Perhaps you can begin to see the problem with this - if this class is never referenced, the static constructor will never run.
Aggressive optimizations
In Release
mode, F# will aggressively inline short methods, literals, and will ignore property accesses whose value is thrown away.
module Library =
let private init =
printfn "In init"
0
let doStuff () =
init |> ignore //<-- will be thrown away
printfn "%s" "doStuff"
To be more clear, this is what init
looks like:
static class ClassLibrary1 {
static Unit init { get { return <StartupCode$Assembly>.$ClassLibrary1.init; } }
}
So without that property access referencing that field on the startup class, there's no part of the startup code class being used in the module, hence the static constructor won't run.
module Library =
let private init =
printfn "In init"
0
let doStuff () =
init |> printfn "%d" // init is accessed
printfn "%s" "doStuff"
The above code works because init
can't be thrown away.
Finally, to demonstrate that any field or property access will do, we write up an example which ensures a property access - the mutable will prevent any optimizations.
module Library =
let mutable str = "Anything will do"
let private init =
printfn "In init"
let doStuff () =
printfn "%s" str
You can see that the init code will still run.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With