积极的 F# 编译器优化是否仅发生在引用的依赖项 + 发布配置上?

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

前几天我在 F# (+ .NET Core 3.1) 中遇到了一些关于 let 绑定初始化(变量)的意外情况,这并不总是发生,具体取决于程序编译器的配置:调试或释放。

好的,问题是沿着这些方向发展的(我有意简化了代码,并且行为仍然可以重现),我创建了一个控制台项目,只有一个文件,如下:

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

ClassLibrary1命名空间实际上是一个引用到控制台项目的库项目。

该库项目也由单个文件组成:

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"

控制台项目运行时的区别

释放输出:

Console: A
Console: B
Console: C
Library: C

调试输出:

Console: A
Console: B
Console: C
Library: A
Library: B
Library: C

这有点令人不安,我和我的同事花了相当多的时间试图弄清楚发生了什么。

所以我想在此上下文中确认编译器优化规则。

我对atm的理解是:

我想知道我的理解对不对


[编辑]

Bent Tranberg 建议将我的 post 复制为:Module values in F# don't get initialized. Why?

所以我检查了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

所以我确实在库项目中添加了一个入口点

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

并修改可执行程序如下:

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

同样的事情发生了,就像以前一样。

我什至将库项目类型更改为可执行项目,但也没有任何改变...

这需要一些挖掘。这是两个不同问题的结果:

启动码

这是因为 fsc 选择为模块生成 IL 的方式。 StartupCode$ 命名空间中模块 is bundled into a separate class 的所有初始化代码。

所以模块的静态构造函数实际上 exists in a another class 命名为 <StartupCode$Assembly>.$ClassLibrary1。也许你可以开始看到这个问题 - 如果这个 class 从未被引用,静态构造函数将永远不会 运行.

积极优化

Release 模式下,F# 将积极地内联短方法、文字,并将 忽略 属性 其值被丢弃的访问。

module Library =
    let private init =
        printfn "In init"
        0

    let doStuff () =
        init |> ignore //<-- will be thrown away
        printfn "%s" "doStuff"

更清楚地说,这就是 init 的样子:

 static class ClassLibrary1 {    
    static Unit init { get { return <StartupCode$Assembly>.$ClassLibrary1.init; } }
 }

因此,如果没有 属性 访问在启动时引用该字段 class,模块中就不会使用启动代码 class 的任何部分,因此静态构造函数不会' t运行。

module Library =
    let private init =
        printfn "In init"
        0

    let doStuff () =
        init |> printfn "%d" // init is accessed
        printfn "%s" "doStuff"

上面的代码可以工作,因为 init 不能被丢弃。 最后,为了证明任何字段或 属性 访问都可以,我们编写了一个确保 属性 访问的示例 - 可变将阻止任何优化。

module Library =
    let mutable str = "Anything will do"

    let private init =
        printfn "In init"        

    let doStuff () =        
        printfn "%s" str

你可以看到初始化代码仍然运行。