多年前,我通过动态编程解决了一个问题:
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
答案 0 :(得分:47)
问题很简单,该程序已针对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倍的速度运行。
故事的道德:
谢谢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倍。 但是这里要学习的是,如果您的程序瓶颈是哈希表查找,那么哈希函数也必须很快