积极的 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
你可以看到初始化代码仍然运行。
前几天我在 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
你可以看到初始化代码仍然运行。