OCaml中的有效求和

时间:2014-05-06 08:33:56

标签: performance ocaml pi

请注意我几乎是OCaml的新手。为了学习一点并测试其性能,我尝试使用Leibniz series实现一个近似Pi的模块。

我的第一次尝试导致堆栈溢出(实际错误,而不是此站点)。从Haskell知道这可能来自太多" thunks"或承诺计算某些东西,同时递归加法,我寻找一些方法来保持最后的结果,同时与下一个相加。我在OCaml课程的注释herehere中找到了summap的以下尾递归实现,并期望编译器产生有效的结果。

但是,使用ocamlopt编译的结果可执行文件比使用clang++编译的C ++版本慢得多。这段代码是否尽可能高效?我缺少一些优化标志吗?

我的完整代码是:

let (--) i j =
  let rec aux n acc =
    if n < i then acc else aux (n-1) (n :: acc)
    in aux j [];;


let sum_list_tr l =
  let rec helper a l = match l with
    | [] -> a
    | h :: t -> helper (a +. h) t
  in helper 0. l


let rec tailmap f l a = match l with
  | [] -> a
  | h :: t -> tailmap f t (f h :: a);;


let rev l =
    let rec helper l a = match l with
      | [] -> a
      | h :: t -> helper t (h :: a)
    in helper l [];;


let efficient_map f l = rev (tailmap f l []);;


let summand n =
  let m = float_of_int n
  in (-1.) ** m /. (2. *. m +. 1.);;


let pi_approx n =
  4. *. sum_list_tr (efficient_map summand (0 -- n));;


let n = int_of_string Sys.argv.(1);;
Printf.printf "%F\n" (pi_approx n);;

仅供参考,以下是我机器上的测量时间:

❯❯❯ time ocaml/main 10000000
3.14159275359
ocaml/main 10000000  3,33s user 0,30s system 99% cpu 3,625 total

❯❯❯ time cpp/main 10000000
3.14159
cpp/main 10000000  0,17s user 0,00s system 99% cpu 0,174 total

为了完整性,让我说明第一个辅助函数,相当于Python的range,来自from this SO thread,并且这是使用OCaml版本4.01.0运行的,通过达尔文13.1.0上的MacPorts。

3 个答案:

答案 0 :(得分:7)

正如我在评论中所指出的那样,OCaml的float被装箱,这使得OCaml与Clang相比处于劣势。

然而,我可能会注意到在Haskell之后尝试OCaml的另一个典型的粗糙边缘: 如果我看到你的程序正在做什么,你就是在创建一个东西列表,然后在该列表上映射一个函数,最后将它折叠成一个结果。

在Haskell中,您可能或多或少地期望这样的程序在编译时自动“deforested”,这样生成的代码就可以有效地实现手头的任务。

在OCaml中,函数可能具有副作用,特别是传递给高阶函数(如map和fold)的函数,这意味着编译器自动砍伐森林要困难得多。程序员必须手工完成。

换句话说:停止构建庞大的短期数据结构,例如0 -- n(efficient_map summand (0 -- n))。当你的程序决定解决一个新的summand时,让它在一次通过中完成它想要做的所有操作。您可以将此视为在Wadler文章中应用这些原则的练习(再次,手动,因为由于各种原因,尽管您的程序是纯粹的,编译器也不会为您执行此操作。)


以下是一些结果:

$ ocamlopt v2.ml
$ time ./a.out 1000000
3.14159165359

real    0m0.020s
user    0m0.013s
sys     0m0.003s
$ ocamlopt v1.ml
$ time ./a.out 1000000
3.14159365359

real    0m0.238s
user    0m0.204s
sys     0m0.029s

v1.ml是您的版本。你可能会认为v2.ml是一个惯用的OCaml版本:

let rec q_pi_approx p n acc =
  if n = p
  then acc
  else q_pi_approx (succ p) n (acc +. (summand p))

let n = int_of_string Sys.argv.(1);;

Printf.printf "%F\n" (4. *. (q_pi_approx 0 n 0.));;

(从代码中重用summand

从最后一个词到第一个词的总和,而不是从第一个词到最后一个词的总和可能更准确。这与您的问题是正交的,但您可以将其视为修改强制为尾递归的函数的练习。此外,(-1.) ** m中的summand表达式由编译器映射到主机上对pow()函数的调用,而您可能希望避免使用a bag of hurt

答案 1 :(得分:6)

我也尝试了几种变体,这是我的结论:

  1. 使用数组
  2. 使用递归
  3. 使用命令式循环
  4. 递归函数比数组实现效率高约30%。势在必行的循环与递归一样有效(甚至可能稍慢)。

    以下是我的实施:

    阵列:

    open Core.Std
    
    let pi_approx n =
      let f m = (-1.) ** m /. (2. *. m +. 1.) in
      let qpi = Array.init n ~f:Float.of_int |>
                Array.map ~f |>
                Array.reduce_exn ~f:(+.) in
      qpi *. 4.0
    

    递归:

    let pi_approx n =
      let rec loop n acc m =
        if m = n
        then acc *. 4.0
        else
          let acc = acc +. (-1.) ** m /. (2. *. m +. 1.) in
          loop n acc (m +. 1.0) in
      let n = float_of_int n in
      loop n 0.0 0.0
    

    可以通过将局部函数loop移到外部来进一步优化,以便编译器可以内联它。

    势在必行的循环:

    let pi_approx n =
      let sum = ref 0. in
      for m = 0 to n -1 do
        let m = float_of_int m in
        sum := !sum +. (-1.) ** m /. (2. *. m +. 1.)
      done;
      4.0 *. !sum
    

    但是,在上面的代码中,为ref创建sum会在每一步中产生装箱/取消装箱,我们可以使用float_ref {{3}进一步优化此代码}:

    type float_ref = { mutable value : float}
    
    let pi_approx n =
      let sum = {value = 0.} in
      for m = 0 to n - 1 do
        let m = float_of_int m in
        sum.value <- sum.value +. (-1.) ** m /. (2. *. m +. 1.)
      done;
      4.0 *. sum.value
    

    记分板

    for-loop (with float_ref) : 1.0
    non-local recursion       : 0.89
    local recursion           : 0.86
    Pascal's version          : 0.77
    for-loop (with float ref) : 0.62
    array                     : 0.47
    original                  : 0.08
    

    更新

    我已经更新了答案,因为我找到了一种方法可以提高40%的速度(或者与@ Pascal的答案相比为33%。

答案 2 :(得分:4)

我想补充一点,虽然在OCaml中装有浮动框,但浮动数组是未装箱的。这是一个程序,它构建一个对应于Leibnitz序列的float数组,并用它来逼近π:

open Array

let q_pi_approx n =
  let summand n  =
    let m = float_of_int n
    in (-1.) ** m /. (2. *. m +. 1.) in
  let a = Array.init n summand in
  Array.fold_left (+.) 0. a

let n = int_of_string Sys.argv.(1);;
Printf.printf "%F\n" (4. *. (q_pi_approx n));;

显然,它仍然比根本不构建任何数据结构的代码慢。执行时间(带有数组的版本是最后一个):

time ./v1 10000000
3.14159275359

real    0m2.479s
user    0m2.380s
sys 0m0.104s

time ./v2 10000000
3.14159255359

real    0m0.402s
user    0m0.400s
sys 0m0.000s

time ./a 10000000
3.14159255359

real    0m0.453s
user    0m0.432s
sys 0m0.020s