【问题标题】:F# code optimization or is it really that slow?F# 代码优化还是真的那么慢?
【发布时间】:2012-02-07 20:47:56
【问题描述】:

我一直在寻找一种使用 .NET 进行适当算法编码的方法,该方法具有现代语言的所有优点(例如,我喜欢强类型检查、运算符重载、lambdas、泛型算法)。通常我用 C++ 编写我的算法(主要是图像处理)。由于 F# 作为一种语言似乎很有趣,所以我玩了一下,但似乎很慢。作为一个最简单的测试,我只是做了一些数组操作 -> 图像的亮度增加:

let r1 = rgbPixels |> Array.map (fun x -> x + byte(10) )

这似乎比比较的 C++ 实现慢至少 8 倍——对于更复杂的算法来说更糟,例如二维卷积。 有没有更快的方法或者我错过了任何特定的编译器设置(是的,构建带有优化的版本......)? 我愿意为好的和高的抽象付费,但这样的开销并不好(我需要在 8 个内核上并行化以补偿:)) - 至少它破坏了进一步学习的动力......我的另一个选择将把我较重的算法留在 C++ 中并与托管 C++ 接口,但这并不好,因为维护托管包装器将是一个相当大的负担。

【问题讨论】:

  • 它可能会被内联,但您可以先用10uy 替换对byte 的调用。
  • 您能否在ideone 上发布您的 C++ 实现,以便我们可以使用?
  • these results 是否与您的相媲美?
  • 我引用的 C++ 代码也来自我一段时间前写的代码项目文章:codeproject.com/Articles/216143/… 简而言之:在 C++ 中,我编写了某种遍历原始图像数据数组的迭代器 -非常简单:就像在 F# 中一样遍历所有元素...
  • @Daniel,是的。如果我用相同数量的像素(2000x3000 24Bit jpg)替换 10000 的数量,我会得到完全相同的结果(342 毫秒) - 与你的示例相同,我接近 0 毫秒

标签: c++ .net f#


【解决方案1】:

如果您担心性能,要记住的重要事项之一是 F# 默认情况下不会改变任何内容。这需要复制许多简单的算法实现,例如您描述的算法。

编辑:我不知道为什么,但是对以下代码的简单测试提供的结果不如Array.map。在执行这些类型的优化时,请务必分析您尝试的任何算法。但是我在formap 之间得到了非常相似的结果。

Array.map 为运算结果创建一个新数组,而不是您想要Array.iteri

rgbPixels |> Array.iteri (fun i x -> rgbPixels.[i] <- x + 10uy)

请注意,这可以包含在您自己的模块中,如下所示

module ArrayM =
    let map f a = a |> Array.iteri (fun i x -> a.[i] <- f x)

不幸的是,这是一个必要的邪恶,因为函数式编程的主要租户之一是在算法允许的范围内尽可能多地坚持不可变对象,然后在完成后更改为性能至关重要的突变。如果您从一开始就知道自己的表现至关重要,那么您将需要从这类助手开始。

另外请注意,可能有一个库可以提供此功能,我只是暂时不知道。

【讨论】:

  • 或者,甚至更快:for i = 0 to rgbPixels.Length-1 do rgbPixels.[i] &lt;- rgbPixels.[i] + 10uy.
  • @Daniel:我试图尽可能接近原始代码。无论如何,我会假设该函数是由 JIT 内联的。
  • @Guvante:我用 FSI 进行了测试:循环花费的时间不到一半……可能是由于没有边界检查,正如 kvb 提到的那样。
  • @TheBigW:CLR 仅在非常特定的情况下删除边界检查。您可能需要检查this question 并确保您的代码符合要求。 for 循环肯定是最快的。
  • @TheBigW:请问您是如何安排这个时间的?您必须小心避免意外计算 JITing 和 CLR 加载时间,这会分别影响像这样的小型测试。例如,如果您的样本的总成本为 150 毫秒,那将导致 20% 的非常小的开销(对于简单的情况,对于更复杂的情况可能会更少或更多)
【解决方案2】:

我认为可以肯定地说,惯用的 F# 通常无法匹配优化的 C++ 的数组操作性能,原因如下:

  1. 根据 .NET 中的数组边界检查数组访问,以确保内存安全。 CLR 的 JIT 编译器能够省略对某些典型代码的边界检查,但这通常需要您使用具有显式边界的 for 循环,而不是更惯用的 F# 构造。
  2. 使用 lambdas 之类的抽象通常会产生少量开销(例如 fun i -&gt; ...)。在紧密循环中,与在循环体中完成的工作相比,这个小开销最终可能会非常重要。
  3. 据我所知,CLR JIT 编译器利用 SSE 指令的程度不如 C++ 编译器。

在账本的另一边,

  1. 您永远不会在 F# 代码中出现缓冲区溢出。
  2. 您的代码将更易于推理。因此,对于给定的总代码复杂度,您通常可以在 F# 中实现比在 C++ 中更复杂的算法。
  3. 如有必要,您可以编写以接近 C++ 的速度运行的单一代码,如果您发现需要编写 C++ 以满足您的性能要求,还可以与不安全的 C++ 代码进行互操作。

【讨论】:

  • 1.除了你在 Array.map 2 中有这个循环。这可以通过内联 3 来避免。另一方面,函数式语言中的并行化要容易得多(如添加单个附加函数调用),并且可以以类似的方式在技术上添加向量操作.
  • 我完全同意以上所有观点,我绝对愿意牺牲一些性能(这是我开始使用 F# 的动机),但上面的测量结果显示一个简单操作的系数为 5 (这就是为什么我猜测我做错了什么)。来到并行化:如果我从 5 倍开始,我会让 5 个核心保持忙碌,而这可能仍然只是我之前在 C++ 中使用一个核心时的状态......
  • @Maciej - 对于需要写入数组的代码,Array 模块中的任何高阶函数都不等同于 for 循环(即在 arr |&gt; Array.iteri (fun i v -&gt; arr.[i] &lt;- v + 1) 中检查阅读将被省略,但在每次迭代时仍会检查写入)。
  • @Maciej - 关于内联,在理论上,尚不清楚是否有一种易于处理的通用方法来确定内联何时会盈利 - 有时代码膨胀实际上会降低性能。在实践层面上,您可以很容易地观察到 F# 编译器并不总是(曾经?)将参数内联到 Array.iteri 和朋友。
  • @TheBigW:我非常喜欢 F#,并且很少建议不要使用它,但它可能它不是适合每个的工具> 工作。
猜你喜欢
  • 2021-10-30
  • 1970-01-01
  • 2011-11-12
  • 1970-01-01
  • 2012-02-13
  • 2020-06-09
  • 1970-01-01
  • 1970-01-01
  • 2011-08-31
相关资源
最近更新 更多