You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

F#生成相同汇编的代码性能差距较大,是什么原因?

问题背景

我正尝试理解为何两段生成完全相同汇编的F#代码会出现如此大的性能差异。我定义了一个结构体用于包装数组,以强制使用度量单位(Units of Measure)作为索引,度量单位会在编译期被擦除,理论上不会带来性能开销,结构体代码如下:

[<Struct>]
type StructWrapper<[<Measure>] 'Measure, 'Value> =
    val Values : array<'Value>

    new (values: array<'Value>) =
        {
            Values = values
        }

    member this.Item
        with get (idx: int<'Measure>) =
            this.Values.[int idx]

    member this.Length = 
        LanguagePrimitives.Int32WithMeasure<'Measure> this.Values.Length

随后我编写了两个测试函数,使用Fasmi工具查看生成的汇编代码:

[<Measure>] type ItemIdx

let test1 (x: array<float>) =
    let mutable acc = 0.0
    let mutable idx = 0
    let len = x.Length

    while idx < len do
        acc <- acc + x[idx]
        idx <- idx + 1

    acc

let test2 (x: StructWrapper<ItemIdx, float>) =
    let mutable acc = 0.0
    let mutable idx = 0<ItemIdx>
    let len = x.Length

    while idx < len do
        acc <- acc + x[idx]
        idx <- idx + 1<ItemIdx>

    acc

查看结果显示test1和test2生成的汇编完全一致,内容如下:

;Arr.test1(Double[])
L0000: vzeroupper
L0003: vxorps xmm0, xmm0, xmm0
L0007: xor eax, eax
L0009: mov edx, [rcx+8]
L000c: cmp eax, edx
L000e: jge short L001e
L0010: movsxd r8, eax
L0013: vaddsd xmm0, xmm0, [rcx+r8*8+0x10]
L001a: inc eax
L001c: jmp short L000c
L001e: ret

;Arr.test2(StructWrapper`1<Double>)
L0000: vzeroupper
L0003: vxorps xmm0, xmm0, xmm0
L0007: xor eax, eax
L0009: mov edx, [rcx+8]
L000c: cmp eax, edx
L000e: jge short L001e
L0010: movsxd r8, eax
L0013: vaddsd xmm0, xmm0, [rcx+r8*8+0x10]
L001a: inc eax
L001c: jmp short L000c
L001e: ret

但我使用BenchmarkDotNet做基准测试时却得到了差异极大的结果,我原本预期二者耗时接近,测试代码如下:

[<Measure>] type ItemIdx

let iterationCount = 1_000
let numberCount = 1_000_000

let rawArray = [|1.0 .. (float numberCount)|]

let structApproach =
    [|1.0 .. (float numberCount)|] 
    |> StructWrapper<ItemIdx, _>

type Benchmark () =

    [<Benchmark>]
    member _.RawArray () =
        let mutable result = 0.0
        let mutable idx = 0
        let len = rawArray.Length
        while idx < len do
            result <- result + rawArray.[idx]
            idx <- idx + 1
            
        result

    [<Benchmark>]
    member _.StructWrapper () =
        let mutable result = 0.0
        let mutable idx = 0<ItemIdx>
        let len = structApproach.Length
        while idx < len do
            result <- result + structApproach.[idx]
            idx <- idx + 1<ItemIdx>
            
        result

基准测试的环境与结果如下:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT DEBUG
DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT

MethodMeanErrorStdDev
RawArray688.8 us1.77 us1.48 us
StructWrapper2,280.1 us24.34 us20.32 us

原因解答

核心差异在于你对比汇编的测试函数和BenchmarkDotNet测试方法的访问上下文不一致:

  1. 你对比汇编的test1和test2都是把操作对象作为参数传入的,这种场景下JIT可以直接把存在寄存器(x64下为rcx)的参数值的内部字段直接用于循环,不需要额外寻址,所以汇编完全一致。
  2. 但Benchmark测试中的structApproach是全局静态值类型变量,RawArray测试访问的rawArray是全局静态引用类型变量:
    • 静态引用类型的数组地址会被JIT在循环前加载到寄存器,循环全程不需要重新访问静态存储区
    • 静态值类型的StructWrapper访问其成员时,JIT不会把它内部的Values数组字段提升到循环外,每次索引访问都需要先从静态存储区读取结构体的Values字段,再做数组寻址,额外增加了大量内存访问开销。

你可以通过修改StructWrapper测试方法验证这个结论:在方法开头先把全局的structApproach赋值给本地变量,再基于本地变量做循环,二者的性能就会基本一致。


内容的提问来源于stack exchange,提问作者Matthew Crews

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.26 09:36:03