如何在F#中有条件地包装sprintf?

时间:2015-07-15 22:52:44

标签: f#

我读过一个类似的问题: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

它可以

  • 获取sprintf提供的格式/参数检查。
  • 具有与原始跟踪函数相同的性能行为,该函数采用lambda&#34; fs&#34;。这意味着如果对跟踪级别的检查返回false,则它本质上是无操作。不支付额外费用(例如字符串格式等)

即使在阅读原始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

到目前为止,似乎仍然没有令人满意的解决方案来兼顾可用性和性能。

4 个答案:

答案 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以避免不必要的格式化,仍然会支付价格。

我个人认为,对于未启用的跟踪,我只需要零开销。在我工作的地方,我们有一个很多的痕迹。这些成本相当快,如果我们没有未启用的跟踪零开销,则会对我们的指标产生负面影响。