如何在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。不需要支付额外费用(例如字符串格式设置等)
即使在阅读了原文的答案之后,我也不知道如何做到这一点

kprintf似乎允许针对格式化字符串调用延续函数。包装器仍然返回一个printf函数返回的函数(该函数可以是一个包含一个或多个参数的函数)。因此,咖喱可以发挥作用。但是,在上述情况下,需要在格式化字符串之前评估一个条件,然后将格式化的字符串应用于Trace.WriteLine。现有的Printf模块似乎有一个允许注入前置条件评估的API。因此,通过包装现有的API似乎不容易实现

你知道如何实现这一目标吗?(我读得非常简短,似乎可以通过提供一个新的派生PrintfEnv来实现。但是,这些是内部类型)

更新 谢谢托马斯和林肯的回答。我认为这两种方法都会影响性能。我用fsi在我的机器上做了一些简单的测量

选项1:我最初的方法,在“false”路径上,根本不计算“fs()。用法不是很好,因为需要编写“fun->sprintf”部分

以下是我得到的:

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>