V. *_*ria 2 ocaml compiler-optimization
有一个众所周知的编译器优化,即展开后跟折叠,这称为hylomorphism(换句话说:循环)。简而言之,不是构建结构并在之后立即销毁它,而是将折叠函数直接插入到结构的生成器上,这会导致在内存中根本不创建结构的就地循环。
例如,要对用户输入的一些数字求和,我们可以这样做
let sumInput () : int =
List.fold_left (+) 0
(List.init 1875 (fun _ -> read_int ()))
Run Code Online (Sandbox Code Playgroud)
中间结构是存储输入数字的列表,由List.init. 我希望 OCaml 的本机编译器ocamlopt将其优化为尾递归循环,例如
let optimSumInput () : int =
let rec optimInput_rec count sum =
if count = 0 then sum
else optimInput_rec (count-1) (sum + read_int ()) in
optimInput_rec 1875 0
Run Code Online (Sandbox Code Playgroud)
但是,ocamlopt -s生成此汇编代码,我们在其中看到camlStdlib__list__init和camlStdlib__list__fold_left:
camlTestOptim__sumInput_80:
.cfi_startproc
subq $8, %rsp
.cfi_adjust_cfa_offset 8
.L102:
movq camlTestOptim__3@GOTPCREL(%rip), %rbx
movq $3751, %rax
call camlStdlib__list__init_204@PLT
.L100:
movq %rax, %rdi
movq $1, %rbx
movq camlTestOptim__2@GOTPCREL(%rip), %rax
addq $8, %rsp
.cfi_adjust_cfa_offset -8
jmp camlStdlib__list__fold_left_250@PLT
.cfi_adjust_cfa_offset 8
.cfi_adjust_cfa_offset -8
.cfi_endproc
.type camlTestOptim__sumInput_80,@function
.size camlTestOptim__sumInput_80,. - camlTestOptim__sumInput_80
Run Code Online (Sandbox Code Playgroud)
是否有编译器开关可以优化列表?在这个特定的例子中,它不会加速太多,但对于比列表更复杂的结构,它会有所作为。
不,OCaml 的优化编译器不会做这样的优化,而且可能永远不会做。这回答了发布的问题。以下是我的个人意见,这不是答案,而是我认为对于正确理解语言以及为什么不应期望进行此类优化所必需的评论。
OCaml 的优化编译器以其可预测性和将函数式程序高效编译为机器码的能力而著称。然而,它不会将坏程序翻译成好的程序,因为编译器密切遵循语言的语义并相信程序员知道他们在做什么。与 Haskell 不同,OCaml 是一种严格的命令式语言,具有更强的语义。与 Haskell 相比,它为程序员提供了对执行过程的更多控制,但这当然是有代价的,因为某些优化即使不是不可能实现也很难实现。在 OCaml 中,默认情况下所有表达式都是不纯的(有效),因此为了删除表达式并用其值替换它,编译器必须证明在此过程中没有丢失任何效果。另一个重要方面,当我们来自 Haskell 领域时,我们应该记住的是,OCaml 是严格的,OCaml 列表也是严格的归纳数据类型。因此,虽然在具有按名称调用语义和惰性列表的 Haskell 中,这种优化可以被视为很自然,但在 OCaml 中它会走得太远,并且本质上会违反程序员和编译器之间的契约。
由于 OCaml 为我们程序员提供了对执行过程的更多控制,因此我们有许多不同的数据结构,它们在数学上是同构的,但提供了不同的权衡。作为程序员,我们的任务是选择最能反映我们的需求和约束的表示。鉴于您的具体示例,选择列表作为基础数据结构有点奇怪且不合理。但是,编译器信任程序员,如果程序员决定在这里使用列表,它会遵循请求。
经验丰富的程序员会在这里选择更好的数据结构,例如,CoreSequence.t或 Lwt 的流,或者只是标准Seq.t迭代器,
let rec ints () = match read_int () with
| exception End_of_file -> Seq.Nil
| s -> Seq.Cons (s,ints)
Run Code Online (Sandbox Code Playgroud)
我们可以将整数和定义为
let sum = Seq.fold_left (+) 0
Run Code Online (Sandbox Code Playgroud)
因此我们的main函数看起来像
let main () =
print_int (sum ints);
print_newline ()
Run Code Online (Sandbox Code Playgroud)
此代码不会创建任何中间列表,因此将自然地应用森林砍伐,而我们不必依赖晦涩的编译器优化。
Haskell 的这种区别在编程和纯数学之间划出了一条界限,因为虽然不同种类的列表在数学上是等价的,但从程序员的角度来看,有很多不同的实现。
因此,OCaml 依靠程序员来做出这个选择,而不是试图比人类更聪明。另一方面,它提供了一个非常强大的抽象机制,它是西格玛代数的具体化,它让我们可以针对抽象进行编程,并在我们想要的时候留在纯数学的世界中,并在绝对必要的时候返回地球。