FSharp 运行我的算法比 Python 慢

Posted

技术标签:

【中文标题】FSharp 运行我的算法比 Python 慢【英文标题】:FSharp runs my algorithm slower than Python 【发布时间】:2011-08-16 13:17:27 【问题描述】:

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

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

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

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

我知道虚拟机的启动成本(在这种情况下是单声道)以及 JIT 如何改进 Python 等语言的性能,但仍然......我预计会加速,而不是减速!

也许我做错了什么?

我已经在这里上传了代码:

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

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

附:当然还有其他收获,例如F# 提供的静态类型安全性 - 但如果在 F# 下命令式算法的结果速度更差……至少可以这么说,我很失望。

EDIT:直接访问,根据 cmets 的要求:

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

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

【问题讨论】:

请使用gist.github.com 之类的方式上传您的代码...下载 tar.gz 文件才能查看您的代码真的很糟糕 这是神话,都是神话。它不是编译的更快,不是解释的更快,不是原生的更快,或者jit的更快。只有更快才是更快。以此为生。 我没有 Python 来测试它,但在我的 Intel Core 2 Duo CPU (2.26 GHz) 上,F# 版本在大约 1.5 秒内完成(在 Windows 上,使用 fsi.exe 和 @987654329 @时间)。但是,我不会尝试理解您的代码 - 我认为如果您发布一些您尝试优化的简单 F# 代码,您将更有可能得到有用的答案(因为不是每个人都想分析您的两个示例)。 另外,从 Python 中逐行翻译代码是开始探索 F# 语法的好方法,但它并没有真正向您展示 F# 的任何好处。我相信如果您尝试使用更惯用的函数式样式来解决问题,您会获得更多乐趣(它可能不会更快,但它很可能更具可读性和更短)。 在我的机器上,Python 运行时间为 1.2 秒,F# 版本运行时间为 1.8 秒。这个基准测试可能表明,Python 有一个出色的字典实现,可能优化了对作为键。 【参考方案1】:

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

问题只是程序已针对 Python 进行了优化。当然,当程序员对一种语言比另一种更熟悉时,这种情况很常见。你只需要学习一组不同的规则,这些规则决定了 F# 程序应该如何优化...... 有几件事突然出现在我身上,例如使用“for i in 1..n do”循环而不是“for i=1 to n do”循环(这通常更快,但在这里并不重要),反复做列表上的 List.mapi 以模仿数组索引(它不必要地分配了中间列表)以及您使用 F# TryGetValue for Dictionary 进行不必要的分配(接受 ref 的 .NET TryGetValue 通常更快,但在这里没有那么多)

...但真正的杀手问题是您使用哈希表来实现密集的 2D 矩阵。在 Python 中使用哈希表是理想的,因为它的哈希表实现已经得到了非常好的优化(这一点可以从您的 Python 代码运行速度与编译为本机代码的 F# 一样快的事实证明!)但是数组是表示密集的更好方法矩阵,特别是当您希望默认值为零时。

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

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

故事的寓意:

F# 字典需要工作...当使用元组作为键时,编译的 F# 比解释 Python 的哈希表慢! 很明显,但重复也无妨:更简洁的代码有时意味着……更慢的代码。

谢谢你,乔恩——非常感谢。

编辑:将 Dictionary 替换为 Array 使得 F# 最终以编译语言预期运行的速度运行这一事实,并不否定修复 Dictionary 速度的需要(我希望 F#来自 MS 的人正在阅读这篇文章)。其他算法依赖于字典/哈希,不能轻易切换到使用数组;每当使用字典时,使程序遭受“解释器速度”,可以说是一个错误。如果正如某些人在 cmets 中所说的那样,问题不在于 F#,而在于 .NET 字典,那么我认为这……是 .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 集合结合使用时,要明确说明结构散列的使用”。 (下面 cmets 中的 Harrop 博士还说,在使用 .NET 集合时,我们应该始终使用结构比较)。

尊敬的 MS 中的 F# 团队,如果有办法自动解决此问题,请执行。

【讨论】:

注意:1. F#字典只是.NET字典。 2. Python字典不是用Python实现的(可能是C)。 显然使用Dictionary(HashIdentity.Structural) 使它更快(可能比 Python 更快)。用结构替换元组(堆分配的)也应该显着提高性能。顺便说一句,如果可以的话,我认为你也应该接受这个答案。 @ttsiodras:我不遵循你的逻辑。您的 Python 击败您的 F# 的唯一 原因是您忘记提供应该在 F# 中始终 提供的 HashIdentity.Structural 相等比较器。只需进行一项微小的更改,F# 就比您的 Python 更快。如果您随后使用结构而不是元组并使用 .NET TryGetValue 而不是 F# 扩展方法并预先调整哈希表大小,那么 F# 将比以前快 7 倍,这比您的 Python 快几倍。所以你不能断定Dictionary 效率低下。 @kvb, @Jon:我用谷歌搜索了很多,发现了这个:cs.hubfs.net/forums/thread/654.aspx(导航到底部)。 Don Syme 明确承认,对于元组,F#应该使用默认的结构比较,就像 Python 一样。他说“我们会在 2006 年将它添加到我们的列表中”,但 5 年后,显然这还没有出现……而且,有趣的是,“这对于来自其他语言和也可能导致更大代码库中的细微错误。”。是的,确实:-) @ttsiodras - 实际上,元组类型从那时起发生了变化,因此默认的相等和散列行为按预期工作。也就是说,该线程中多次调用(1, 2, 3).GetHashCode() 得到不同结果的示例不再发生。只是内置相等和散列运算的性能特征不如使用HashIdentity.Structural快。【参考方案2】:

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

【讨论】:

【参考方案3】:

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

--

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

我认为性能问题与垃圾收集有关(但我尚未使用分析器进行检查)。在 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# 元组是引用类型,但是 团队有一种感觉 他们可以实现表演 如果两个,也许三个,改进, 元素元组是值类型 反而。一些创建过的团队 内部元组使用了 value 引用类型,因为它们的 场景非常敏感 创建大量托管对象。

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

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

简而言之,我将元组替换为这种类型:

type Tup = x: int; y: int

此外,这似乎是一个细节,但您应该将List.mapi (fun i x -&gt; (i,x)) fileSizes 移出封闭循环。我相信 Python enumerate 实际上并没有分配列表(因此在 F# 中只分配一次列表,或者使用 Seq 模块,或者使用可变计数器是公平的)。

【讨论】:

关于您的 List.mapi 评论:我确实尝试过我自己的基于“seq”的枚举:let enumerate c = seq let idx = ref 0 for elem in c do yield (!idx, elem) idx := !idx + 1 ...但它没有影响 - 速度仍然很慢。正如我上面所说,原来罪魁祸首是 .NET Dictionary 的糟糕性能...... @ttsiodras:我不这么认为。通过我的更改,代码比 Python 实现要快一点,这意味着字典在 .NET 中并没有那么慢。当然,当您知道索引时,数组比哈希表快得多,但是您正在更改算法。 @ttsiodras - 结构标识是底层 .NET 框架完全不知道的 F# 概念,因此 .NET 字典类型不能将其用作默认值。 在当前版本的 F# 中真的还有获得引用相等的危险吗?字典中的元组键似乎并非如此。结构 F# 类型是否会在其他情况下发生? @wmeyer - 元组根据其组成值的相等性和散列定义它们的相等性和散列。如果它们的值的相等和散列已经是结构的,那么元组也将具有结构相等和散列(例如int*int)。但是,如果它们的值不是结构化的,那么元组的相等性和散列也不会是结构化的(例如 int[]*int[])。 F# 的 = 运算符和 hash 函数即使在这些类型上也会在结构上运行,HashIdentity.Structural 相等比较器也是如此。【参考方案4】:

嗯.. 如果哈希表是主要瓶颈,那么它就是哈希函数本身。没有看具体的哈希函数,但是对于最常见的哈希函数之一,即

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

模数运算 % 非常慢,如果 p 和 q 的形式为 2^k - 1,我们可以使用与、加法和移位运算进行模数。

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

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

w-bit 的随机奇数种子在哪里。

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

【讨论】:

以上是关于FSharp 运行我的算法比 Python 慢的主要内容,如果未能解决你的问题,请参考以下文章

Python(算法)-时间复杂度和空间复杂度

算法基础

算法的基本概念

算法——基础知识

时间复杂度和空间复杂度

时间复杂度和空间复杂度