我试图用蛮力的方法来解决ITA Software's "Word Nubmers" puzzle。看起来我的Haskell版本比C#/ C ++版本慢10倍。
感谢Bryan O'Sullivan's answer,我能够“纠正”我的程序,使其达到可接受的性能。你可以阅读他的代码比我的清洁得多。我将在这里概述要点。
Int
为Int64
。除非您unsafeCoerce
,否则您应该使用Int
。这样您就不必拥有fromIntegral
。在Windows 32位GHC上执行Int64
只是 darn 慢,避免它。 (这实际上不是GHC的错。正如我在下面的博客文章中提到的,32位程序中的64位整数一般都很慢(至少在Windows中))-fllvm
或-fvia-C
表现。quotRem
至divMod
,quotRem
已足够。这让我加速了20%。Data.Vector
至Data.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
(注意:我完全清楚强制它不是解决这个问题的正确方法。我主要感兴趣的是让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))
答案 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)
以下是我在快速调查中可以提出的两点建议:
请注意,当您使用32位版本的GHC时,使用Int64
非常慢,这是目前Haskell平台的默认设置。这也是previous performance problem中的主要恶棍(我还提供了一些细节)。
由于原因,我不太了解divMod
函数似乎没有内联。结果,数字在堆上返回。分别使用div
和mod
时,wordLength'
纯粹在堆栈上执行。
可悲的是,我目前没有64位GHC来测试这是否足以解决问题。