Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do aggressive F# compiler optimizations only occur on referenced dependencies + release configuration?

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:

  • Executing project (the console one in my example above) does initialize the variable regardless of the configuration.
  • Dependencies (the library project in my example) do initialize the variable only with a debug configuration.

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:

  • https://stackoverflow.com/a/6630262/4636721
  • https://stackoverflow.com/a/6630264/4636721

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

like image 818
Natalie Perret Avatar asked Sep 14 '25 11:09

Natalie Perret


1 Answers

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.

like image 199
Asti Avatar answered Sep 17 '25 06:09

Asti