分析Haskell程序的慢性能

时间:2011-08-07 05:21:11

标签: performance haskell profiling

我试图用蛮力的方法来解决ITA Software's "Word Nubmers" puzzle。看起来我的Haskell版本比C#/ C ++版本慢10倍。

答案

感谢Bryan O'Sullivan's answer,我能够“纠正”我的程序,使其达到可接受的性能。你可以阅读他的代码比我的清洁得多。我将在这里概述要点。

    在Linux GHC x64上,
  • IntInt64。除非您unsafeCoerce,否则您应该使用Int。这样您就不必拥有fromIntegral。在Windows 32位GHC上执行Int64只是 darn 慢,避免它。 (这实际上不是GHC的错。正如我在下面的博客文章中提到的,32位程序中的64位整数一般都很慢(至少在Windows中))
  • -fllvm-fvia-C表现。
  • 首选quotRemdivModquotRem已足够。这让我加速了20%。
  • 一般情况下,首选Data.VectorData.Array作为“数组”
  • 大量使用包装工人模式。

以上几点足以让我比我原来的版本提升100%。

In my blog post,我详细介绍了如何将原始程序与Bryan程序相匹配的逐步说明示例。还有其他一些要点。

原始问题

(这可能听起来像“你能为我做的工作”一文,但我认为这样一个具体的例子非常有启发性,因为剖析Haskell的表现往往被视为神话)

(如评论中所述,我认为我误解了这个问题。但是谁关心,我们可以专注于不同问题的表现)

这是我对该问题的快速回顾的版本:

A wordNumber is defined as

wordNumber 1 = "one"
wordNumber 2 = "onetwo"
wordNumber 3 = "onethree"
wordNumber 15 = "onetwothreefourfivesixseveneightnineteneleventwelvethirteenfourteenfifteen"
...

Problem: Find the 51-billion-th letter of (wordNumber Infinity); assume that letter is found at 'wordNumber x', also find 'sum [1..x]'

从命令的角度来看,一个天真的算法就是拥有2个计数器,一个用于数字之和,另一个用于长度之和。继续计算每个wordNumber的长度,并“break”以返回结果。

强制暴力方法在C#中实现:http://ideone.com/JjCb3。在我的电脑上找到答案大约需要1.5分钟。还有一个C++ implementation在我的计算机上运行45秒。

然后我实现了一个暴力的Haskell版本:http://ideone.com/ngfFq。它无法在我的机器上完成5分钟的计算。 (反讽:它的行数多于C#版本)

以下是Haskell计划的-p个人资料:http://hpaste.org/49934

问题:如何让它在C#版本上表现得相对较好?我有明显的错误吗?

(注意:我完全清楚强制它不是解决这个问题的正确方法。我主要感兴趣的是让Haskell版本的性能与C#版本相当。现在它至少慢了5倍这么明显我错过了一些明显的东西)

(注2:它似乎没有空间泄漏。程序在我的计算机上以恒定内存(大约2MB)运行)

(注3:我正在使用`ghc -O2 WordNumber.hs编译)

为了使问题更易于阅读,我提供了两个版本的“要点”。

// C#
long sumNum = 0;
long sumLen = 0;

long target = 51000000000;
long i = 1;

for (; i < 999999999; i++)
{
    // WordiLength(1)   = 3   "one"
    // WordiLength(101) = 13  "onehundredone"
    long newLength = sumLen + WordiLength(i);
    if (newLength >= target)
        break;

    sumNum += i;
    sumLen = newLength;
}
Console.WriteLine(Wordify(i)[Convert.ToInt32(target - sumLen - 1)]);

-

-- Haskell
-- This has become totally ugly during my squeeze for 
-- performance

-- Tail recursive
-- n-th number (51000000000 in our problem) -> accumulated result -> list of 'zipped' left to try
-- accumulated has the format (sum of numbers, current lengths of the whole chain, the current number)
solve :: Int64 -> (Int64, Int64, Int64) -> [(Int64, Int64)] -> (Int64, Int64, Int64)
solve !n !acc@(!sumNum, !sumLen, !curr) ((!num, !len):xs)
    | sumLen' >= n = (sumNum', sumLen, num)
    | otherwise = solve n (sumNum', sumLen', num) xs
    where
        sumNum' = sumNum + num
        sumLen' = sumLen + len

-- wordLength 1   = 3    "one"
-- wordLength 101 = 13   "onehundredone"
wordLength :: Int64 -> Int64
-- wordLength = ...

solution :: Int64 -> (Int64, Char)
solution !x =
    let (sumNum, sumLen, n) = solve x (0,0,1) (map (\n -> (n, wordLength n)) [1..])
    in (sumNum, (wordify n) !! (fromIntegral $ x - sumLen - 1))

2 个答案:

答案 0 :(得分:10)

我写了一个要点,其中包含C++ version(来自a Haskell-cafe message的你的副本,修复了错误)和Haskell translation

请注意,这两者在结构上几乎完全相同。使用-fllvm进行编译时,Haskell代码的运行速度大约是C ++代码的一半,这非常好。

现在让我们将我的Haskell wordLength代码与您的代码进行比较。你传递了一个额外的不必要的参数,这是不必要的(你在编写我翻译的C ++代码时显然已经知道了)。此外,大量的爆炸模式表明恐慌;他们几乎都没用。

您的solve功能也非常困惑。

  • 您以三种不同的方式传递参数:常规Int,3元组和列表!哇。

  • 这个函数的行为必然不是很规律,所以当你通过使用列表来提供你的计数器时你没有任何风格,你可能会强制GHC分配内存。换句话说,这会混淆代码并使其变慢。

  • 通过使用三个参数的元组(没有明显的原因),你再次努力迫使GHC为循环中的每一步分配内存,如果你传递参数可以避免这样做直接

  • 只有您的n参数才能以合理的方式处理,但您不需要使用爆炸模式。

  • 需要爆炸模式的参数是sumNum,因为在循环完成之前,您永远不会检查其值。 GHC的严格分析仪将与其他人打交道。所有其他爆炸模式都是不必要的,最糟糕的是误导。

答案 1 :(得分:5)

以下是我在快速调查中可以提出的两点建议:

  1. 请注意,当您使用32位版本的GHC时,使用Int64非常慢,这是目前Haskell平台的默认设置。这也是previous performance problem中的主要恶棍(我还提供了一些细节)。

  2. 由于原因,我不太了解divMod函数似乎没有内联。结果,数字在堆上返回。分别使用divmod时,wordLength'纯粹在堆栈上执行。

  3. 可悲的是,我目前没有64位GHC来测试这是否足以解决问题。