为什么在F#中使用printf会很慢?

17

我很惊讶F#中的printf有多慢。我有一些用C#编写的程序,处理大型数据文件并写出多个CSV文件。起初,我使用fprintf writer "%s,%d,%f,%f,%f,%s",认为这很简单且效率合理。

然而,过了一段时间,我开始对等待文件处理感到有些厌烦。(我需要处理4gb大小的XML文件,并从其中写出条目)。

当我通过性能分析器运行我的应用程序时,我惊讶地发现printf是其中一个非常缓慢的方法。

我改变了代码,不再使用printf,现在性能好多了。Printf的性能影响了我的整体应用程序性能。

举个例子,我的原始代码如下:

fprintf sectorWriter "\"%s\",%f,%f,%d,%d,\"%s\",\"%s\",\"%s\",%d,%d,%d,%d,\"%s\",%d,%d,%d,%d,%s,%d"
    sector.Label sector.Longitude sector.Latitude sector.RNCId sector.CellId
    siteName sector.Switch sector.Technology (int sector.Azimuth) sector.PrimaryScramblingCode
    (int sector.FrequencyBand) (int sector.Height) sector.PatternName (int sector.Beamwidth) 
    (int sector.ElectricalTilt) (int sector.MechanicalTilt) (int (sector.ElectricalTilt + sector.MechanicalTilt))
    sector.SectorType (int sector.Radius)

我已将其更改为以下内容。

seq {
    yield sector.Label; yield string sector.Longitude; yield string sector.Latitude; yield string sector.RNCId; yield string sector.CellId; 
    yield siteName; yield sector.Switch; yield sector.Technology; yield string (int sector.Azimuth); yield string sector.PrimaryScramblingCode;
    yield string (int sector.FrequencyBand); yield string (int sector.Height); yield sector.PatternName; yield string (int sector.Beamwidth); 
    yield string (int sector.ElectricalTilt); yield string (int sector.MechanicalTilt); 
    yield string (int (sector.ElectricalTilt + sector.MechanicalTilt));
    yield sector.SectorType; yield string (int sector.Radius)
}
|> writeCSV sectorWriter

辅助函数

let writeDelimited delimiter (writer:TextWriter) (values:seq<string>) =
    values
    |> Seq.fold (fun (s:string) v -> if s.Length = 0 then v else s + delimiter + v) ""
    |> writer.WriteLine

let writeCSV (writer:TextWriter) (values:seq<string>) = writeDelimited "," writer values

我正在写出大约30,000行的文件,没有什么特别的。


1
printf是一个非常通用的工具。这并不是免费的。 - Peter G.
3
相对于什么而言,速度慢?你能否发一些类似于你的代码,我们可以用来进行性能分析? - Mauricio Scheffer
2
@Nick - 你可以更高效地使用String.concat来连接你的字符串序列,而不是使用Seq.fold - Stephen Swensen
2
@Nick: 如果你关心性能的话,为什么要使用字符串拼接而不是StringBuilder呢? - ildjarn
@Jon Harrop。那么,Nick Randell 使用了错误的打印方式吗?他应该重复使用带有绑定格式的 printf 吗?您是否有其他建议,如何以足够快的方式使用 printf 解决 Nick Randall 的问题? - Peter G.
显示剩余9条评论
4个回答

11

我不确定这有多重要,但是...

检查printf的代码:

https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/printf.fs

我看到了

// The general technique used this file is to interpret
// a format string and use reflection to construct a function value that matches
// the specification of the format string.  

我认为词语“反射”可能回答了这个问题。

printf适用于编写简单的类型安全输出,但是如果您想要在内部循环中获得良好的性能,您可能需要使用更低级别的.NET API来编写输出。我还没有进行自己的基准测试来查看。


10

TextWriter已经缓冲其输出。我建议使用Write单独输出每个值,而不是格式化整个行并将其传递给WriteLine。在我的笔记本电脑上,使用您的函数编写100,000行几乎需要一分钟的时间,而使用以下功能则只需半秒钟即可运行。

let writeRow (writer:TextWriter) siteName (sector:Sector) = 
  let inline write (value:'a) (delim:char) = 
    writer.Write(value)
    writer.Write(delim)
  let inline quote s = "\"" + s + "\""
  write (quote sector.Label) ','
  write sector.Longitude ','
  write sector.Latitude ','
  write sector.RNCId ','
  write sector.CellId ','
  write (quote siteName) ','
  write (quote sector.Switch) ','
  write (quote sector.Technology) ','
  write (int sector.Azimuth) ','
  write sector.PrimaryScramblingCode ','
  write (int sector.FrequencyBand) ','
  write (int sector.Height) ','
  write (quote sector.PatternName) ','
  write (int sector.Beamwidth) ','
  write (int sector.ElectricalTilt) ','
  write (int sector.MechanicalTilt) ','
  write (int (sector.ElectricalTilt + sector.MechanicalTilt)) ','
  write sector.SectorType ','
  write (int sector.Radius) '\n'

8
现在F# 3.1已经预览发布,据称printf的性能提高了40倍。你可能需要看一下这个:
引用: F# 3.1编译器/库的增加 Printf性能 F# 3.1核心库为类型安全格式化的printf函数家族提供改进的性能。例如,使用以下格式字符串打印现在运行速度最多快了40倍(尽管您的确切里程可能有所不同): 没有改变您的代码即可利用这种改进的性能,但您确实需要使用F#3.1 FSharp.Core.dll运行时组件。

6

编辑:此答案仅适用于简单的格式化字符串,例如“%s”或“%d”。请参见下面的评论。

有趣的是,如果您可以制作一个柯里化函数并重复使用它,那么反射只会执行一次。示例:

let w = new System.IO.StringWriter() :> System.IO.TextWriter
let printer = fprintf w "%d"
let printer2 d = fprintf w "%d" d

let print1() = 
   for i = 1 to 100000 do
      printer 2
let print2() = 
   for i = 1 to 100000 do
      printer2 2
let time f = 
   let sw = System.Diagnostics.Stopwatch()
   sw.Start()
   f()
   printfn "%s" (sw.ElapsedMilliseconds.ToString())

time print1
time print2

在我的机器上,print1花费了48毫秒,而print2花费了1158毫秒。


我已经为你的答案点了赞,但是现在我有机会测试后,我没有看到柯里化版本有任何区别。我正在使用 let writef = fprintf TextWriter.Null "\"%s\",%f,%f,%d,%d,\"%s\",\"%s\",\"%s\",%d,%d,%d,%d,\"%s\",%d,%d,%d,%d,%s,%d" - Daniel
1
@Daniel - 很好的发现!经过进一步测试,似乎只有在只有一个参数的情况下才会获得显著的加速。有两个参数就足以使它再次变慢。 - Robert Jeppesen
1
@Daniel:找到了,这是一个简单情况的优化。在 https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/printf.fs 的第511行。我猜在生产FSharp.Core中有比'%s'更多的优化。 - Robert Jeppesen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接