【问题标题】:Performance issue性能问题
【发布时间】:2017-04-13 04:00:13
【问题描述】:

我正在使用时间序列 type TSeries = [(Day, Double)],但需要将第一个 Day 元素转换为 Double 以进行进一步处理(例如绘图等)。

将日期范围映射到相应的 Double 范围 [lobound, upbound],其中最早的日期映射到 lobound,最新的日期映射到 upbound,这是一个基本的转换。为了实现它,我首先需要获取日期范围的最小值和最大值。我遇到了性能问题,但我不确定为什么以及如何解决它。

这里是代码(时间序列不假定是排序的):

module Main where

import Data.Time (Day, fromGregorian, diffDays)

type TSeries = [(Day, Double)]

-- time-series to (Double, Double) mapping function
toDbl :: (Day -> Double) -> TSeries -> [(Double, Double)]
toDbl mapX ts = map (\(d,x) -> (mapX d, x)) ts

-- Day to Double mapping function - fast
mapDays1 :: (Day, Double) -> (Day, Double) -> Day -> Double
mapDays1 (d0,x0) (d1,x1) d = ((fromIntegral $ diffDays d d0) * x1 + (fromIntegral $ diffDays d1 d) * x0) / diff10
    where diff10 = fromIntegral $ diffDays d1 d0

-- Day to Double mapping function - slow
mapDays2 :: TSeries -> Double -> Double -> Day -> Double
mapDays2 ts x0 x1 d = mapDays1 (d0,x0) (d1,x1) d
    where d0 = minimum $ map fst ts
          d1 = maximum $ map fst ts

-- example time-series
func :: Int -> Double
func d = sin $ pi / 14 * (fromIntegral d)
ts = [(fromGregorian y m d, func d) | y <- [2000..2016], m <- [1..12], d <- [1..28]] :: TSeries

-- speed test
main = do
    let mindate = minimum $ map fst ts
        maxdate = maximum $ map fst ts
        test1 = toDbl (mapDays1 (mindate,0.0) (maxdate,100.0)) ts
        test2 = toDbl (mapDays2 ts 0.0 100.0) ts

    -- print $ sum $ map fst test1 -- this is fast
    print $ sum $ map fst test2 -- this is slow

我执行的测试(首先对 X 轴求和)不相关,但它很简单并且很好地说明了性能问题。

基本上 mapDays1 和 mapDays2 是相同的,只是为了获得适当的缩放,我需要在外部计算最小和最大日期并将它们传递给 mapDays1,而这是在 mapDays2 中“内部”完成的。

问题是 mapDays2 与 mapDays1 版本相比非常慢。我怀疑最小和最大计算被调用了很多次(而不是一次),但我不明白为什么,我不确定如何修复 mapDays2 以获得类似于 mapDays1 的性能。

【问题讨论】:

    标签: haskell memoization


    【解决方案1】:

    这个问题确实与记忆有关。问题是您调用 mapDays1mapDays2 时没有传递它们的所有参数,因此这些调用只会创建 thunk。

    问题

    这意味着 thunk 仅在 map 内完成,因此对 mapDays2 的不同调用无法共享它们对 d0 = minimum $ map fst tsd1 = maximum $ map fst ts 的结果,并且 最大值和最小值重新获得每次评估。可以想象d0d1 取决于最后一个Day 参数的情况,在这种情况下,每次都重新评估d0d1 是不正确的 .

    相比之下,应该很清楚mindate = minimum $ map fst tsmaxdate = maximum $ map fst ts只需要计算一次。

    修复mapDays2

    虽然我们喜欢假装f x y = ef x = \y -&gt; e 相同,但它并不在幕后。您希望 GHC 在传递除最后一个参数之外的所有参数时避免产生 thunk。只需将d 移到等号上。然后,您返回的函数将只计算一次d0d1

    -- Day to Double mapping function - slow
    mapDays2 :: TSeries -> Double -> Double -> Day -> Double
    mapDays2 ts x0 x1 = \d -> mapDays1 (d0,x0) (d1,x1) d
        where d0 = minimum $ map fst ts
              d1 = maximum $ map fst ts
    

    【讨论】:

    • 谢谢,它运行良好。解决方案非常微妙;它如何防止 GHC 提早产生 thunk?
    • 即使我们假装柯里化使所有函数都只接受一个参数,但这并不是它实际工作的方式(那将是非常低效的)。 GHC 生成具有您指定的多个参数的函数,并使用 thunk 计算出所有内部柯里化。因此,接受 4 个参数的函数与接受 3 个参数并返回接受一个参数的函数之间存在区别。
    • "it is not under the hood" 这里的行为可能是由于内联和随后的简单优化(只有语法上完全应用右侧的函数可以内联),而不是更诡异的东西关于 ghc 如何处理函数。
    猜你喜欢
    • 2022-01-04
    • 2010-09-05
    • 2012-09-25
    • 2015-05-28
    • 2015-06-20
    • 2018-09-27
    • 2017-04-25
    • 2015-08-02
    相关资源
    最近更新 更多