检查计算的 F# 性能影响?



使用已检查模块对性能有影响吗? 我已经用 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.ovfadd.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倍左右。

相关内容

  • 没有找到相关文章

最新更新