F#性能对Checked Calcs的影响?

时间:2017-03-30 17:27:43

标签: performance f# integer-overflow

使用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如果有一些性能受到打击,但我还没有见过。

1 个答案:

答案 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代码

未选中:

    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.ovfadd.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倍。