【问题标题】:Fast bitarray in OCamlOCaml 中的快速位数组
【发布时间】:2013-01-21 19:08:53
【问题描述】:

另一个综合基准:Sieve of Eratosthenes

C++

#include <vector>
#include <cmath>

void find_primes(int n, std::vector<int>& out)
{
   std::vector<bool> is_prime(n + 1, true);
   int last = sqrt(n);
   for (int i = 2; i <= last; ++i)
   {
      if (is_prime[i])
      {
         for (int j = i * i; j <= n; j += i)
         {
            is_prime[j] = false;
         }
      }
   }

   for (unsigned i = 2; i < is_prime.size(); ++i)
   {
      if (is_prime[i])
      {
         out.push_back(i);
      }
   }
}

OCaml(使用 Jane Street's CoreRes 库)

open Core.Std
module Bits = Res.Bits
module Vect = Res.Array

let find_primes n =
  let is_prime = Bits.make (n + 1) true in
  let last = float n |! sqrt |! Float.iround_exn ~dir:`Zero in
  for i = 2 to last do
    if not (Bits.get is_prime i) then () else begin
      let j = ref (i * i) in
      while !j <= n; do
        Bits.set is_prime !j false;
        j := !j + i;
      done;
    end;
  done;
  let ar = Vect.empty () in
  for i = 2 to n do
    if Bits.get is_prime i then Vect.add_one ar i else ()
  done;
  ar

我很惊讶 OCaml 版本(本机)比 C++ 慢 13 倍。我用Core_extended.Bitarray 替换了Res.Bits,但速度变慢了~18 倍。为什么这么慢? OCaml 不为位操作提供快速操作吗?有没有其他快速实现位数组的方法?

明确一点:我来自 C++ 世界,并认为 OCaml 作为编写性能关键代码的可能替代方案。其实这样的结果我有点害怕。

编辑:

分析结果

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ms/call  ms/call  name    
 50.81      1.26     1.26                             camlRes__pos_1113
  9.72      1.50     0.24                             camlRes__unsafe_get_1117
  6.68      1.66     0.17                             camlRes__unsafe_set_1122
  6.28      1.82     0.16                             camlNopres_impl__set_1054
  6.07      1.97     0.15                             camlNopres_impl__get_1051
  5.47      2.10     0.14 47786824     0.00     0.00  caml_apply3
  3.64      2.19     0.09 22106943     0.00     0.00  caml_apply2
  2.43      2.25     0.06   817003     0.00     0.00  caml_oldify_one
  2.02      2.30     0.05        1    50.00   265.14  camlPrimes__find_primes_64139
  1.21      2.33     0.03                             camlRes__unsafe_get_1041
...

【问题讨论】:

  • 您是否对代码进行了概要分析以查看它在哪里花费时间?
  • 是的。我在 OCaml 方面还不够好,但 gprof 说该程序大部分时间都花在位数组操作上。我尝试用常规数组替换位数组,它只比 C++ 慢 3.3 倍。显然位数组是一个瓶颈。
  • 对于编译器编写者来说,生成性能关键代码并不容易,而且最糟糕的是,大多数编译器制造商(除了 Clang 的人)都希望按照自己的方式进行 :-( 。我的意见:对于性能关键代码这些天坚持(按此顺序):C++,(如果你想年轻就死,在这里插入 Java 和 Fortran),Javascript(但请阅读优化指南),带有 Ghc 的 Haskell ......体面但不完全在那里:大多数其他人都没有使用 LLVM,也没有微软/谷歌的预算。实际上,微软和谷歌的预算也不是保证。
  • 听起来很像。我很确定它可以通过更好地实现位操作优化来解决[或类似的东西]。但是我什至在 OCaml 上都达不到您的水平-我几乎看不懂您发布的代码-只是想确保您在尝试查找问题时正在查看正确的内容,在查找时“猜错”并不罕见代码慢的地方。
  • 并不是说它对性能很重要,但是你编写了奇怪的 Ocaml 代码。你知道你可以在条件语句中省略else,只要它的类型是unit?所以没有必要写else ()。我尝试使用Bigarray,但它给出的结果比gasche 的字符串解决方案稍慢。哦,那个sqrt 也很讨厌,它引入了不必要的数字错误(对于足够大的n)。

标签: c++ performance algorithm ocaml bitarray


【解决方案1】:

您是否先尝试使用简单的数据结构,然后再使用复杂的数据结构?

在我的机器上,以下代码仅比 C++ 版本慢 4 倍(请注意,我做了最小的更改以使用数组作为缓存,并使用列表来累积结果;您可以使用数组 get/set 语法糖):

let find_primes n =
  let is_prime = Array.make (n + 1) true in
  let last = int_of_float (sqrt (float n)) in
  for i = 2 to last do
    if not (Array.get is_prime i) then () else begin
      let j = ref (i * i) in
      while !j <= n; do
        Array.set is_prime !j false;
        j := !j + i;
      done;
    end;
  done;
  let ar = ref [] in
  for i = 2 to n do
    if Array.get is_prime i then ar := i :: !ar else ()
  done;
  ar

(慢 4 倍:计算 10_000_000 个第一个素数需要 4 秒,而 1 秒 对于您的代码中的 g++ -O1 或 -O2)

意识到您的位向量解决方案的效率可能 来自经济内存布局,我改代码使用 字符串而不是数组:

let find_primes n =
  let is_prime = String.make (n + 1) '0' in
  let last = int_of_float (sqrt (float n)) in
  for i = 2 to last do
    if not (String.get is_prime i = '0') then () else begin
      let j = ref (i * i) in
      while !j <= n; do
        String.set is_prime !j '1';
        j := !j + i;
      done;
    end;
  done;
  let ar = ref [] in
  for i = 2 to n do
    if String.get is_prime i = '0' then ar := i :: !ar else ()
  done;
  ar

现在只需要 2 秒,这使得它比 C++ 慢 2 倍 解决方案。

【讨论】:

  • 使用字符串是个好主意,它避免了单个整数的装箱惩罚。
  • 是的,我尝试了常规 Array 并得到了相同的结果(大约慢了 4 倍)。但是,它消耗了太多的内存。使用 String 是个好主意。无论如何,它每比特消耗 8 位,所以我无法计算,比如说,100 亿,这可以通过每比特实现来实现。
  • @JeffreyScofield 你能解释一下拳击处罚吗?整数是否装在常规数组中?
  • 他们没有装箱,但他们放弃了一点来标记这一事实。即,留出一点表示它们没有装箱。
  • @JeffreyScofield:我敢打赌,任何潜在的标记惩罚都会在这些广泛传播的访问所引发的缓存未命中噪声中丢失。我很确定字符串(或 C++ 位向量)解决方案的性能提升都归结为更经济的数据结构的新发现的缓存效率。我不知道提议的位向量结构是如何实现的,但我敢打赌,性能成本是由于函数调用(因为缺少 .cmx 阻止内联或其他原因)而不是实际代码。
【解决方案2】:

看来杰弗里斯科菲尔德是对的。这种可怕的性能下降是由于divmod 操作造成的。

我制作了小型 Bitarray 模块的原型

module Bitarray = struct
  type t = { len : int; buf : string }

  let create len x =
    let init = (if x = true then '\255' else '\000') in
    let buf = String.make (len / 8 + 1) init in
    { len = len; buf = buf }

  let get t i =
    let ch = int_of_char (t.buf.[i lsr 3]) in
    let mask = 1 lsl (i land 7) in
    (ch land mask) <> 0

  let set t i b =
    let index = i lsr 3 in
    let ch = int_of_char (t.buf.[index]) in
    let mask = 1 lsl (i land 7) in
    let new_ch = if b then (ch lor mask) else (ch land lnot mask) in
    t.buf.[index] <- char_of_int new_ch
end

它使用字符串作为字节数组(每个字符 8 位)。最初我使用x / 8x mod 8 进行位提取。它比 C++ 代码慢 10 倍。然后我将它们替换为x lsr 3x land 7。现在,它只比 C++ 慢 4 倍。

【讨论】:

    【解决方案3】:

    像这样比较微基准并不经常有用,但基本结论可能是正确的。这是 OCaml 处于明显劣势的情况。 C++ 可以访问或多或少的理想表示(机器整数向量)。 OCaml 可以生成向量,但不能直接获取机器整数。所以 OCaml 必须使用 div 和 mod,而 C++ 可以使用 shift 和 mask。

    我复制了这个测试(使用不同的位向量库),发现在 OCaml 中花费了相当多的时间来构建结果,而不是位数组。因此,该测试可能无法准确地衡量您的想法。

    更新

    我尝试了一些快速测试,将 32 个布尔值打包成 63 位整数。它似乎确实让事情进展得更快,但只是一点点。这不是一个完美的测试,但它表明 gasche 是正确的,非 2 次方效应很小。

    【讨论】:

    • 我们必须使用divmod 来在其中找到wordoffset
    • 如果你使用整个机器字,你会去divmod乘以2的幂。你可以对这些使用移位和掩码,这样更快。 OCaml 为每个机器字留出 1 位作为装箱标签。这是它的缺点(div 和 mod 31 或 63),我怀疑它是这个微基准测试中速度差异的主要原因。
    • 好的,知道了!由于标签位,它似乎真的很慢。
    【解决方案4】:

    请确保安装包含 .cmx 文件的 Core(.cmxa 还不够!),否则跨模块内联将不起作用。您的个人资料表明某些调用可能没有被内联,这可以解释效率的巨大损失。

    遗憾的是,许多 OCaml 项目使用的 Oasis 打包工具目前存在一个错误,导致它无法安装 .cmx 文件。 Core 包也受到此问题的影响,可能与您使用的包管理器(Opam、Godi)无关。

    【讨论】:

      猜你喜欢
      • 2014-12-10
      • 2011-02-27
      • 2011-09-29
      • 2012-04-16
      • 2020-06-03
      • 1970-01-01
      • 2013-06-06
      • 2013-01-31
      • 1970-01-01
      相关资源
      最近更新 更多