循环和范围运算符的基准
Benchmark for loop and range operator
我了解到基于范围的循环在某些编程语言上具有更好的性能。 Swift中是否如此。例如在 Playgroud 中:
func timeDebug(desc: String, function: ()->() )
{
let start : UInt64 = mach_absolute_time()
function()
let duration : UInt64 = mach_absolute_time() - start
var info : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
mach_timebase_info(&info)
let total = (duration * UInt64(info.numer) / UInt64(info.denom)) / 1_000
println("\(desc): \(total) µs.")
}
func loopOne(){
for i in 0..<4000 {
println(i);
}
}
func loopTwo(){
for var i = 0; i < 4000; i++ {
println(i);
}
}
基于范围的循环
timeDebug("Loop One time"){
loopOne(); // Loop One time: 2075159 µs.
}
正常for循环
timeDebug("Loop Two time"){
loopTwo(); // Loop Two time: 1905956 µs.
}
如何在 swift 中正确进行基准测试?
// 在设备上更新
第一个运行
循环两次:54 µs。
循环一次:482 µs.
第二
循环两次:44 µs。
循环一次:382 µs.
第三
循环两次:43 µs。
循环一次:419 µs.
第四
循环两次:44 µs。
循环一次:399 µs.
// 更新 2
func printTimeElapsedWhenRunningCode(title:String, operation:()->()) {
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
println("Time elapsed for \(title): \(timeElapsed) s")
}
printTimeElapsedWhenRunningCode("Loop Two time") {
loopTwo(); // Time elapsed for Loop Two time: 4.10079956054688e-05 s
}
printTimeElapsedWhenRunningCode("Loop One time") {
loopOne(); // Time elapsed for Loop One time: 0.000500023365020752 s.
}
你真的不应该在 playgrounds 中进行基准测试,因为它们没有经过优化。除非您对调试时需要多长时间感兴趣,否则您应该只对基准优化构建 (swiftc -O
)。
要了解为什么基于范围的循环可以更快,您可以查看为两个选项生成的程序集:
基于范围
% echo "for i in 0..<4_000 { println(i) }" | swiftc -O -emit-assembly -
; snip opening boiler plate...
LBB0_1:
movq %rbx, -32(%rbp)
; increment i
incq %rbx
movq %r14, %rdi
movq %r15, %rsi
; print (pre-incremented) i
callq __TFSs7printlnU__FQ_T_
; compare i to 4_000
cmpq 00, %rbx
; loop if not equal
jne LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
.cfi_endproc
C 风格 for
循环
% echo "for var i = 0;i < 4_000;++i { println(i) }" | swiftc -O -emit-assembly -
; snip opening boiler plate...
LBB0_1:
movq %rbx, -32(%rbp)
movq %r14, %rdi
movq %r15, %rsi
; print i
callq __TFSs7printlnU__FQ_T_
; increment i
incq %rbx
; jump if overflow
jo LBB0_4
; compare i to 4_000
cmpq 00, %rbx
; loop if less than
jl LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
LBB0_4:
; raise illegal instruction due to overflow
ud2
.cfi_endproc
所以 C 风格循环变慢的原因是因为它执行了一个额外的操作——检查溢出。编写 Range
是为了避免溢出检查(或预先执行),或者优化器更能够使用 Range
版本消除它。
如果改用免检查加法运算符,则可以消除此检查。这会生成与基于范围的版本几乎相同的代码(唯一的区别是代码的一些无关紧要的排序):
% echo "for var i = 0;i < 4_000;i = i &+ 1 { println(i) }" | swiftc -O -emit-assembly -
; snip
LBB0_1:
movq %rbx, -32(%rbp)
movq %r14, %rdi
movq %r15, %rsi
callq __TFSs7printlnU__FQ_T_
incq %rbx
cmpq 00, %rbx
jne LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
.cfi_endproc
从不对未优化的构建进行基准测试
如果您想了解原因,请尝试查看上面基于 Range
的版本的输出,但没有优化:echo "for var i = 0;i < 4_000;++i { println(i) }" | swiftc -Onone -emit-assembly -
。你会看到它输出了 lot 多的代码。这是因为通过 for…in
使用的 Range
是一种抽象,一种与自定义运算符和函数返回生成器一起使用的结构,并进行了很多安全检查和其他有用的事情。这使得 很多 更容易 write/read 编码。但是当你打开优化器时,所有这些都消失了,你留下了非常高效的代码。
基准测试
关于基准测试的方法,这是我倾向于使用的代码,只是替换数组:
import CoreFoundation.CFDate
func timeRun<T>(name: String, f: ()->T) -> String {
let start = CFAbsoluteTimeGetCurrent()
let result = f()
let end = CFAbsoluteTimeGetCurrent()
let timeStr = toString(Int((end - start) * 1_000_000))
return "\(name)\t\(timeStr)µs, produced \(result)"
}
let n = 4_000
let runs: [(String,()->Void)] = [
("for in range", {
for i in 0..<n { println(i) }
}),
("plain ol for", {
for var i = 0;i < n;++i { println(i) }
}),
("w/o overflow", {
for var i = 0;i < n;i = i &+ 1 { println(i) }
}),
]
println("\n".join(map(runs, timeRun)))
但结果可能毫无意义,因为 println
期间的抖动可能会掩盖实际测量结果。要真正进行基准测试(假设您不只是相信程序集分析 :),您需要用非常轻量级的东西替换它。
我了解到基于范围的循环在某些编程语言上具有更好的性能。 Swift中是否如此。例如在 Playgroud 中:
func timeDebug(desc: String, function: ()->() )
{
let start : UInt64 = mach_absolute_time()
function()
let duration : UInt64 = mach_absolute_time() - start
var info : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
mach_timebase_info(&info)
let total = (duration * UInt64(info.numer) / UInt64(info.denom)) / 1_000
println("\(desc): \(total) µs.")
}
func loopOne(){
for i in 0..<4000 {
println(i);
}
}
func loopTwo(){
for var i = 0; i < 4000; i++ {
println(i);
}
}
基于范围的循环
timeDebug("Loop One time"){
loopOne(); // Loop One time: 2075159 µs.
}
正常for循环
timeDebug("Loop Two time"){
loopTwo(); // Loop Two time: 1905956 µs.
}
如何在 swift 中正确进行基准测试?
// 在设备上更新
第一个运行
循环两次:54 µs。
循环一次:482 µs.
第二
循环两次:44 µs。
循环一次:382 µs.
第三
循环两次:43 µs。
循环一次:419 µs.
第四
循环两次:44 µs。
循环一次:399 µs.
// 更新 2
func printTimeElapsedWhenRunningCode(title:String, operation:()->()) {
let startTime = CFAbsoluteTimeGetCurrent()
operation()
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
println("Time elapsed for \(title): \(timeElapsed) s")
}
printTimeElapsedWhenRunningCode("Loop Two time") {
loopTwo(); // Time elapsed for Loop Two time: 4.10079956054688e-05 s
}
printTimeElapsedWhenRunningCode("Loop One time") {
loopOne(); // Time elapsed for Loop One time: 0.000500023365020752 s.
}
你真的不应该在 playgrounds 中进行基准测试,因为它们没有经过优化。除非您对调试时需要多长时间感兴趣,否则您应该只对基准优化构建 (swiftc -O
)。
要了解为什么基于范围的循环可以更快,您可以查看为两个选项生成的程序集:
基于范围
% echo "for i in 0..<4_000 { println(i) }" | swiftc -O -emit-assembly -
; snip opening boiler plate...
LBB0_1:
movq %rbx, -32(%rbp)
; increment i
incq %rbx
movq %r14, %rdi
movq %r15, %rsi
; print (pre-incremented) i
callq __TFSs7printlnU__FQ_T_
; compare i to 4_000
cmpq 00, %rbx
; loop if not equal
jne LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
.cfi_endproc
C 风格 for
循环
% echo "for var i = 0;i < 4_000;++i { println(i) }" | swiftc -O -emit-assembly -
; snip opening boiler plate...
LBB0_1:
movq %rbx, -32(%rbp)
movq %r14, %rdi
movq %r15, %rsi
; print i
callq __TFSs7printlnU__FQ_T_
; increment i
incq %rbx
; jump if overflow
jo LBB0_4
; compare i to 4_000
cmpq 00, %rbx
; loop if less than
jl LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
LBB0_4:
; raise illegal instruction due to overflow
ud2
.cfi_endproc
所以 C 风格循环变慢的原因是因为它执行了一个额外的操作——检查溢出。编写 Range
是为了避免溢出检查(或预先执行),或者优化器更能够使用 Range
版本消除它。
如果改用免检查加法运算符,则可以消除此检查。这会生成与基于范围的版本几乎相同的代码(唯一的区别是代码的一些无关紧要的排序):
% echo "for var i = 0;i < 4_000;i = i &+ 1 { println(i) }" | swiftc -O -emit-assembly -
; snip
LBB0_1:
movq %rbx, -32(%rbp)
movq %r14, %rdi
movq %r15, %rsi
callq __TFSs7printlnU__FQ_T_
incq %rbx
cmpq 00, %rbx
jne LBB0_1
xorl %eax, %eax
addq , %rsp
popq %rbx
popq %r14
popq %r15
popq %rbp
retq
.cfi_endproc
从不对未优化的构建进行基准测试
如果您想了解原因,请尝试查看上面基于 Range
的版本的输出,但没有优化:echo "for var i = 0;i < 4_000;++i { println(i) }" | swiftc -Onone -emit-assembly -
。你会看到它输出了 lot 多的代码。这是因为通过 for…in
使用的 Range
是一种抽象,一种与自定义运算符和函数返回生成器一起使用的结构,并进行了很多安全检查和其他有用的事情。这使得 很多 更容易 write/read 编码。但是当你打开优化器时,所有这些都消失了,你留下了非常高效的代码。
基准测试
关于基准测试的方法,这是我倾向于使用的代码,只是替换数组:
import CoreFoundation.CFDate
func timeRun<T>(name: String, f: ()->T) -> String {
let start = CFAbsoluteTimeGetCurrent()
let result = f()
let end = CFAbsoluteTimeGetCurrent()
let timeStr = toString(Int((end - start) * 1_000_000))
return "\(name)\t\(timeStr)µs, produced \(result)"
}
let n = 4_000
let runs: [(String,()->Void)] = [
("for in range", {
for i in 0..<n { println(i) }
}),
("plain ol for", {
for var i = 0;i < n;++i { println(i) }
}),
("w/o overflow", {
for var i = 0;i < n;i = i &+ 1 { println(i) }
}),
]
println("\n".join(map(runs, timeRun)))
但结果可能毫无意义,因为 println
期间的抖动可能会掩盖实际测量结果。要真正进行基准测试(假设您不只是相信程序集分析 :),您需要用非常轻量级的东西替换它。