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

标签 .net-core f# compiler-optimization

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

好的,问题出在这些方面(我故意简化了代码,并且仍然可以重现行为),我创建了一个项目,它是一个带有单个文件的控制台,如下所示:
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 namespace 实际上是一个引用到控制台项目的库项目。

该库项目也由单个文件组成:
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 建议我的帖子与以下内容重复:Module values in F# don't get initialized. Why?

    所以我检查了那个帖子中给出的答案:
  • 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
    


    所以我确实在库项目中添加了一个入口点
    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。一个模块的所有初始化代码 is bundled into a separate classStartupCode$命名空间。

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

    积极优化

    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; } }
     }
    

    因此,如果没有引用启动类上的该字段的属性访问,则模块中没有使用启动代码类的任何部分,因此静态构造函数将不会运行。
    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
    

    您可以看到 init 代码仍然会运行。

    关于.net-core - 积极的 F# 编译器优化是否只发生在引用的依赖项 + 发布配置上?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61937274/

    相关文章:

    performance - 通过提前计算条件来避免停顿管道

    c - 是否允许编译器优化浮点常量乘法

    c# - log4net 2.0.8 nuget 中缺少 SmtpAppender?

    c# - 处置 DbContext 创建的实体对象

    c# - net462 应用程序引用 netstandard1.5 库时出现 "Could not load file or assembly"错误。但为什么?

    F# 类型提供程序在引用项目中查找示例文件,而不是引用项目

    f# - 是否可以在运行时获取联合体的标签?

    f# - 将 seq 尾部递归复制到 F# 中的列表

    c - __attribute__((pure)) 应用于 void 函数

    reactjs - .NET webapi 端点在 Postman 中工作,但 Chrome 给出了 net::ERR_FAILED