请注意我几乎是OCaml的新手。为了学习一点并测试其性能,我尝试使用Leibniz series实现一个近似Pi的模块。
我的第一次尝试导致堆栈溢出(实际错误,而不是此站点)。从Haskell知道这可能来自太多" thunks"或承诺计算某些东西,同时递归加法,我寻找一些方法来保持最后的结果,同时与下一个相加。我在OCaml课程的注释here和here中找到了sum
和map
的以下尾递归实现,并期望编译器产生有效的结果。
但是,使用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。
答案 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)
我也尝试了几种变体,这是我的结论:
递归函数比数组实现效率高约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