如何在F#中有条件地包装sprintf?
我读过一个类似的问题:,但我的要求有点不同,所以我想知道它是否可行 首先,我想解释一下这个场景,我现在有一个跟踪函数,比如如何在F#中有条件地包装sprintf?,f#,F#,我读过一个类似的问题:,但我的要求有点不同,所以我想知道它是否可行 首先,我想解释一下这个场景,我现在有一个跟踪函数,比如 let Trace traceLevel ( fs : unit -> string) = if traceLevel <= Config.TraceLevel then Trace.WriteLine <| fs() 总是写“有趣->sprintf”部分是相当乏味的。理想情况下,最好能提供一种用户只需编写的风格 Trace 4
let Trace traceLevel ( fs : unit -> string) =
if traceLevel <= Config.TraceLevel then
Trace.WriteLine <| fs()
总是写“有趣->sprintf”部分是相当乏味的。理想情况下,最好能提供一种用户只需编写的风格
Trace 4 "%s : %i" "abc" 1
它可以
- 获取sprintf提供的格式/参数检查
- 与采用lambda“fs”的原始跟踪函数具有相同的性能行为。这意味着,如果跟踪级别的检查返回false,则本质上是一个no-op。不需要支付额外费用(例如字符串格式设置等)
trace1:
Real: 00:00:00.009, CPU: 00:00:00.015, GC gen0: 2, gen1: 1, gen2: 0
trace2:
Real: 00:00:00.709, CPU: 00:00:00.703, GC gen0: 54, gen1: 1, gen2: 0
trace3:
Real: 00:00:50.918, CPU: 00:00:50.906, GC gen0: 431, gen1: 5, gen2: 0
因此,与选项1(尤其是选项3)相比,选项2和3都会带来显著的性能损失。如果字符串格式更复杂,这个差距就会扩大。例如,如果我将格式和参数更改为
"%s: %i %i %i %i %i" (i.ToString()) i (i * 2) (i * 3) (i * 4) (i * 5)
我明白了
到目前为止,似乎还没有令人满意的解决方案来获得可用性和性能。诀窍是使用
kprintf
函数:
let trace level fmt =
Printf.kprintf (fun s -> if level > 3 then printfn "%s" s) fmt
trace 3 "Number %d" 10
trace 4 "Better number %d" 42
您可以通过部分应用程序使用它,以便kprintf
的格式字符串所需的所有参数都将成为您正在定义的函数的参数
然后,函数用最后一个字符串调用一个continuation,这样您就可以决定如何处理它了。这里有一种方法,但是“no op”的情况需要使用反射和装箱,因此可能比简单地格式化字符串并将其丢弃要慢得多:-)
开放系统
打开Microsoft.FSharp.Reflection
让rec dummyFunc(funcTy:Type)返回=
如果是FSharpType.IsFunction(funcTy),则
让retTy=funcTy.GenericTypeArguments。[1]
MakeFunction(funcTy,(fun->dummyFunc retTy retVal))
else box retVal
让跟踪lvl(fmt:Printf.StringFormat())
跟踪3“%s:%i”abc“1//abc:1
跟踪4“%s:%i”“abc”1//
看看您的需求,在我看来,最重要的事情不是避免跟踪/记录本身,
但是避免了格式化要跟踪的字符串的工作
例如,使用System.Diagnostics.Trace
而不是printf
对您没有帮助,因为花费时间的是sprintf
,是吗
因此,有两种方法可以延迟格式化。一种是使用一个单位函数,就像你最初做的那样。或者,您可以使用lazy
作为等效项
open System
let traceUnitFn lvl (fs : unit -> string) =
if lvl <= 3 then Console.WriteLine(fs())
let traceLazy lvl (s:Lazy<string>) =
if lvl <= 3 then Console.WriteLine(s.Force())
如果我们测试这些,我们得到:
printfn "trace0Param"
#time
for i in 1..1000000 do
trace0Param 4 "hello"
#time
// trace0Param
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 8, gen1: 0, gen2: 0
printfn "trace1Param"
#time
for i in 1..1000000 do
trace1Param 4 "%i" i
#time
// trace1Param
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 7, gen1: 0, gen2: 0
printfn "trace2Param with i.ToString"
#time
for i in 1..1000000 do
trace2Param 4 "%s:%i" (i.ToString()) i
#time
// trace2Param with i.ToString
// Real: 00:00:00.123, CPU: 00:00:00.124, GC gen0: 25, gen1: 0, gen2: 0
前两个调用的速度与原来的一样快,因此问题都出在i.ToString()
调用中
如果我们将字符串参数硬编码为“hello”,则可以确认这一点:
最后一个也一样快。注意GC的数量也少了多少。如果性能非常关键,GC将对您造成伤害
所以问题真的变成了:为了追踪它们,你在转换值方面做了多少工作?
你会经常做像i.ToString()
这样昂贵的事情吗?如果不是,那么你根本不需要懒惰
最后,也是更重要的一点,所有这些微观分析测量都是断章取义的,任何基于它们的决策都是不成熟的
例如,即使是最差的实现,每秒也要进行800万次跟踪。基于对真实系统的分析,这真的是一个瓶颈吗?
如果没有,那么我就不必担心这些,只需选择最简单的实现 在@latkin建议的基础上,可以添加备忘录,以在一定程度上提高性能
module Trace4 =
let cache =
let d = ConcurrentDictionary<Type, obj> ()
d.[typeof<unit>] <- box ()
d
let rec buildFunction (ftype : Type) : obj =
let retTy = ftype.GenericTypeArguments.[1]
let retVal = getFunction retTy
FSharpValue.MakeFunction(ftype, (fun _ -> retVal))
and getFunction (ftype : Type) : obj =
cache.GetOrAdd (ftype, buildFunction)
let trace4 lvl (fmt : Printf.StringFormat<'T, unit>) =
if lvl <= 3 then Printf.kprintf (fun s -> Console.WriteLine(s)) fmt
else downcast Trace4.getFunction typeof<'T>
模块跟踪4=
让缓存=
设d=ConcurrentDictionary()
d、 [typeof]retVal)
和getFunction(ftype:Type):obj=
cache.GetOrAdd(ftype,buildFunction)
让trace4 lvl(fmt:Printf.StringFormat
在我看来,i.ToString()
似乎增加了一些显著的开销。即使为了避免不必要的格式设置而扩展Core.PrintF
,仍然要付出代价
就我个人而言,我完全赞成为未启用的跟踪设置零开销。在我工作的地方,我们有很多跟踪。这些跟踪的成本加起来相当快,如果我们没有为未启用的跟踪设置零开销,这将对我们的指标产生负面影响。如果我理解正确,字符串的格式仍然是以前的”(fun s->if level>3然后调用printfn“%s”s)。目标是在条件为false时不格式化字符串。此外,我需要将sprintf的结果提供给Trace.WriteLine。抱歉,我遗漏了你问题的这一部分。你担心性能吗?正确。我们希望使用各种详细级别编写代码
"%s: %i %i %i %i %i" (i.ToString()) i (i * 2) (i * 3) (i * 4) (i * 5)
trace1:
Real: 00:00:00.007, CPU: 00:00:00.015, GC gen0: 3, gen1: 1, gen2: 0
trace2:
Real: 00:00:01.912, CPU: 00:00:01.921, GC gen0: 136, gen1: 0, gen2: 0
trace3:
Real: 00:02:10.683, CPU: 00:02:10.671, GC gen0: 1074, gen1: 14, gen2: 1
let trace level fmt =
Printf.kprintf (fun s -> if level > 3 then printfn "%s" s) fmt
trace 3 "Number %d" 10
trace 4 "Better number %d" 42
open System
open Microsoft.FSharp.Reflection
let rec dummyFunc (funcTy : Type) retVal =
if FSharpType.IsFunction(funcTy) then
let retTy = funcTy.GenericTypeArguments.[1]
FSharpValue.MakeFunction(funcTy, (fun _ -> dummyFunc retTy retVal))
else box retVal
let trace lvl (fmt : Printf.StringFormat<'t, unit>) =
if lvl <= 3 then Printf.kprintf (fun s -> Console.WriteLine(s)) fmt
else downcast (dummyFunc typeof<'t> ())
trace 3 "%s : %i" "abc" 1 // abc : 1
trace 4 "%s : %i" "abc" 1 // <nothing>
open System
let traceUnitFn lvl (fs : unit -> string) =
if lvl <= 3 then Console.WriteLine(fs())
let traceLazy lvl (s:Lazy<string>) =
if lvl <= 3 then Console.WriteLine(s.Force())
printfn "traceUnitFn"
#time
for i in 1..1000000 do
traceUnitFn 4 (fun _ -> sprintf "%s:%i" (i.ToString()) i)
#time
// traceUnitFn
// Real: 00:00:00.008, CPU: 00:00:00.000, GC gen0: 7, gen1: 0, gen2: 0
printfn "traceLazy"
#time
for i in 1..1000000 do
traceLazy 4 <| lazy (sprintf "%s:%i" (string i) i)
#time
// traceLazy
// Real: 00:00:00.053, CPU: 00:00:00.046, GC gen0: 56, gen1: 0, gen2: 0
let trace0Param level fmt =
if level <= 3 then printfn fmt
let trace1Param level fmt x1 =
if level <= 3 then printfn fmt x1
let trace2Param level fmt x1 x2 =
if level <= 3 then printfn fmt x1 x2
printfn "trace0Param"
#time
for i in 1..1000000 do
trace0Param 4 "hello"
#time
// trace0Param
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 8, gen1: 0, gen2: 0
printfn "trace1Param"
#time
for i in 1..1000000 do
trace1Param 4 "%i" i
#time
// trace1Param
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 7, gen1: 0, gen2: 0
printfn "trace2Param with i.ToString"
#time
for i in 1..1000000 do
trace2Param 4 "%s:%i" (i.ToString()) i
#time
// trace2Param with i.ToString
// Real: 00:00:00.123, CPU: 00:00:00.124, GC gen0: 25, gen1: 0, gen2: 0
printfn "trace2Param with hello"
#time
for i in 1..1000000 do
trace2Param 4 "%s:%i" "hello" i
#time
// trace2Param with hello
// Real: 00:00:00.007, CPU: 00:00:00.000, GC gen0: 7, gen1: 0, gen2: 0
module Trace4 =
let cache =
let d = ConcurrentDictionary<Type, obj> ()
d.[typeof<unit>] <- box ()
d
let rec buildFunction (ftype : Type) : obj =
let retTy = ftype.GenericTypeArguments.[1]
let retVal = getFunction retTy
FSharpValue.MakeFunction(ftype, (fun _ -> retVal))
and getFunction (ftype : Type) : obj =
cache.GetOrAdd (ftype, buildFunction)
let trace4 lvl (fmt : Printf.StringFormat<'T, unit>) =
if lvl <= 3 then Printf.kprintf (fun s -> Console.WriteLine(s)) fmt
else downcast Trace4.getFunction typeof<'T>