我有以下 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
}
我用两种方式对其进行基准测试。
iterCount
对函数进行计时。let startTime1 = DispatchTime.now()
let result1 = rawComputeAll(iterCount: iterCount, timeStep: timeStep)
let endTime1 = DispatchTime.now()
(我实际上使用了一个正确的基准测试框架来报告运行时的分布,但为了简洁和易于重现而在此处使用这个简单的计时代码)。
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 链接 展示了这个问题。
那么,我有两个问题。
我对发布版本进行了所有测量(分析和基准测试)。
性能差异很可能是由于编译器执行了循环不变代码运动 (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,并且循环的每次迭代都会执行不应该在循环中执行的内容。