为什么Clojure在Alioth基准测试中表现不如Scala?

11

添加了“性能”标签,因为大多数类似的问题都可以在这里找到。 - Rex Kerr
Clojure还没有完全成熟 - Clojure 1.0于2009年5月发布,而Scala的第一个公开版本是在2003年。 - igouy
5个回答

11

任何语言都可以编写快速或慢速的代码 :-)

根据对一些Clojure代码的快速检查,我认为性能差异的主要原因是Clojure基准测试代码尚未完全优化以使用最快的语言功能。

例如,Clojure中以下功能非常酷且有用于开发方便,但会产生一些运行时性能开销:

  • 惰性序列和列表
  • 使用反射的动态Java互操作性
  • 运行时函数组合 / 一等函数
  • 多方法/动态分派
  • 使用eval或REPL进行动态编译
  • BigInteger算术

如果您想获得绝对最大的性能(以一些额外的复杂性为代价),则应重写代码以避免这些问题,并使用以下内容:

  • 静态类型提示(以避免反射)
  • 暂变
  • 宏(用于编译时代码操作)
  • 协议
  • Java原始数据类型和数组
  • 循环/递归进行迭代

通过巧妙地使用上述内容,我发现在Clojure 1.2+中通常可以获得非常接近Java性能的水平,例如考虑以下代码以执行一百万次加法:

未经优化的Clojure使用懒惰序列和BigInteger算术。它很好用,但不是特别快:

(reduce 
  (fn [acc val] (unchecked-int (unchecked-add (int acc) (int val)))) 
  (range 0 1000000))

=> "Elapsed time: 65.201243 msecs"

使用原始算术和循环/递归进行优化的Clojure:

(loop [acc (int 0) i (int 0)] 
  (if (>= i (int 1000000)) 
    acc 
    (recur (unchecked-add acc i) (unchecked-inc i)) ))

=> "Elapsed time: 0.691474 msecs"

Java代码,一个相当标准的迭代循环:

public static int addMillion() {
    int result=0;
    for (int i=0; i<1000000; i++) {
        result+=i;
    }
    return result;
}

=> "Elapsed time: 0.692081 msecs"

p.s. 我在 Clojure 代码中使用了 unchecked-add 而不是 +,以匹配 Java 的整数溢出行为。


10
您可以贡献一些程序来证明在Clojure 1.2+中可以特别地“接近Java性能”;-) - igouy
我有一个使用Clojure(目标版本为1.2)编写的列表实现,具有快速随机访问功能,其随机访问速度与使用Java编写的向量完全相同。因此,可以达到Java的速度。然而,这不是一个可疑的基准测试,而是在实际程序中非常有用的东西。因此,也许不能算作演示。 - kotarak
4
@igouy - 无稽之谈,我已经展示了确切的技术,说明如何使Clojure与Java的速度相匹配,这正是回答的全部目的。请注意,我的“业务要求”明确说明是执行一百万次加法,因此结果是无关紧要的。 - mikera
4
目前在优化基准测试方面存在一些犹豫。Clojure 1.3 目前处于 alpha 阶段,带来了许多性能改进,并删除了许多用于快速代码的注释。大多数人认为最好等待 1.3 稳定后再进行基准测试的优化。 - Nicolas Oury
@igouy:我做得更好了,并修改了代码,使它们产生相同的结果。结论和时间与预期相同。 - mikera
显示剩余5条评论

4

Clojure是一种动态语言,它需要进行大量的运行时检查。

编辑,现在的情绪不那么轻浮:没有查看实际的Clojure基准测试实现,性能差异可能也可以归因于典型的Clojure代码避免使用可变状态,其他条件相同,使用可变状态的算法通常优于不使用可变状态的算法。

一个程序员能否编写正确使用可变状态的代码是另一个问题,当然纯函数式代码的声明性质意味着很多优化在理论上都成为可能。


1
@dnolen:你是在说Clojure在运行时不执行类型检查吗?还是Scala会执行呢? - sepp2k
3
@dnolen 这个观点是错误的,因为________。正确答案是________。如果您掌握了我们不知道的知识,请解释一下! - Mike M.
1
@Rex,作为一种动态语言,令人印象深刻。不要忘记,HotSpot最初是作为Smalltalk VM(尽管是带有可选类型注释的Smalltalk变体! :))而诞生的。 - Alex Cruise
1
@Alex C. - 我同意这对于一种动态语言来说是令人印象深刻的,但在很多方面,Scala 由于其可能的类型推断而“感觉”像一种动态语言。与例如 C 和 Java 相比,LuaJIT 是唯一值得称赞的东西。 - Rex Kerr
2
@Alex - 当你没有时间去阅读代码时,你的评论会保持轻浮模式;-) [如果代码必须以“牺牲惯用性”为代价进行重写,则您已经了解了该语言实现中惯用代码的性能。] - igouy
显示剩余7条评论

3

部分原因是由于测试本身,即您必须编写代码(无论使用哪种语言),以执行与Java版本完全相同的实现步骤。哦,您有一种不同、可能更快的方法来执行X吗?太糟糕了。

当测试的基础是循环和数组操作,以及需要多个浮动的代码块的一般命令式样式时,您将得到毫无意义的数字。


1
据我所知,还有一个“有趣的替代方案”程序部分。解决问题但不符合基准约束条件的程序仍然会显示出来。例如,对于C++和Python3,有许多比提交基准测试的程序更快的替代方案。也许有人可以提交一些优雅的Clojure代码,即使只是为了“替代方案”部分。 - Kintaro
3
不正确。您无需编写代码以“执行与Java版本完全相同的实现步骤”。但是您必须使用相同的算法-因此对于pi位数,您必须使用Haskell的“Unbounded Spigot Algorithm”。 - igouy
3
不正确。如果你的语言有更快的内置功能可以完成一部分工作,这并不是一个“显式”的测试来检验你的语言如何实现某个算法。你可以使用它并进行清理。以JavaScript V8正则表达式为例,感谢Google的工程师们付出了相当大的努力制作了一个惊人的正则表达式引擎(被V8使用),其性能得到了显著提升。当然,如果你没有告诉人们至少使用相同的算法,那么你只是在测试谁能想出最聪明的算法,这是一个与不同语言执行特定任务时性能表现不同的问题是_不同的_问题。 - Rex Kerr

1

许多基准测试较慢,因为直到最近,要想从Clojure中获得良好的性能需要更多的工作和专业知识。这些基准测试主要针对1.2版本。

Clojure 1.3有很多改进,使编写高性能代码变得更加简单。即便如此,社区中熟悉Clojure代码性能优化的人并不是很多,因此随着人们提交更好的版本,这将是一个逐渐的过程。


Clojure 1.3 还没有完全成熟,因此 - Clojure 1.3 Alpha 1、Clojure 1.3 Alpha 2、Clojure 1.3 Alpha 3 等等。 - igouy

-1

Scala的特性通常是逐步引入的,在大多数情况下会指定相应的Java代码。这使得Scala成为Java的一种进化,考虑到新功能和性能之间的平衡。

另一方面,Clojure则是以非常不同的编程方法为设计基础,并具有与Java进行交互的特性。然而,两种语言之间存在的阻抗失配(以及JVM是为Java准备的)可能会使Clojure稍微慢一些。对于某些问题,Clojure可能更快(例如使用不可变集合),但对于大多数应用程序可能并非如此。


在大多数情况下,Clojure持久化数据结构应该比相应的Java数据结构稍微慢一些。然而,在并发访问的情况下,它们将是正确的。 :) - Alex Miller
1
Clojure明确地旨在实现与Java相当的性能。但是要达到这个目标仍然需要不断努力。 - Alex Miller
@Alex:啊哈,没错,就像Lisp旨在实现汇编语言的性能一样...25年后它仍然还在不断发展 :) - Diego Sevilla
1
我认为1.3快照中的工作实际上已经大大地弥补了已知的差距。关键在于Clojure代码编译成与Java完全相同的字节码,没有包装器或中间调用。如果不是这样,通常很容易检测到(通过反射警告),并进行修复(通过提示)。 - Alex Miller

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接