【问题标题】:F# Performance Impact of Checked Calcs?检查计算的 F# 性能影响?
【发布时间】:2017-03-30 17:27:43
【问题描述】:

使用 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是否有一些性能影响,但我还没有看到。

【问题讨论】:

  • 请注意,您的示例中似乎没有使用任何算术运算符,因此这可能不是对其效率的良好测试...
  • 也许是这样,但整数类型在 FSharp.Core.Operators.Checked 中被重载,所以部分问题是确定当溢出可能不是问题时是否对 int 类型的常规使用有影响.
  • 那些不是你看到的类型,那些是转换为数字类型的函数。类型完全相同,因此您当前的测试不会显示任何内容。如果你用checked做一些算术运算,会有一些额外的IL指令,所以它可能会慢一些,但很少需要担心。
  • 感谢您的澄清。

标签: performance f# integer-overflow


【解决方案1】:

当您测量性能时,包含 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.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,则 Unchecked 示例中两个整数加法的成本应该是 1 个时钟周期。

在 Checked 示例中,我们执行add, jo, add, jo。在这种情况下,CPU 很可能无法并行化,其成本应该在 4-6 个时钟周期左右。

另一个有趣的区别是添加的顺序发生了变化。选中添加后,操作的顺序很重要,但未选中时,抖动(和 CPU)在移动操作时具有更大的灵活性,可能会提高性能。

长话短说;对于像(+) 这样的廉价操作,与Unchecked 相比,Checked 的开销应该在4x-6x 左右。

这假定没有溢出异常。 .NET 异常的成本可能比整数加法高出 100,000x 倍。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-07-14
    • 2016-04-22
    • 2021-04-18
    • 2021-05-13
    • 1970-01-01
    • 2011-02-14
    • 2017-02-01
    • 2010-09-22
    相关资源
    最近更新 更多