FSharp运行我的算法比Python慢

时间:2011-05-01 17:54:31

标签: python algorithm performance f# dynamic-programming

多年前,我通过动态编程解决了一个问题:

https://www.thanassis.space/fillupDVD.html

解决方案是用Python编写的。

作为扩展视野的一部分,我最近开始学习OCaml / F#。有什么更好的方法来测试水域,而不是直接将我用Python编写的命令式代码移植到F# - 并从那里开始,逐步向功能性编程解决方案迈进。

第一个直接端口的结果......令人不安:

在Python下:

  bash$ time python fitToSize.py
  ....
  real    0m1.482s
  user    0m1.413s
  sys     0m0.067s

在FSharp下:

  bash$ time mono ./fitToSize.exe
  ....
  real    0m2.235s
  user    0m2.427s
  sys     0m0.063s

(如果您注意到上面的“mono”:我在Windows下测试过,使用Visual Studio - 速度相同)。

至少可以说,我很困惑。 Python比F#运行代码更快?使用.NET运行时编译的二进制文件运行SLOWER而不是Python的解释代码?!?!

我知道VM的启动成本(在这种情况下为单声道)以及JIT如何改进Python等语言的东西,但仍然......我期望加速,而不是减速!

我做错了吗?

我在这里上传了代码:

https://www.thanassis.space/fsharp.slower.than.python.tar.gz

请注意,F#代码或多或少是Python代码的直接逐行转换。

P.S。当然还有其他收获,例如F#提供的静态类型安全 - 但如果在F#下强制算法的结果速度更差......我很失望,至少可以说。

编辑:根据评论中的要求直接访问:

Python代码:https://gist.github.com/950697

FSharp代码:https://gist.github.com/950699

4 个答案:

答案 0 :(得分:47)

我通过电子邮件联系的Jon Harrop博士解释了发生了什么:

  

问题很简单,该程序已针对Python进行了优化。当程序员比其他语言更熟悉一种语言时,这种情况很常见。你只需要学习一套不同的规则来规定如何优化F#程序......   有几件事情在我身上跳了出来,例如使用“for i in 1..n do”循环而不是“for i = 1 to n do”循环(一般来说速度更快但在这里不重要),反复做列表上的List.mapi模仿数组索引(不必要地分配中间列表)和你对字典的F#TryGetValue的使用,它不必要地分配(接受ref的.NET TryGetValue一般更快但在这里没那么多)< / p>      

...但真正的杀手问题原来是你使用哈希表来实现密集的二维矩阵。使用哈希表在Python中是理想的,因为它的哈希表实现已得到极好的优化(事实证明你的Python代码运行速度与编译到本机代码的F#一样快!)但是数组是表示密集的更好方法矩阵,特别是当你想要一个默认值为零时。

有趣的是,当我第一次编码这个算法时,我 DID 使用了一个表 - 为了清晰起见,我将实现更改为字典(避免数组边界检查使代码更简单) - 并且更容易推理)。

Jon将我的代码(返回:-))转换为array version,并以100倍的速度运行。

故事的道德:

  • F#Dictionary需要工作......当使用元组作为键时,编译的F#比解释Python的哈希表慢!
  • 明显,但重复没有害处:更清洁的代码有时意味着......代码更慢。

谢谢Jon,非常感谢。

编辑:用数组替换字典使得F#最终以预期运行的编译语言运行的速度运行,并不否定需要修复字典的速度(我希望F# MS的人正在读这个)。其他算法依赖于字典/哈希,并且不能轻易切换到使用数组;每当使用字典时,使程序遭受“解释器速度”,可以说是一个错误。如果像正如一些人在评论中所说的那样,问题不在于F#,而在于.NET Dictionary,那么我认为这......是.NET中的一个错误!

EDIT2 :最简单的解决方案,不需要算法切换到数组(某些算法根本不适合),就是改变这个:

let optimalResults = new Dictionary<_,_>()

进入这个:

let optimalResults = new Dictionary<_,_>(HashIdentity.Structural)

这一变化使F#代码的运行速度提高了2.7倍,最终击败了Python(速度提高了1.6倍)。奇怪的是,默认情况下元组 使用结构比较,所以原则上,字典对键的比较是相同的(有或没有结构)。 Harrop博士认为速度差异可能归因于虚拟调度:“AFAIK,.NET几乎无法优化虚拟调度,现代硬件上虚拟调度的成本非常高,因为它是一个”计算goto“将程序计数器跳转到一个不可预测的位置,因此破坏了分支预测逻辑,几乎肯定会导致整个CPU管道被刷新并重新加载“

简单地说,并且正如Don Syme(look at the bottom 3 answers)所建议的那样,“在将引用类型的键与.NET集合结合使用时,应明确使用结构散列”。 (Harrop博士在下面的评论中也说我们应该总是在使用.NET集合时使用结构比较。)

亲爱的F#团队,如果有办法自动解决这个问题,请做。

答案 1 :(得分:8)

正如Jon Harrop指出的那样,使用Dictionary(HashIdentity.Structural)简单地构建字典可以显着提高性能(在我的计算机上为3倍)。这几乎可以肯定是为了获得比Python更好的性能而需要进行的微创改变,并保持代码惯用(而不是用结构替换元组等)并与Python实现并行。

答案 2 :(得分:5)

编辑:我错了,这不是值类型与引用类型的问题。性能问题与散列函数有关,如其他注释中所述。我在这里保留我的答案,因为这是一个棘手的讨论。我的代码部分修复了性能问题,但这不是干净且推荐的解决方案。

-

在我的计算机上,我通过用结构替换元组,使样本运行两倍。这意味着,等效的F#代码应该比Python代码运行得更快。我不同意评论说.NET哈希表很慢,我相信与Python或其他语言实现没有显着差异。另外,我不同意“你不能一对一翻译代码期望它更快”:对于大多数任务,F#代码通常比Python快(静态类型对编译器非常有帮助)。在您的示例中,大部分时间都花在哈希表查找上,因此可以想象两种语言应该几乎一样快。

我认为性能问题与gabage收集有关(但我没有用探查器检查过)。在SO问题(Why is the new Tuple type in .Net 4.0 a reference type (class) and not a value type (struct))和MSDN页面(Building tuples)中讨论了使用元组的原因比结构更慢的原因:

  

如果它们是引用类型,那么   意味着可能有很多垃圾   如果要更改元素,则会生成   在紧密循环中的元组中。 [...]   F#元组是引用类型,但是   团队有一种感觉   他们可以实现一个表现   改进,如果两个,也许三个,   元素元组是值类型   代替。一些已创建的团队   内部元组使用了价值   参考类型,因为他们的   情景非常敏感   创建大量托管对象。

当然,正如Jon在另一篇评论中所说,你的例子中明显的优化是用数组替换哈希表。数组显然要快得多(整数索引,没有散列,没有碰撞处理,没有重新分配,更紧凑),但这非常特定于你的问题,并没有解释Python的性能差异(据我所知, Python代码使用的是哈希表,而不是数组。)

为了重现我的50%加速,这里是完整的代码:http://pastebin.com/nbYrEi5d

简而言之,我用这种类型替换了元组:

type Tup = {x: int; y: int}

此外,它看起来像一个细节,但你应该将List.mapi (fun i x -> (i,x)) fileSizes移出封闭循环。我相信Python enumerate实际上并没有分配一个列表(所以在F#中只分配一次列表,或者使用Seq模块,或使用一个可变计数器)是公平的。

答案 3 :(得分:0)

嗯..如果哈希表是主要瓶颈,那么它恰好是哈希函数本身。没有看过特定的哈希函数,而是最常见的哈希函数之一

(((a * x + b)%p)%q

模运算%非常缓慢,如果p和q的形式为2 ^ k-1,我们可以使用and进行模运算,然后加上和进行移位。

Dietzfelbingers通用哈希函数h_a:[2 ^ w]-> [2 ^ l]

lowerbound(((((a * x)%2 ^ w)/ 2 ^(w-l))

这是w位的随机奇数种子。

可以通过(a * x)>>(w-l)计算得出,它的速度比第一个哈希函数快。我必须实现带有链接列表的哈希表作为冲突处理。实施和测试花了10分钟,我们必须用两个功能对其进行测试,并分析速度差异。我记得第二个哈希函数的速度增益取决于表的大小,约为4到10倍。 但是这里要学习的是,如果您的程序瓶颈是哈希表查找,那么哈希函数也必须很快