Scala VS Java,性能和内存?

我热衷于研究 Scala,有一个基本问题我似乎找不到答案: 一般来说,Scala 和 Java 在性能和内存使用方面有区别吗?

88032 次浏览

Java 和 Scala 都能编译成 JVM 字节码,所以差别不大。您可以得到的最佳比较可能是在 计算机语言基准测试游戏上,它实际上表明 Java 和 Scala 具有相同的内存使用情况。在列出的一些基准测试中,Scala 只比 Java 慢 有点,但这可能仅仅是因为程序的实现不同。

不过说真的,他们俩那么亲密,根本不值得担心。使用更具表现力的语言(比如 Scala)所带来的生产力提升,远远超过了最低限度(如果有的话)的性能损失。

计算机语言基准游戏:

速度测试 java/scala 1.71/2.25

内存测试 java/scala 66.55/80.81

因此,这个基准测试表明 java 的速度提高了24% ,scala 使用的内存增加了21% 。

总而言之,这没什么大不了的,而且在现实世界的应用程序中也没什么关系,因为大部分时间都被数据库和网络占用了。

底线: 如果 Scala 让你和你的团队(以及你离开后接手项目的人)更有效率,那么你应该去做。

Scala 使得在不知不觉中使用大量内存变得非常容易。这通常是非常强大的,但有时可以是恼人的。例如,假设您有一个字符串数组(称为 array) ,以及从这些字符串到文件的映射(称为 mapping)。假设您希望获取映射中的所有文件,并且这些文件来自长度大于2的字符串。用爪哇语,你可以

int n = 0;
for (String s: array) {
if (s.length > 2 && mapping.containsKey(s)) n++;
}
String[] bigEnough = new String[n];
n = 0;
for (String s: array) {
if (s.length <= 2) continue;
bigEnough[n++] = mapping.get(s);
}

在 Scala 中,做同样事情最简洁的方法是:

val bigEnough = array.filter(_.length > 2).flatMap(mapping.get)

放松!但是,除非您非常熟悉集合的工作方式,否则您可能不会意识到这种方式会为 数组中的每个元素创建一个额外的中间数组(使用 filter)和一个额外的对象(使用 mapping.get,它返回一个选项)。它还创建了两个函数对象(一个用于过滤器,一个用于 latMap) ,不过由于函数对象很小,这很少是一个主要问题。

所以基本上,内存使用在基本层次上是相同的。但是 Scala 的库有许多强大的方法,可以让您轻松地创建大量(通常是短期的)对象。垃圾收集器通常非常擅长处理这类垃圾,但是如果您完全不知道正在使用什么内存,那么在 Scala 中可能比在 Java 中更早遇到麻烦。

请注意,为了获得类 Java 的性能,计算机语言基准测试(Computer Language Benchmark Game Scala)代码是以类 Java 的风格编写的,因此具有类 Java 的内存使用。您可以在 Scala 中这样做: 如果您编写的代码看起来像高性能的 Java 代码,那么它就是高性能的 Scala 代码。(能够以更惯用的 Scala 风格编写它,并且仍然获得良好的性能,但这取决于具体情况。)

我应该补充的是,每花费一定的编程时间,我的 Scala 代码通常是 再快点,而不是 Java 代码,因为在 Scala 中,我可以用更少的努力完成冗长的非性能关键部分,并且花更多的精力优化算法和性能关键部分的代码。

像 Java 一样编写 Scala,您可以期望发出几乎相同的字节码——使用几乎相同的度量。

使用不可变对象和更高阶的函数,以更“惯用”的方式编写它,这样就会慢一些,大一些。这个经验法则的一个例外是,当使用泛型对象时,类型参数使用 @specialised注释,这将创建更大的字节码,通过避免装箱/拆箱,可以超过 Java 的性能。

同样值得一提的是,在编写可以并行运行的代码时,增加内存/减少速度是一种不可避免的权衡。惯用的 Scala 代码在本质上比典型的 Java 代码更具有声明性,并且通常只需要4个字符(.par)就可以完全并行。

所以如果

  • Scala 代码比单个线程中的 Java 代码长1.25倍
  • 它可以分为4个核(现在甚至在笔记本电脑上也很常见)
  • 对于并行运行时间为(1.24/4 =)0.3125 x 原始 Java

那么你会说 Scala 代码现在相对慢了25% ,还是快了3倍?

正确答案取决于你如何定义“性能”:)

@ higherkinded 关于主题 Scala 性能考虑的演示文稿,它对 Java/Scala 进行了一些比较。

工具:

伟大的博客文章:

我是一个新用户,所以我不能给 Rex Kerr 的答案添加评论(顺便说一句,允许新用户“回答”而不是“评论”是一个非常奇怪的规则)。

我注册只是为了回应“哎呀,Java 是如此冗长和如此努力工作”的影射,雷克斯的流行答案在上面。当然,您可以编写更简洁的 Scala 代码,但是给出的 Java 示例显然过于臃肿。大多数 Java 开发人员会编写如下代码:

List<String> bigEnough = new ArrayList<String>();
for(String s : array) {
if(s.length() > 2 && mapping.get(s) != null) {
bigEnough.add(mapping.get(s));
}
}

当然,如果我们假设 Eclipse 没有为您完成大部分实际的输入工作,并且保存的每个字符都真正使您成为更好的程序员,那么您可以编写以下代码:

List b=new ArrayList();
for(String s:array)
if(s.length()>2 && mapping.get(s) != null) b.add(mapping.get(s));

现在,我不仅节省了输入完整变量名和花括号的时间(让我可以再花5秒钟思考深入的算法思想) ,而且我还可以在混淆比赛中输入我的代码,还有可能为假期赚到额外的钱。

关于紧循环,其他人也回答了这个问题,尽管我评论过的 Rex Kerr 的例子之间似乎有明显的性能差异。

这个答案确实是针对那些可能调查需要紧循环优化作为设计缺陷的人。

我对 Scala 相对比较陌生(大约一年左右) ,但是到目前为止,它给人的感觉是,它允许你相对容易地在设计、实现和执行的许多方面进行 推迟(有足够的背景阅读和实验:)

延期设计特点:

推迟实施特点:

延迟执行特性: (抱歉,没有链接)

  • 线程安全的惰性值
  • 通过名字
  • 一元的东西

对我来说,这些特性可以帮助我们走向快速、紧凑的应用程序。


Rex Kerr 的示例在执行的哪些方面被延迟方面有所不同。在 Java 示例中,内存分配被推迟,直到 Scala 示例推迟映射查找时计算内存的大小。对我来说,它们看起来是完全不同的算法。

下面是我所认为的 Java 例子中的一个苹果对苹果的等价物:

val bigEnough = array.collect({
case k: String if k.length > 2 && mapping.contains(k) => mapping(k)
})

没有中间集合,没有 Option实例等。 这也保留了集合类型,所以 bigEnough的类型是 Array[File]-Arraycollect实现可能会像 Kerr 先生的 Java 代码所做的那样。

上面列出的延迟设计特性还允许 Scala 的集合 API 开发人员在未来的版本中实现快速的特定于 Array 的集合实现,而不会破坏 API。这就是我所说的通往速度之路。

另外:

val bigEnough = array.withFilter(_.length > 2).flatMap(mapping.get)

我在这里使用的 withFilter方法代替 filter修复了中间集合问题,但仍然存在 Option 实例问题。


Scala 中简单执行速度的一个例子是日志记录。

在 Java 中,我们可以这样写:

if (logger.isDebugEnabled())
logger.debug("trace");

在 Scala 中,这只是:

logger.debug("trace")

因为要在 Scala 中调试的消息参数的类型是“ => String”,我认为这是一个没有参数的函数,在计算时会执行,但是文档会调用 pass-by-name。

编辑{ Scala 中的函数是对象,所以这里有一个额外的对象。对于我的工作来说,一个简单对象的重量值得消除不必要地计算日志消息的可能性。 }

这并不能使代码更快,但它确实使它更有可能更快,我们不太可能有经验,通过和清理其他人的代码集体。

对我来说,这是 Scala 中的一致主题。


尽管有一些提示,但是硬代码无法捕捉到为什么 Scala 更快的原因。

我觉得这是代码重用和 Scala 代码质量上限的结合。

在 Java 中,令人敬畏的代码常常被迫成为难以理解的混乱,因此在生产质量 API 中并不真正可行,因为大多数程序员不能使用它。

我非常希望 Scala 能够允许我们中的 Einstein 实现更有效的 API,可能通过 DSL 表达。Scala 的核心 API 已经在这条道路上走得很远了。

Java 示例实际上并不是典型应用程序的习惯用法。 这样的优化代码可以在系统库方法中找到。但是它会使用一个正确类型的数组,即 File [] ,并且不会抛出 IndexOutOfbound sException。(计数和加法的不同过滤条件)。 我的版本是(总是)因为我不喜欢花一个小时搜索一个 bug,这个 bug 是通过节省2秒钟在 Eclipse 中单击一个键引入的) :

List<File> bigEnough = new ArrayList<File>();
for(String s : array) {
if(s.length() > 2) {
File file = mapping.get(s);
if (file != null) {
bigEnough.add(file);
}
}
}

但是我可以从我当前的项目中为您带来许多其他难看的 Java 代码示例。我试图通过剔除常见的结构和行为来避免编码的常见复制和修改方式。

在我的抽象 DAO 基类中,有一个用于公共缓存机制的抽象内部类。对于每个具体的模型对象类型,都有一个抽象 DAO 基类的子类,其中内部类被子类化,以便为从数据库加载时创建业务对象的方法提供一个实现。(我们不能使用 ORM 工具,因为我们通过专有的 API 访问另一个系统。)

这段子类化和实例化代码在 Java 中并不清晰,在 Scala 中非常易读。