使用已检查模块对性能有影响吗? 我已经用 int 类型的序列对其进行了测试,没有看到明显的差异。 有时检查版本更快,有时未选中版本更快,但通常不会太多。
Seq.initInfinite (fun x-> x) |> Seq.item 1000000000;;
Real: 00:00:05.272, CPU: 00:00:05.272, GC gen0: 0, gen1: 0, gen2: 0 val it : int = 1000000000
open Checked
Seq.initInfinite (fun x-> x) |> Seq.item 1000000000;;
Real: 00:00:04.785, CPU: 00:00:04.773, GC gen0: 0, gen1: 0, gen2: 0 val it : int = 1000000000
基本上,我试图弄清楚总是打开选中是否有任何缺点。 (我遇到了一个不是很明显的溢出,所以我现在扮演一个不想要另一颗破碎的心的紧张情人的角色。 我能想出的不总是使用 Checked 的唯一非人为原因是是否有一些性能影响,但我还没有看到。
当你衡量性能时,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
因此,似乎通过使用"选中"增加了一些开销。int add 的成本应该小于循环开销,因此Checked
的开销高于2x
可能更接近4x
。
出于好奇,我们可以使用以下工具检查 IL 代码ILSpy
:
猖獗:
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
唯一的区别是"未选中"使用add
和"已选中"使用add.ovf
。add.ovf
添加溢出检查。
我们可以通过查看抖动的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
的代码。
这张图表向我们展示了整数相加的成本小于 1 个时钟周期。它小于 1 个时钟周期的原因是现代 CPU 可以并行执行某些指令。
该图表还向我们显示了 CPU 正确预测的分支大约需要 1-2 个时钟周期。
因此,假设吞吐量至少为 2,则在"未检查"示例中,两个整数加法的成本应为 1 个时钟周期。
在已检查示例中,我们执行add, jo, add, jo
。在这种情况下,CPU 很可能无法并行化,其成本应该在 4-6 个时钟周期左右。
另一个有趣的区别是添加顺序发生了变化。通过检查添加,操作的顺序很重要,但未经检查时,抖动(和CPU)具有更大的灵活性,可以移动操作,从而提高性能。
长话短说;对于像(+)
这样的廉价操作,与Unchecked
相比,Checked
的开销应该在4x-6x
左右。
这假定没有溢出异常。.NET 异常的成本可能比整数加法贵100,000x
倍左右。