【问题标题】:Simple π(x) in Haskell vs C++Haskell 与 C++ 中的简单 π(x)
【发布时间】:2014-02-19 22:45:46
【问题描述】:

我正在学习 Haskell。我的兴趣是将它用于个人计算机实验。现在,我正在尝试看看 Haskell 的速度有多快。许多人声称与 C(++) 相当,如果这是真的,我会非常高兴(我应该注意,我将使用 Haskell,无论它是否快速,但快速仍然是一件好事)。

我的测试程序用一个非常简单的算法实现了 π(x):素数将结果加 1。素数在 1 和 √x 之间没有整数除数。这不是一场算法大战,这纯粹是为了编译器性能。

Haskell 在我的计算机上似乎慢了大约 6 倍,这很好(仍然比纯 Python 快 100 倍),但这可能只是因为我是 Haskell 新手。

现在,我的问题是:如何在不改变算法的情况下优化 Haskell 实现? Haskell 的性能真的与 C 相当吗?

这是我的Haskell 代码:

import System.Environment

-- a simple integer square root
isqrt :: Int -> Int
isqrt = floor . sqrt . fromIntegral

-- primality test        
prime :: Int -> Bool
prime x = null [x | q <- [3, 5..isqrt x], rem x q == 0]

main = do
  n <- fmap (read . head) getArgs
  print $ length $ filter prime (2:[3, 5..n])

这是我的C++ 代码:

#include <iostream>
#include <cmath>
#include <cstdlib>
using namespace std;

bool isPrime(int);

int main(int argc, char* argv[]) {
    int primes = 10000, count = 0;
    if (argc > 1) {
        primes = atoi(argv[1]);
    }
    if (isPrime(2)) {
        count++;
    }
    for (int i = 3; i <= primes; i+=2) {
        if (isPrime(i)){
            count++;
        }
    }
    cout << count << endl;
    return 0;
}

bool isPrime(int x){
    for (int i = 2; i <= floor(sqrt(x)); i++) {
        if (x % i == 0) {
            return false;
        }
    }
    return true;
}

【问题讨论】:

  • 你编译你的代码优化了吗?
  • 在使用 GHC 编译时,您需要使用 -O2。 LLVM 位应该与它没有太大关系(不完全确定,我从未将它主要用作 Windows 开发人员)。
  • 因此,您通过运行没有数据结构甚至任何不可预测分支的程序,将函数式编程系统与具有显式内存管理的命令式系统进行比较。对了,别忘了-fprofile-generate-fprofile-use
  • 顺便说一句,当我看到那些 sqrt/floor 计算时,我的眼睛很痛。检查q &lt;= sqrt(x) 是否可以表达q*q &lt;= x 所以你有它:一个整数乘法与几个浮点指令。
  • @Ingo: ... 其中一个是完全不需要的 (i

标签: c++ performance haskell


【解决方案1】:

您的 Haskell 版本正在 prime 中构建一个惰性列表,仅用于测试它是否为空。这似乎确实是一个瓶颈。以下版本在我的机器上运行速度与 C++ 版本一样快:

prime :: Int -> Bool
prime x = go 3
  where
    go q | q <= isqrt x = if rem x q == 0 then False else go (q+2)
    go _  = True

使用 -O2 编译时为 3.31 秒,而使用 gcc 4.8 和 -O3 编译时使用 gcc 4.8 时为 3.18 秒。

当然,“猜测”程序优化慢的地方并不是一个很好的方法。幸运的是,Haskell 有很好的分析工具。

编译运行

$ ghc --make primes.hs -O2 -prof -auto-all -fforce-recomp && ./primes 5000000 +RTS -p

给予

# primes.prof
  Thu Feb 20 00:49 2014 Time and Allocation Profiling Report  (Final)

     primes +RTS -p -RTS 5000000

  total time  =        5.71 secs   (5710 ticks @ 1000 us, 1 processor)
  total alloc = 259,580,976 bytes  (excludes profiling overheads)

COST CENTRE MODULE  %time %alloc

prime.go    Main     96.4    0.0
main        Main      2.0   84.6
isqrt       Main      0.9   15.4

                                                      individual     inherited
COST CENTRE MODULE                  no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                     45           0    0.0    0.0   100.0  100.0
 main       Main                     91           0    2.0   84.6   100.0  100.0
  prime     Main                     92     2500000    0.7    0.0    98.0   15.4
   prime.go Main                     93   326103491   96.4    0.0    97.3   15.4
    isqrt   Main                     95           0    0.9   15.4     0.9   15.4

--- >8 ---

这清楚地表明prime 是事情变热的地方。有关分析的更多信息,我将向您推荐Real World Haskell, Chap 25

要真正了解发生了什么,您可以查看(一种)GHC 的中间语言Core,它将向您展示优化后代码的样子。一些好的信息是at the Haskell wiki。除非必要,否则我不建议这样做,但很高兴知道存在这种可能性。

关于您的其他问题:

1) 如何在不改变算法的情况下优化 Haskell 实现?

配置文件,并尝试编写内部循环,以便它们不会进行任何内存分配,并且可以由编译器进行严格限制。这样做需要一些练习和经验。

2) Haskell 的性能真的与 C 相当吗?

这取决于。 GHC 很神奇,通常可以很好地优化你的程序。如果您知道自己在做什么,通常可以接近优化 C 的性能(C 速度的 100% - 200%)。也就是说,这些优化并不总是容易或看起来很漂亮,高级 Haskell 可能会更慢。但是不要忘记,在使用 Haskell 时,您会获得惊人的表现力和高级抽象。它通常对于除性能最关键的应用程序之外的所有应用程序来说都足够快,即使这样,您也可以通过一些分析和性能优化来非常接近 C。

【讨论】:

  • 哇!感谢您提供的所有信息。 (特别是分析部分)。我确实尝试了尾递归版本,但由于某种原因它真的很慢(我使用多个参数来携带变量)。我的 C++ (4.8.2) 真的很快吗?它仍然比 Haskell 的速度快 1.5 倍,尽管我自己很高兴。我不会很快放弃表现力,我只是想看看它有多痛。 :) 我会看看其他人是否有什么要补充的……如果没有,你的答案就是公认的。
  • @PythonNut:如果你在启用分析的情况下运行它,它当然会更慢。根据我的经验,您可以非常接近(+ 50-100%),但有时很难/不可能实现平价。取决于你的经验,我不是这里的绝对专家。但是,在您和性能之间存在的问题是,这些富有表现力的语言可以让人很容易使用不会像准系统循环那样快的高级构造。惰性列表就是一个很好的例子。不过,大多数时候这并不重要,而且您的速度仍然不错。
  • 我认为prime 的问题与其说是懒惰不如说是拆箱。使用@Paul 的prime,ghc 会生成一个未装箱的内部循环。使用惰性列表,内部循环非常好,但它适用于装箱的整数,因此必须在每一步取消装箱。顺便说一句,通过将原始 prime 定义更改为使用未装箱的向量而不是列表,我获得了与递归 prime 相同的性能。
  • 您尝试过V.null $ V.filter (\q -&gt; rem x q == 0) $ V.iterateN (quot (isqrt x) 2) (+2) $ 3 吗?看来你可能会赢一点generate
  • 使用 Criterion 检查,带有 iterateN 和正确数量元素的版本与循环一样快,所以 V.null . V.filter ((== 0) . (rem x)) . V.iterateN ((isqrt x - 1) `quot` 2) (+2) $ 3
【解决方案2】:

我不认为 Haskell 版本(原始版本和第一个答案改进)与 C++ 版本等效。 原因是这样的: 两者都只考虑每隔一个元素(在prime函数中),而C++版本扫描每个元素(在isPrime()函数中只考虑i++。

当我修复这个问题时(在 C++ 的 isPrime() 函数中将 i++ 更改为 i+=2),我的运行时间几乎减少到优化 Haskell 版本(2.1s C++ vs 6s Haskell)的 1/3。

两者的输出保持不变(当然)。 请注意,这不是对 C++ 版本的具体优化,只是对 Haskell 版本中已经应用的技巧的改编。

【讨论】:

  • 我也必须从 3 开始,与两个 Hasell 版本相同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多