我读过一个类似的问题:Magic sprintf function - how to wrap it?,但我的要求有点不同,所以我想知道它是否可行。
首先,我想稍微解释一下这个场景,我目前有一个像
这样的跟踪功能let Trace traceLevel ( fs : unit -> string) =
if traceLevel <= Config.TraceLevel then
Trace.WriteLine <| fs()
所以功能&#34; fs&#34;仅当traceLevel小于或等于Config.TraceLevel指定的跟踪级别时,才会调用它来生成字符串。 因此,当traceLevel大于Config.TraceLevel时,它是一个无操作。 &#34; FS&#34;根本没有评估。
虽然不限于此,但在实践中,几乎所有用例都是
Trace 4 (fun _ -> sprintf "%s : %i" "abc" 1)
总是写出&#34;有趣_ - &gt;这是非常繁琐的。的sprintf&#34;部分。理想情况下,提供用户可以写的味道
会很不错Trace 4 "%s : %i" "abc" 1
它可以
即使在阅读原始SO question的答案之后,我也无法弄清楚如何实现这一目标。
似乎kprintf允许对格式化字符串调用continuation函数。包装器仍然返回一个printf函数返回的函数(然后它可以是一个带有一个或多个参数的函数)。因此,currying可以发挥作用。但是,在上面的情况中,需要的是在格式化字符串之前评估条件,然后将格式化的字符串应用于Trace.WriteLine。似乎现有的Printf模块具有允许注入前置条件评估的API。因此,通过包装现有API似乎不容易实现。
有关如何实现这一目标的任何想法? (我非常简短地阅读FSharp.Core/printf.fs,似乎可以通过提供新的派生PrintfEnv来实现。但是,这些是内部类型。)
感谢Tomas和Lincoln的回答。我认为这两种方法都会受到一些影响。我用fsi在我的机器上进行了一些简单的测量。
选项1:我的原始方法,在&#34; false&#34;路径,&#34; fs()&#34;根本没有评估。用法不是很好,因为需要写一个&#34; fun _ - &gt;的sprintf&#34;一部分。
let trace1 lvl (fs : unit -> string) =
if lvl <= 3 then Console.WriteLine(fs())
选项2:格式化字符串但将其丢弃在&#34; false&#34;路径
let trace2 lvl fmt =
Printf.kprintf (fun s -> if lvl <= 3 then Console.WriteLine(s)) fmt
选项3:通过递归,反射和方框
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 trace3 lvl (fmt : Printf.StringFormat<'t, unit>) =
if lvl <= 3 then Printf.kprintf (fun s -> Console.WriteLine(s)) fmt
else downcast (dummyFunc typeof<'t> ())
现在我用三个代码计算了所有这三个代码
for i in 1..1000000 do
trace1 4 (fun _ -> sprintf "%s:%i" (i.ToString()) i)
for i in 1..1000000 do
trace2 4 "%s:%i" (i.ToString()) i
for i in 1..1000000 do
trace3 4 "%s:%i" (i.ToString()) i
这是我得到的:
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)
我得到了
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
到目前为止,似乎仍然没有令人满意的解决方案来兼顾可用性和性能。
答案 0 :(得分:2)
诀窍是使用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,因此您可以决定如何处理它。
答案 1 :(得分:1)
这是一种方法,但是&#34; no-op&#34; case需要使用反射和装箱,所以它可能比简单地格式化字符串并将其丢弃要慢得多: - )
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>
答案 2 :(得分: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 "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
所以,好吧,使用lazy
会慢得多。
但sprintf
真的是瓶颈吗?我们试着直接调用它。
首先,我们需要为每个参数提供一个单独的函数:
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
前两个速度与原始速度一样快,因此i.ToString()
调用中存在问题。
如果我们将字符串参数硬编码为“hello”,我们可以确认这一点:
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
最后一个同样快。并注意到GC的数量也减少了多少。如果性能至关重要,GC会伤害你。
所以问题真的变成了:你在改造价值以追踪它们方面做了多少工作?
你会经常做i.ToString()
这样的昂贵的事情吗?如果没有,那么你根本不需要懒惰。
最后,更重要的是,所有这些微观剖析测量都是脱离背景绝对无用的,任何基于它们的决定都为时过早。
例如,即使是最糟糕的实现也是每秒 800万条跟踪。基于对真实系统进行分析,这真的是一个瓶颈吗? 如果没有,那么我不会担心这些,只选择最简单的实现。
答案 3 :(得分:1)
在@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>
在我看来,i.ToString()
正在增加一些重要的开销。即使有人会延长Core.PrintF
以避免不必要的格式化,仍然会支付价格。
我个人认为,对于未启用的跟踪,我只需要零开销。在我工作的地方,我们有一个很多的痕迹。这些成本相当快,如果我们没有未启用的跟踪零开销,则会对我们的指标产生负面影响。