如何提高在Haskell中生成输出字符串的性能?

4
我有一个程序,我试图让它运行更快,主要是为了学习更多关于Haskell的知识。作为比较,我已经用C写了同样的程序,并获得了4倍的速度提升。我预期C会更快,但这种差异让我认为我做错了什么。
因此,我对Haskell代码进行了分析,发现超过50%的时间花费在生成格式化的String上。所以仅这一部分就比我的整个C程序运行时间还要长。这个函数类似于以下形式:
display :: POSIXTime -> [(Int, Centi)] -> IO()
display t l = putStrLn $ t_str ++ " " ++ l_str
    where
        t_str = show . timeToTimeOfDay . unsafeCoerce $ (t `mod` posixDayLength)
        l_str = intercalate " " $ map displayPair l
        displayPair (a,b) = show a ++ " " ++ show b

关于代码的注释: unsafeCoerce是将NominalDiffTime转换为DiffTime,它们具有相同的类型,但这比我之前使用的toRational . fromRational更快。 CentiData.Fixed中定义,是一个带有2位小数的数字。 TimeOfDay只包含小时、分钟和秒(以皮秒精度存储)。 `mod` posixDayLength是为了忽略日期,仅获取当天的时间(因为这是我关心的...从时间戳得到,并且我知道它必须是今天 - 我只关心今天的时间)。
我尝试过使用ShowS(String -> String)来连接结果,但这并不明显更快。
我尝试过使用Data.Text,但这使得代码变慢了(可能是花费了太多时间打包字符串)。
我曾经把putStrLn放在一个单独的函数中,但在这里它更快(少建立thunk?但是为什么?)。
有没有简单的方法可以改进Haskell的输出性能?我有所遗漏吗?

4
type String = [Char]并不适合(也不是设计用来)处理对性能要求较高的任务(Data.ByteStringData.Text则更为适用)。但这是否重要呢?我认为这很可能受到IO性能的限制,C版本几乎没有在程序本身中执行任何时间。 - leftaroundabout
尝试使用“Text”而不是“String”!链表并不适用于高性能。 - kqr
2
@dave 使用 Data.Text.IO - Michael Snoyman
1
如果您发布了可以编译并使用“criterion”或“TimeIt”的完整代码,我很乐意帮助您。 - Thomas M. DuBuisson
你的一些结构很慢。例如,你经常使用 ++ - 使用 Text 的 append 会更快。 - Thomas M. DuBuisson
显示剩余8条评论
1个回答

0

为了产生输出,最高的性能可以通过避免使用String而选择ByteString或Text来实现。为了构建输出,有一个特殊的数据类型称为Builder。在[ByteString] hackage描述中有一个带有示例的良好描述。

生成的代码如下:

import Data.Monoid
display :: POSIXTime -> [(Int, Centi)] -> IO()
display t l = hPutBuilder stdout $ t_str <> space <> l_str
    where
        space = (byteString . pack) " "
        t_str = (byteString . pack . show . timeToTimeOfDay . unsafeCoerce) $ (t `mod` posixDayLength)
        l_str = foldr <> space $ map displayPair l
        displayPair (a,b) = intDec a <> space <> (byteString . pack . show) b

构建器数据类型会构建出一些块,然后将它们在 O(1) 的时间复杂度内连接到输出缓冲区中。不幸的是,并非所有类型都有相应的构建器,只有基本类型才有。因此,对于其他类型的输出,唯一的解决方案就是打包字符串……或者编写一个函数来创建构建器(并将其添加到库中?)。


您可以使用 OverloadedStrings 扩展来避免构造(然后立即丢弃)任何字符串字面量。 您可以使用 Data.Text.Builder 来编写构建器,而不是使用 show 来显示值(毕竟这是其预期用途)。 我认为完全没有必要使用 unsafeCoerce,建议避免使用它。 - Rein Henrichs
@ReinHenrichs:看一下问题为什么存在:需要将NominalDiffTime转换为DiffTime,两者具有相同的表示形式,并且比(fromRational . toRational)更快。 - dave

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