为什么当主函数移动到循环内时,这段代码会加速?

问题描述 投票:0回答:1

我有以下 Swift 代码,它接受迭代计数和另一个参数,并在循环中执行一些计算。

@inline(never)
func rawComputeAll(iterCount: Int, timeStep: Double) -> Double {
    // most things up here are constants, scroll down to loop
    let mediumDensity = 995.59
    let defaultCapacity = 0.04017672438703612,
        thermalResistance = 0.0042879141883543715
    let specificHeat = 4184.0
    let inputTemperature = 300.0,
        inputEntropyFlow = 1000.0
    var storedSpecificEnthalpy = 1.0e5,
        storedFlow = 0.5,
        storedMass = defaultCapacity * mediumDensity,
        storedSpecificHeat = specificHeat
    var outputTemperature = 300.0,
        outputSpecificEnthalpy = 1.0e5
    // ACTUAL WORK
    for _ in 0..<iterCount {
        outputTemperature = inputTemperature - thermalResistance * inputEntropyFlow
        let storedTemperature = 273.15 + storedSpecificEnthalpy / storedSpecificHeat
        let deltaT = outputTemperature - storedTemperature
        let sign = deltaT != 0.0 ? deltaT / abs(deltaT) : deltaT
        let deltaS = sign * inputEntropyFlow * timeStep
        let deltaU = deltaS * 1.0
        let inputMass = storedFlow * timeStep
        let inputEnergy = inputMass * outputSpecificEnthalpy
        let totalMass = storedMass + inputMass
        let stateEnergy = storedMass * outputSpecificEnthalpy + inputEnergy + deltaU
        storedSpecificEnthalpy = stateEnergy / totalMass
    }
    return storedSpecificEnthalpy
}

我用两种方式对其进行基准测试。

  1. 像这样直接用一些大的
    iterCount
    对函数进行计时。
let startTime1 = DispatchTime.now()
let result1 = rawComputeAll(iterCount: iterCount, timeStep: timeStep)
let endTime1 = DispatchTime.now()

(我实际上使用了一个正确的基准测试框架来报告运行时的分布,但为了简洁和易于重现而在此处使用这个简单的计时代码)。

  1. 将循环移到外部并将迭代计数作为 1 传递给函数。我还对
    timeStep
    进行了一些计算,以便 swift 编译器不会优化循环。
var result2 = Double.random(in: 0.1...10.0)
let startTime2 = DispatchTime.now()
for _ in 1...iterCount {
    result2 = rawComputeAll(iterCount: 1, timeStep: timeStep + scaling*result2)
}
let endTime2 = DispatchTime.now()

现在,我预计第二种方法会更慢,因为循环中函数调用的开销(注意用于确保这一点的

@inline(never)
),而且还因为它在循环体中执行了一些额外的 FLOP。

但是,事实证明,这种在外部时钟上进行循环的方法比直接的基准测试要快得多——在我的机器上,它们之间存在 30% 的差异。这里有一个 Godbolt 链接 展示了这个问题。

那么,我有两个问题。

  1. 为什么第二个基准测试明显更快,尽管原则上它应该做更多的工作?
  2. 到目前为止我只有猜测和分析并没有透露任何有趣的东西。我如何诊断这种性能病理并发现根本原因?

我对发布版本进行了所有测量(分析和基准测试)。

swift performance profiling benchmarking
1个回答
0
投票

性能差异很可能是由于编译器执行了循环不变代码运动 (LICM) 优化。我通过检查反汇编发现了这一点(用于反汇编的编译器资源管理器链接)。在这种特殊情况下,进行以下计算

let inputMass = storedFlow * timeStep
let inputEnergy = inputMass * outputSpecificEnthalpy
let totalMass = storedMass + inputMass
let stateEnergy = storedMass * outputSpecificEnthalpy + inputEnergy + deltaU

被提升到循环之外。这可以在程序集中看到,感兴趣的指令用

###
标记(在编译器资源管理器链接中的颜色编码输出中可以更好地看到)。

output.rawComputeAll(iterCount: Swift.Int, timeStep: Swift.Double) -> Swift.Double:
        test    rdi, rdi
        js      .LBB1_6
        je      .LBB1_2
        movsd   xmm2, qword ptr [rip + .LCPI1_1]
        mulsd   xmm2, xmm0                         ###
        movsd   xmm11, qword ptr [rip + .LCPI1_0]
        movapd  xmm3, xmm2
        mulsd   xmm3, xmm11.                       ###
        addsd   xmm2, qword ptr [rip + .LCPI1_2]   ###
        addsd   xmm3, qword ptr [rip + .LCPI1_3]   ###
        movsd   xmm4, qword ptr [rip + .LCPI1_4]
        movsd   xmm5, qword ptr [rip + .LCPI1_5]
        movsd   xmm6, qword ptr [rip + .LCPI1_6]
        movapd  xmm7, xmmword ptr [rip + .LCPI1_7]
        xorpd   xmm8, xmm8
        movsd   xmm9, qword ptr [rip + .LCPI1_8]
        movsd   xmm10, qword ptr [rip + .LCPI1_9]
.LBB1_4:
        divsd   xmm11, xmm4
        addsd   xmm11, xmm5
        movapd  xmm1, xmm6
        subsd   xmm1, xmm11
        movapd  xmm11, xmm1
        andpd   xmm11, xmm7
        movapd  xmm12, xmm1
        cmpeqsd xmm12, xmm8
        movapd  xmm13, xmm12
        ...

特别地,分支

.LBB1_4
是循环的分支目标,因此该分支目标之前的浮点运算指令不会在循环中执行。

通过将基准循环放置在函数之外,可以有效地消除 LCM,并且循环的每次迭代都会执行不应该在循环中执行的内容。

© www.soinside.com 2019 - 2024. All rights reserved.