当您测量性能时,包含 Seq 通常不是一个好主意,因为 Seq 会增加大量开销(至少与 int 操作相比),因此您有可能大部分时间都花在 Seq 上,而不是在你想测试的代码中。
我为(+)写了一个小测试程序:
let clock =
let sw = System.Diagnostics.Stopwatch ()
sw.Start ()
fun () ->
sw.ElapsedMilliseconds
let dbreak () = System.Diagnostics.Debugger.Break ()
let time a =
let b = clock ()
let r = a ()
let n = clock ()
let d = n - b
d, r
module Unchecked =
let run c () =
let rec loop a i =
if i < c then
loop (a + 1) (i + 1)
else
a
loop 0 0
module Checked =
open Checked
let run c () =
let rec loop a i =
if i < c then
loop (a + 1) (i + 1)
else
a
loop 0 0
[<EntryPoint>]
let main argv =
let count = 1000000000
let testCases =
[|
"Unchecked" , Unchecked.run
"Checked" , Checked.run
|]
for nm, a in testCases do
printfn "Running %s ..." nm
let ms, r = time (a count)
printfn "... it took %d ms, result is %A" ms r
0
性能结果是这样的:
Running Unchecked ...
... it took 561 ms, result is 1000000000
Running Checked ...
... it took 1103 ms, result is 1000000000
因此,使用 Checked 似乎会增加一些开销。 int add 的开销应该小于循环开销,所以Checked 的开销高于2x 可能更接近4x。
出于好奇,我们可以使用ILSpy 之类的工具检查 IL 代码:
未选中:
IL_0000: nop
IL_0001: ldarg.2
IL_0002: ldarg.0
IL_0003: bge.s IL_0014
IL_0005: ldarg.0
IL_0006: ldarg.1
IL_0007: ldc.i4.1
IL_0008: add
IL_0009: ldarg.2
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: starg.s i
IL_000e: starg.s a
IL_0010: starg.s c
IL_0012: br.s IL_0000
已检查:
IL_0000: nop
IL_0001: ldarg.2
IL_0002: ldarg.0
IL_0003: bge.s IL_0014
IL_0005: ldarg.0
IL_0006: ldarg.1
IL_0007: ldc.i4.1
IL_0008: add.ovf
IL_0009: ldarg.2
IL_000a: ldc.i4.1
IL_000b: add.ovf
IL_000c: starg.s i
IL_000e: starg.s a
IL_0010: starg.s c
IL_0012: br.s IL_0000
唯一的区别是 Unchecked 使用 add 而 Checked 使用 add.ovf。 add.ovf 添加溢出检查。
我们可以通过查看 jitted x86_64 代码来更深入地挖掘。
未选中:
; if i < c then
00007FF926A611B3 cmp esi,ebx
00007FF926A611B5 jge 00007FF926A611BD
; i + 1
00007FF926A611B7 inc esi
; a + 1
00007FF926A611B9 inc edi
; loop (a + 1) (i + 1)
00007FF926A611BB jmp 00007FF926A611B3
已检查:
; if i < c then
00007FF926A62613 cmp esi,ebx
00007FF926A62615 jge 00007FF926A62623
; a + 1
00007FF926A62617 add edi,1
; Overflow?
00007FF926A6261A jo 00007FF926A6262D
; i + 1
00007FF926A6261C add esi,1
; Overflow?
00007FF926A6261F jo 00007FF926A6262D
; loop (a + 1) (i + 1)
00007FF926A62621 jmp 00007FF926A62613
现在Checked 开销的原因是可见的。每次操作后,抖动插入条件指令jo,如果设置了溢出标志,则跳转到引发OverflowException的代码。
chart 向我们展示了整数加法的成本小于 1 个时钟周期。它小于 1 个时钟周期的原因是现代 CPU 可以并行执行某些指令。
图表还向我们展示了 CPU 正确预测的分支大约需要 1-2 个时钟周期。
因此假设吞吐量至少为 2,则 Unchecked 示例中两个整数加法的成本应该是 1 个时钟周期。
在 Checked 示例中,我们执行add, jo, add, jo。在这种情况下,CPU 很可能无法并行化,其成本应该在 4-6 个时钟周期左右。
另一个有趣的区别是添加的顺序发生了变化。选中添加后,操作的顺序很重要,但未选中时,抖动(和 CPU)在移动操作时具有更大的灵活性,可能会提高性能。
长话短说;对于像(+) 这样的廉价操作,与Unchecked 相比,Checked 的开销应该在4x-6x 左右。
这假定没有溢出异常。 .NET 异常的成本可能比整数加法高出 100,000x 倍。