FSharp运行我的algorithm比Python慢
几年前,我通过dynamic编程解决了一个问题:
https://www.thanassis.space/fillupDVD.html
该解决scheme是用Python编码的。
作为扩展我的视野的一部分,我最近开始学习OCaml / F#。 有什么更好的方法来testing水域,而不是通过直接移植我在Python中编写的命令代码到F#,然后从那里开始,逐步向function性编程解决scheme迈进。
这第一个直接港口的结果是令人不安的:
在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
(如果你注意到上面的“单声道”:我也在Windows下testing,与Visual Studio – 相同的速度)。
我很困惑,至less可以说。 Python比F#更快地运行代码? 使用.NET运行库的已编译二进制文件比Python的解释代码运行SLOWER?!?!
我知道VM的启动成本(在这种情况下是单声道),以及JIT如何改进像Python这样的语言的东西,但仍然…我期望加速,而不是放缓!
我可能做错了吗?
我已经在这里上传了代码:
https://www.thanassis.space/fsharp.slower.than.python.tar.gz
请注意,F#代码或多或less是对Python代码的直接逐行转换。
PS当然还有其他的收益,例如F#提供的静态types安全性,但是如果F#下的命令式algorithm的结果速度更糟……我很失望,至less可以说。
编辑 :直接访问,在评论中的要求:
Python代码: https : //gist.github.com/950697
FSharp代码: https : //gist.github.com/950699
我通过电子邮件联系的Jon Harrop博士解释了发生了什么事情:
问题很简单,该程序已针对Python进行了优化。 当程序员比其他人更熟悉一种语言时,这是很常见的。 你只需要学习一套不同的规则来决定如何优化F#程序……有几件事情在我身上跳出来,比如使用“for i in 1..n do”循环,而不是“for i = 1到n做“循环(这是一般较快,但在这里不重要),反复做一个列表List.mapi模仿一个数组索引(不必要地分配中间名单)和您使用F#TryGetValue分配字典不必要地(接受ref的.NET TryGetValue总体来说速度更快,但在这里并不那么多)
…但真正的杀手问题原来是你使用哈希表来实现密集的2Dmatrix。 使用散列表在Python中是理想的,因为它的散列表实现已经非常好的优化了(就像你的Python代码运行速度与F#编译为本地代码一样快),但是数组是一种更好的方式来表示密集matrix,特别是当你想要一个默认值为零时。
有趣的是,当我第一次编写这个algorithm时,我使用了一个表格 – 为了清晰起见,我将实现改为了字典(避免了数组边界检查使代码更简单 – 而且更容易推理)。
乔恩转换我的代码(回:-))到它的数组版本 ,并以100倍的速度运行。
故事的道德启示:
- F#字典需要工作…当使用元组作为键时,编译的F#比解释Python的哈希表要慢!
- 显而易见,但重复无害:更清洁的代码有时意味着更慢的代码。
谢谢Jon,非常感谢。
编辑 :事实上,用数组replace字典使F#终于运行在一个编译语言预计运行的速度,并不否定字典的速度(我希望F#人正在阅读这个)修复的需要。 其他algorithm依赖于字典/散列,不能轻易切换到使用数组; 无论何时使用字典,制作程序都会遇到“解释速度”,可以说是一个错误。 如果正如一些人在评论中所说的那样,问题不在于F#而是在.NET Dictionary中,那么我认为这是.NET中的一个错误!
编辑2 :最清晰的解决scheme,不需要algorithm切换到数组(有些algorithm根本不会顺从)是改变这一点:
let optimalResults = new Dictionary<_,_>()
进入这个:
let optimalResults = new Dictionary<_,_>(HashIdentity.Structural)
这一变化使得F#代码运行速度提高了2.7倍,从而最终击败了Python(速度提高了1.6倍)。 奇怪的是,元组默认情况下使用了结构比较,所以原则上键上的字典的比较是相同的(有或没有结构)。 哈罗普博士认为速度差异可归因于虚拟调度: “AFAIK,.NET在虚拟调度的优化方面做得不多,在现代硬件上虚拟调度的成本非常高,因为它是跳转程序的”计算转换“与不可预测的位置相对立,因此破坏了分支预测逻辑,并且几乎肯定会导致整个CPUstream水线被刷新和重新加载“ 。
简而言之,正如Don Syme( 请看底部的3个答案 )所build议的那样,“在使用引用键和.NET集合时,要明确使用结构化哈希”。 (哈罗普博士在下面的评论中也表示,我们应该总是使用.NET集合时的结构比较)。
亲爱的F#团队,如果有办法自动修复这个,请做。
正如Jon Harrop指出的那样,使用Dictionary(HashIdentity.Structural)
构build字典可以大大提高性能(在我的计算机上是3的倍数)。 这几乎可以肯定是为了获得比Python更好的性能而需要做的微创改变,并且保持你的代码惯用(而不是用结构replace元组等等)并且与Python实现并行。
编辑:我错了,这不是一个值types与引用types的问题。 性能问题与哈希函数有关,正如其他评论中所解释的那样。 我保留我的答案,因为有一个interentant讨论。 我的代码部分解决了性能问题,但这不是一个干净和推荐的解决scheme。
–
在我的计算机上,我通过用一个结构体replace元组,使您的示例运行速度提高了一倍 。 这意味着,等价的F#代码应该比你的Python代码运行得更快。 我不同意这样的评论,说.NET的哈希表是慢的,我相信与Python或其他语言的实现没有显着的差异。 另外,我不同意“你不能一对一翻译代码,希望它更快”:对于大多数任务来说,F#代码通常比Python更快(静态types对编译器非常有帮助)。 在你的示例中,大部分时间都花在做散列表查找上,所以可以想象,这两种语言应该几乎一样快。
我认为性能问题与gabage收集(但我没有与一个分析器检查)有关。 在一个SO问题( 为什么.Net 4.0中的新Tupletypes是一个引用types(类)而不是一个值types(struct) )和一个MSDN页面( Building元组 ):
如果它们是引用types,这意味着如果要在紧凑循环中更改元组中的元素,则可能会产生大量垃圾。 F#元组是参考types,但是团队感觉如果两个,也许三个元组元素是值types,他们可以实现性能改进。 一些创build了内部元组的团队使用了值而不是引用types,因为他们的场景对创build大量的pipe理对象非常敏感。
当然,正如Jon在另一个评论中所说的那样,在你的例子中明显的优化是用数组replacehashtable。 数组显然要快得多(整数索引,没有散列,没有碰撞处理,没有重新分配,更紧凑),但是这是非常具体的问题,并没有解释与Python的性能差异(据我所知, Python代码使用哈希表,而不是数组)。
重现我的50%加速,这是完整的代码: http : //pastebin.com/nbYrEi5d
总之,我用这种typesreplace了元组:
type Tup = {x: int; y: int}
另外,它看起来像一个细节,但你应该将List.mapi (fun ix -> (i,x)) fileSizes
移出封闭循环。 我相信Python enumerate
实际上并没有分配一个列表(因此在F#中只分配一次列表是公平的,或者使用Seq
模块,或使用可变计数器)。