【问题标题】:JIT optimization in scenario combining inline and non-mutable boolean module property结合内联和非可变布尔模块属性的场景中的 JIT 优化
【发布时间】:2011-01-22 19:57:12
【问题描述】:

在下面的程序中,

module Program

let condition = System.DateTime.Now.Millisecond % 2 = 0

let inline reliesOnCondition (x:int) =
    if condition then
        printfn "%i" x

[<EntryPoint>]
let main args =
    reliesOnConditional System.DateTime.Now.Second
    0

如果 condition 在模块加载时结果为 false,JIT 是否会优化掉表达式 reliesOnCondition System.DateTime.Now.Second

【问题讨论】:

    标签: .net optimization f# jit


    【解决方案1】:

    没有*。当 F# 为程序发出 IL 时,对 condition 的所有访问都是通过属性访问器完成的。由于这个额外的间接层,JIT 引擎无法优化掉reliesOnCondition 的整个主体。

    让我展示一下我是如何发现这一点的。 (也在old blog 帖子中列出。)

    创建新的 F# 项目“SOQ”

    构建我们的 F# 应用程序。我只是将条件硬编码为false

    module Program
    
    let condition = false
    
    let inline reliesOnCondition (x:int) =
        if condition then
            printfn "%i" x
    
    [<EntryPoint>]
    let main args =
        printfn "(attach a debugger and press any key)"
        System.Console.ReadKey(true) |> ignore
    
        reliesOnCondition System.DateTime.Now.Second
        0
    

    反汇编它并用 PDB 中的 IL 操作码重新组装它

    接下来使用ildasm 使用/SOURCE 参数反汇编IL 二进制文件。这不仅会为您提供源代码的 IL 转储,还包括作为 cmets 保留的原始源代码。

    ildasm SOQ.exe /OUT=SOQ-annotated.exe.il /SOURCE
    

    从 IL 重新组装我们的二进制文件

    接下来使用ilasm 重新组装IL 二进制文件,但传入/DEBUG 标志以获得PDB。生成的应用程序将具有 两个 级别的代码。首先,原始的F#将作为cmets保留,实际代码将是IL指令。

    ilasm SOQ-annotated.exe.il /DEBUG
    

    运行进程并附加 Visual Studio 调试器

    运行新注释的程序。这将导致应用程序像往常一样进行 JIT-ted。接下来,将 Visual Studio 调试器附加到活动进程。

    单步调试代码

    在 VS 调试器中查看 IL 转储是不够的。右键单击“堆栈跟踪”窗口并检查 Go To Disassembly。这将为您显示实际的 x86 指令。

    这里是 x86 操作码转储。请注意顶部的原始 F# 源代码行 (ildasm /SOURCE)、其下方的 IL 指令 (ilasm /DEBUG) 以及下方的 x86 指令(由 Visual Studio 提供)。

    //000014:     reliesOnCondition System.DateTime.Now.Second
        IL_0026:  call       valuetype [mscorlib]System.DateTime [mscorlib]System.DateTime::get_Now()
    000000db  lea         ecx,[ebp-58h] 
    000000de  call        595E8C00 
        IL_002b:  stloc.3
    000000e3  lea         edi,[ebp-30h] 
    000000e6  lea         esi,[ebp-58h] 
    000000e9  movq        xmm0,mmword ptr [esi] 
    000000ed  movq        mmword ptr [edi],xmm0 
        IL_002c:  ldloca.s   V_3
    000000f1  lea         eax,[ebp-30h] 
    000000f4  mov         dword ptr [ebp-74h],eax 
        IL_002e:  call       instance int32 [mscorlib]System.DateTime::get_Second()
    000000f7  mov         ecx,dword ptr [ebp-74h] 
    000000fa  call        5960A670 
    000000ff  mov         dword ptr [ebp-5Ch],eax 
        IL_0033:  stloc.2
    00000102  mov         eax,dword ptr [ebp-5Ch] 
    00000105  mov         dword ptr [ebp-28h],eax 
        IL_0034:  call       bool Program::get_condition()
    00000108  call        dword ptr ds:[004232D8h] 
    0000010e  mov         dword ptr [ebp-60h],eax 
        IL_0039:  brfalse.s  IL_003d
    00000111  cmp         dword ptr [ebp-60h],0 
    00000115  je          0000011A 
    ... snip ...
    

    如您所见,IL 指令 34 调用 Program::get_condition(),因此 JIT 没有足够的信息来正确消除无操作函数调用。 (请注意,只有函数可以标记为内联,所以你不能再进一步了。)

    *在我的机器上 (x64 Win7)。 x86 和 x64 JIT 引擎以及您是否使用 NGEN 生成可执行文件之间存在差异。您的里程可能会有所不同。

    【讨论】:

    • 感谢@Chris,这是一个非常强大的调试技术。
    • 非常感谢您分享这项有价值的技术,但我给 @Hans 打了勾,因为他提供了 JIT 不考虑死代码优化的运行时值的关键信息。
    【解决方案2】:

    赞成的答案不正确,属性访问器间接通常不会阻止 JIT 优化器忽略死代码。大多数简单的都被内联,它们的编译时间值被考虑在内。这意味着它不会优化表达式,因为 condition 的值仅在运行时才知道。在 condition 值已知之后代码被 jitted 的事实并没有改变这一点,优化器必须能够静态地确定值。这里只有文字就足够了,你会得到一个带有条件编译的文字(C#中的#if)。有关优化器的更多背景信息,请查看this answer

    【讨论】:

    • 如果 condition 是 C# 风格的静态只读字段,JIT 是否能够在省略死代码时考虑它的运行时值?另外,您知道为什么@Chris 的示例中的属性调用没有被省略,因为他确实使用了编译时文字?
    • 这就是我提到的“必须能够静态确定值”的条款。我怀疑 Chris 要么没有反汇编 Release 版本,要么忘记关闭“Suppress JIT optimization on module load”调试选项。默认情况下它是打开的,以使调试发布构建代码变得容易。
    • 好的,谢谢 - 我想也许 JIT 可以做一些运行时分析(那个子句是特定于属性优化的)。
    猜你喜欢
    • 2018-06-04
    • 1970-01-01
    • 1970-01-01
    • 2013-02-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多