使用Checked模块会对性能产生影响吗?我已经用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会有任何不利因素。 (我遇到了一个并不是很明显的溢出,所以我现在扮演的是那个不想要另一个伤心的被抛弃的情人的角色。)我能想出的唯一非人为的理由并不总是使用Checked如果有一些性能受到打击,但我还没有见过。
答案 0 :(得分:3)
当您衡量效果时,包含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_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
,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,则未经检查的示例中两个整数加法的成本应为1个时钟周期。
在Checked示例中,我们执行add, jo, add, jo
。在这种情况下,很可能CPU不能并行化,并且成本应该在4-6个时钟周期内。
另一个有趣的区别是添加顺序发生了变化。通过检查添加,操作的顺序很重要,但是如果不加以控制,抖动(和CPU)可以更灵活地移动操作,从而可能提高性能。
故事很长;对于(+)
等廉价操作,与Checked
相比,4x-6x
的开销应该在Unchecked
左右。
这假定没有溢出异常。 .NET异常的成本可能比整数加法的成本高100,000x
倍。