Scala与Java相比的性能


41

首先,我想明确指出,这不是确定哪种语言更好的语言-X-语言-Y的问题。

我已经使用Java很长时间了,我打算继续使用它。与此并行的是,我目前对Scala的学习非常感兴趣:除了一些让我印象深刻的小事情之外,我真的可以很好地使用这种语言。

我的问题是:在执行速度和内存消耗方面,用Scala编写的软件与用Java编写的软件相比如何?当然,通常这是一个很难回答的问题,但是我希望模式匹配,高阶函数等更高层次的结构会带来一些开销。

但是,我目前在Scala的经验仅限于50行以下代码的小示例,并且到目前为止,我还没有运行任何基准测试。因此,我没有真实数据。

如果事实证明Scala Java上确实有一些开销,那么混合使用Scala / Java项目是否有意义,在该项目中,Scala中的较复杂部分和Java中的性能关键部分都可以编码?这是常见的做法吗?

编辑1

我运行了一个小型基准测试:构建一个整数列表,将每个整数乘以2并将其放入新列表中,然后打印结果列表。我编写了一个Java实现(Java 6)和一个Scala实现(Scala 2.9)。我已经在Ubuntu 10.04下的Eclipse Indigo上都运行了。

结果是可比的:Java 480 ms,Scala 493 ms(平均100次迭代)。这是我使用过的片段。

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

因此,在这种情况下,Scala开销(使用范围,地图,lambda)似乎确实很小,与世界工程师提供的信息相距不远。

也许还有其他Scala构造应该谨慎使用,因为它们执行起来特别繁重

编辑2

你们中有些人指出,内部循环中的println占用了大部分执行时间。我删除了它们,并将列表的大小设置为100000而不是20000。对于Java,结果平均为88毫秒,对于Scala为49毫秒。


5
我可以想象,由于Scala可以编译为JVM字节码,因此在其他所有条件相同的情况下,性能在理论上可以等同于在同一JVM下运行的Java。我认为,不同之处在于Scala编译器如何创建字节码以及是否如此高效地创建字节码。
maple_shaft

2
@maple_shaft:也许Scala编译时间有开销?
FrustratedWithFormsDesigner 2012年

1
@Giorgio Scala对象和Java对象之间没有运行时区别,它们都是按字节代码定义和运行的JVM对象。例如,Scala作为一种语言具有闭包的概念,但是当编译闭包时,它们将被编译为带有字节码的多个类。从理论上讲,我可以物理上编写可以编译为完全相同的字节码的Java代码,并且运行时行为也完全相同。
maple_shaft

2
@maple_shaft:这正是我要针对的目标:与相应的Java代码相比,我发现上面的Scala代码更加简洁和易读。我只是想知道出于性能原因用Java编写Scala项目的一部分是否有意义,以及这些部分应该是什么。
乔治

2
运行时将主要由println调用占用。您需要更多的计算密集型测试。
凯文·克莱恩

Answers:


39

在Java中,您可以简洁高效地完成Scala中无法做到的一件事:枚举。对于其他所有内容,即使对于Scala库中速度较慢的构造,也可以在Scala中获得有效的版本。

因此,在大多数情况下,您无需在代码中添加Java。即使对于在Java中使用枚举的代码,Scala中通常也有一个足够好的解决方案-我将例外放在具有额外方法且使用了int常量值的枚举中。

至于要注意什么,这是一些事情。

  • 如果使用“丰富我的库”模式,请始终转换为类。例如:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
  • 警惕收集方法-因为它们在大多数情况下都是多态的,所以JVM不会对其进行优化。您无需避免使用它们,而应在关键部分上加以注意。请注意,for在Scala中是通过方法调用和匿名类实现的。

  • 如果使用一个Java类,如StringArrayAnyVal类,对应于Java基本,宁可由爪哇提供的方法时,存在替代。例如,使用lengthon StringArray代替size

  • 避免粗心使用隐式转换,因为您会发现自己误用了转换而不是设计使然。

  • 扩展类而不是特征。例如,如果要扩展Function1,则扩展AbstractFunction1

  • 使用-optimise和专业化可以获取大部分Scala。

  • 了解正在发生的事情:javap是您的朋友,还是一堆显示事情进展的Scala标志。

  • Scala习惯用法旨在提高正确性并使代码更简洁和可维护。它们不是为提高速度而设计的,因此,如果您需要使用它null而不是Option在关键路径上使用,请这样做!Scala是多范式是有原因的。

  • 请记住,性能的真正衡量标准是运行代码。有关忽略该规则可能发生的情况的示例,请参见此问题


1
+1:许多有用的信息,即使是关于我仍然必须学习的主题的信息,但在阅读它们之前先阅读一些提示是很有用的。
乔治

为什么第一种方法使用反射?无论如何它都会生成匿名类,那么为什么不使用它而不是反射呢?
Oleksandr.Bezhan,2013年

@ Oleksandr.Bezhan匿名类是Java概念,而不是Scala。它生成类型细化。不能从外部访问未覆盖其基类的匿名类方法。Scala的类型细化并非如此,因此达到该方法的唯一方法是通过反射。
Daniel C. Sobral

这听起来很可怕。特别是:“提防收集方法-因为它们在大多数情况下都是多态的,因此JVM不会对其进行优化。您不必避免使用它们,而应在关键部分加以注意。”
马特

21

根据针对单核32位系统的Benchmarks Game,Scala的中位数速度是Java的80%。对于四核x64计算机,性能大约相同。在大多数情况下,甚至内存使用情况和代码密度也非常相似。我会说,基于这些(不是很科学的)分析,您断言Scala给Java增加了一些开销是正确的。它似乎并没有增加大量的开销,所以我怀疑诊断更高级的物品占用更多的空间/时间是最正确的选择。


2
对于这样的回答,请只使用直接比较,因为帮助页面提示(shootout.alioth.debian.org/help.php#comparetwo
igouy

18
  • 如果仅在Scala中编写类似Java / C的代码,Scala的性能将非常出色。编译器将使用JVM原语IntChar何时能等。While循环在Scala中同样有效。
  • 请记住,lambda表达式被编译为这些Function类的匿名子类的实例。如果将lambda传递给map,则需要实例化匿名类(并且可能需要传递一些本地变量),然后每次迭代都会从apply调用中产生额外的函数调用开销(带有一些参数传递)。
  • 诸如此类的许多类scala.util.Random只是等效的JRE类的包装器。额外的函数调用有点浪费。
  • 注意性能关键代码中的隐式函数。java.lang.Math.signum(x)比来回x.signum()转换要直接得多RichInt
  • 与Java相比,Scala的主要性能优势是专业化。请记住,在库代码中很少使用专业化。

5
  • a)据我所知,我不得不指出,静态main方法中的代码无法很好地优化。您应该将关键代码移到其他位置。
  • b)从长期的观察来看,我建议不要在性能测试上做大量的工作(除非它正是您要优化的东西,但是谁应该读过200万个值?)。您正在测量println,这不是很有趣。用max替换println:
(1 to 20000).toList.map (_ * 2).max

将系统上的时间从800毫秒减少到20毫秒。

  • c)理解力有点慢(尽管我们必须承认,它一直都在进步)。请改用while或tailrecursive函数。在此示例中,它不在外部循环中。使用@ tailrec-annotation来测试梯级精度。
  • d)与C / Assembler比较失败。例如,您不会为不同的体系结构重写scala代码。与历史情况的其他重要区别是
    • JIT编译器,根据输入数据进行动态优化,并可能进行动态优化
    • 缓存未命中的重要性
    • 并行调用的重要性日益提高。如今,Scala的解决方案无需并行处理即可工作。在Java中这是不可能的,除非您要做更多的工作。

2
我从循环中删除了println,实际上Scala代码比Java代码要快。
乔治

与C和Assembler进行比较的意义如下:高级语言具有更强大的抽象,但是您可能需要使用低级语言来提高性能。是否同时将Scala作为高级语言和Java作为低级语言?也许不是,因为Scala似乎提供与Java类似的性能。
乔治

我认为对于Clojure或Scala来说并没有多大关系,但是当我以前使用jRuby和Jython时,我可能会用Java编写对性能要求更高的代码。与那两个人相比,我看到了巨大的差距,但是那是在几年前……可能会更好。
钻机2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.