几年前,我通过动态编程解决了一个问题:
https://www.thanassis.space/fillupDVD.html
解决方案是用Python编写的。
作为拓展视野的一部分,我最近开始学习OCaml/F。有什么比直接将我用Python编写的命令代码移植到F#来测试waters更好的方法呢?从这里开始,逐步走向函数式编程解决方案。
第一个结果,直接端口。。。令人不安:
在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运行时编译的二进制文件比Python的解释代码运行得慢?!?!
我知道VMs(本例中是mono)的启动成本,以及jit如何改进Python之类的语言,但仍然。。。我期待的是加速,而不是减速!
我可能做错什么了吗?
我已经在这里上传了代码:
https://www.thanassis.space/fsharp.slower.than.python.tar.gz
注意,F#代码或多或少是Python代码的直接逐行翻译。
当然还有其他好处,例如F#提供的静态类型安全性——但是如果命令式算法的结果速度在F#下更差。。。至少可以说,我很失望。
编辑:根据评论中的要求直接访问:
Python代码:https://gist.github.com/950697
FSharp代码:https://gist.github.com/950699
我通过电子邮件联系了乔恩·哈罗普医生,他解释了发生的事情:
有趣的是,当我第一次编码这个算法时,我确实使用了一个表——为了清晰起见,我将实现改为字典(避免数组边界检查使代码更简单,也更容易推理)。
Jon将我的代码(back:-)转换为它的array version,它以100倍的速度运行。
故事的寓意:
谢谢你,乔恩——非常感谢。
编辑:事实上,用数组替换字典使F最终以编译语言预期的运行速度运行,这并不意味着需要修正字典的速度(我希望来自MS的F#人正在阅读这个)。其他的算法依赖于字典/散列,并且不能很容易地切换到使用数组;使得程序在使用字典时遭受“解释器速度”的影响,可以说是一个bug。如果,正如一些人在评论中所说,问题不在于F,而在于.NET字典,那么我认为。。。是.NET中的一个bug!
EDIT2:最清晰的解决方案是,不需要算法切换到数组(有些算法根本不适合这样做)来更改:
进入这个:
这一变化使F#代码的运行速度提高了2.7倍,从而最终击败了Python(提高了1.6倍)。奇怪的是元组默认情况下使用结构比较,因此原则上,字典对键进行的比较是相同的(有或没有结构)。Harrop博士认为,速度差可能是由于虚拟调度造成的:“AFAIK,.NET对优化虚拟调度几乎没有作用,而且在现代硬件上,虚拟调度的成本极高,因为它是一个“计算的goto”,将程序计数器跳到一个不可预测的位置,因此,破坏分支预测逻辑,几乎肯定会导致整个CPU管道被刷新和重新加载“。
简单地说,正如Don Syme(look at the bottom 3 answers)所建议的,“在将引用类型的键与.NET集合结合使用时,要明确使用结构哈希”。(Harrop博士在下面的评论中还说,在使用.NET集合时,我们应该始终使用结构比较)。
亲爱的微软团队,如果有办法自动解决这个问题,请这样做。
正如Jon Harrop所指出的,简单地使用
Dictionary(HashIdentity.Structural)
构造字典可以显著提高性能(在我的计算机上是3倍)。这几乎可以肯定是为了获得比Python更好的性能而需要进行的微创更改,并使代码保持习惯性(而不是用结构等替换元组),并与Python实现并行。编辑:我错了,这不是值类型对引用类型的问题。性能问题与散列函数有关,如其他注释中所述。我把答案留在这里是因为有一个有趣的讨论。我的代码部分修复了性能问题,但这不是一个干净和推荐的解决方案。
--
在我的计算机上,我用一个结构替换元组,使您的示例运行速度比运行速度快两倍。这意味着,等价的F#代码应该比Python代码运行得更快。我不同意那些说.NET哈希表速度慢的评论,我相信与Python或其他语言实现没有显著的区别。另外,我不同意“你不能期望1对1的转换代码更快”:对于大多数任务,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)中讨论过了:
当然,正如Jon在另一条评论中所说,示例中明显的优化是用数组替换哈希表。数组显然要快得多(整数索引、无散列、无冲突处理、无重新分配、更紧凑),但这是针对您的问题的,它不能解释与Python的性能差异(据我所知,Python代码使用的是散列表,而不是数组)。
为了重现我50%的加速,这里是完整的代码:http://pastebin.com/nbYrEi5d
简而言之,我用这种类型替换了元组:
而且,它看起来像一个细节,但是您应该将
List.mapi (fun i x -> (i,x)) fileSizes
移出封闭循环。我相信Pythonenumerate
实际上并不分配列表(因此在F#中只分配一次列表,或者使用Seq
模块,或者使用可变计数器是公平的)。相关问题 更多 >
编程相关推荐