检查计算的 F# 性能影响?
F# Performance Impact of Checked Calcs?
使用 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是不是有一些性能下降,但我还没有看到。
当您衡量性能时,包含 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.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,Unchecked 示例中两个整数相加的成本应该是 1 个时钟周期。
在 Checked 示例中,我们执行 add, jo, add, jo
。在这种情况下,很可能 CPU 无法并行化,其成本应该在 4-6 个时钟周期左右。
另一个有趣的区别是添加的顺序发生了变化。选中添加后,操作顺序很重要,但未选中时,抖动(和 CPU)具有更大的灵活性,移动操作可能会提高性能。
长话短说;对于像 (+)
这样的廉价操作,与 Unchecked
.
相比,Checked
的开销应该在 4x-6x
左右
这假定没有溢出异常。 .NET 异常的成本可能是整数加法的 100,000x
倍。
使用 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是不是有一些性能下降,但我还没有看到。
当您衡量性能时,包含 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
唯一的区别是 Unchecked 使用 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,Unchecked 示例中两个整数相加的成本应该是 1 个时钟周期。
在 Checked 示例中,我们执行 add, jo, add, jo
。在这种情况下,很可能 CPU 无法并行化,其成本应该在 4-6 个时钟周期左右。
另一个有趣的区别是添加的顺序发生了变化。选中添加后,操作顺序很重要,但未选中时,抖动(和 CPU)具有更大的灵活性,移动操作可能会提高性能。
长话短说;对于像 (+)
这样的廉价操作,与 Unchecked
.
Checked
的开销应该在 4x-6x
左右
这假定没有溢出异常。 .NET 异常的成本可能是整数加法的 100,000x
倍。