如何优化 Scala 中的 for-comprehensions 和循环?
Posted
技术标签:
【中文标题】如何优化 Scala 中的 for-comprehensions 和循环?【英文标题】:How to optimize for-comprehensions and loops in Scala? 【发布时间】:2011-09-03 00:32:16 【问题描述】:所以 Scala 应该和 Java 一样快。我正在重新审视我最初在 Java 中解决的 Scala 中的一些 Project Euler 问题。具体问题 5:“能被 1 到 20 的所有数整除的最小正数是多少?”
这是我的 Java 解决方案,在我的机器上完成需要 0.7 秒:
public class P005_evenly_divisible implements Runnable
final int t = 20;
public void run()
int i = 10;
while(!isEvenlyDivisible(i, t))
i += 2;
System.out.println(i);
boolean isEvenlyDivisible(int a, int b)
for (int i = 2; i <= b; i++)
if (a % i != 0)
return false;
return true;
public static void main(String[] args)
new P005_evenly_divisible().run();
这是我对 Scala 的“直接翻译”,耗时 103 秒(长 147 倍!)
object P005_JavaStyle
val t:Int = 20;
def run
var i = 10
while(!isEvenlyDivisible(i,t))
i += 2
println(i)
def isEvenlyDivisible(a:Int, b:Int):Boolean =
for (i <- 2 to b)
if (a % i != 0)
return false
return true
def main(args : Array[String])
run
最后这是我对函数式编程的尝试,耗时 39 秒(55 倍)
object P005 extends App
def isDivis(x:Int) = (1 to 20) forall x % _ == 0
def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
println (find (2))
在 64 位 Windows 7 上使用 Scala 2.9.0.1。如何提高性能?难道我做错了什么?还是 Java 更快?
【问题讨论】:
你是用scala shell编译还是解释? 有比使用试用除法更好的方法 (Hint)。 你没有展示你是如何计时的。您是否尝试过为run
方法计时?
@hammar - 是的,只是用笔和纸的方式完成了:写下每个从高开始的数字的质因数,然后划掉你已经拥有的更高数字的因数,这样你就完成了与 (5*2*2)*(19)*(3*3)*(17)*(2*2)*()*(7)*(13)*()*(11) = 232792560跨度>
+1 这是我几周来在 SO 上看到的最有趣的问题(这也是我在很长一段时间内看到的最佳答案)。
【参考方案1】:
在这种特殊情况下的问题是您从 for 表达式中返回。这反过来又被转换为抛出 NonLocalReturnException,它在封闭方法中被捕获。优化器可以消除 foreach 但还不能消除 throw/catch。投掷/接球很昂贵。但是由于这种嵌套返回在 Scala 程序中很少见,优化器还没有解决这种情况。正在改进优化器,希望能很快解决这个问题。
【讨论】:
相当沉重,返回成为异常。我敢肯定它在某处被记录在案,但它具有无法理解的隐藏魔法的气味。这真的是唯一的方法吗? 如果返回发生在闭包内部,这似乎是最好的选择。来自外部闭包的返回当然会直接转换为字节码中的返回指令。 我确定我忽略了一些东西,但为什么不编译从闭包内部的返回来设置封闭的布尔标志和返回值,并在闭包调用返回后检查呢? 为什么他的函数式算法还是慢了 55 倍?它看起来不应该遭受如此糟糕的表现 现在,在 2014 年,我再次对其进行了测试,对我来说性能如下:java -> 0.3s;斯卡拉-> 3.6s; scala优化-> 3.5s; scala函数-> 4s;看起来比三年前好多了,但是……还是差距太大了。我们可以期待更多的性能改进吗?换句话说,Martin,理论上还有什么可以优化的吗?【参考方案2】:已经讨论了 Scala 特有的问题,但主要问题是使用蛮力算法不是很酷。考虑一下(比原始 Java 代码快得多):
def gcd(a: Int, b: Int): Int =
if (a == 0)
b
else
gcd(b % a, a)
print (1 to 20 reduce ((a, b) =>
a / gcd(a, b) * b
))
【讨论】:
这些问题比较了跨语言的特定逻辑的性能。算法是否对问题最优并不重要。【参考方案3】:尝试解决方案中给出的单行Scala for Project Euler
给出的时间至少比你的快,虽然离while循环很远.. :)
【讨论】:
和我的功能版很相似。您可以将我的写为def r(n:Int):Int = if ((1 to 20) forall n % _ == 0) n else r (n+2); r(2)
,它比 Pavel 的短 4 个字符。 :) 但是我并没有假装我的代码有什么好处——当我发布这个问题时,我总共编写了大约 30 行 Scala 代码。【参考方案4】:
我只是想为那些可能对 Scala 失去信心的人发表评论,因为这类问题几乎出现在所有函数式语言的性能中。如果您在 Haskell 中优化折叠,您通常必须将其重写为递归尾调用优化循环,否则您将面临性能和内存问题。
我知道不幸的是,FP 还没有优化到我们不必考虑这样的事情的地步,但这根本不是 Scala 特有的问题。
【讨论】:
【参考方案5】:我发现的一些加速forall
方法的方法:
原版:41.3 秒
def isDivis(x:Int) = (1 to 20) forall x % _ == 0
预实例化范围,因此我们不会每次都创建新范围:9.0 秒
val r = (1 to 20)
def isDivis(x:Int) = r forall x % _ == 0
转换为列表而不是范围:4.8 秒
val rl = (1 to 20).toList
def isDivis(x:Int) = rl forall x % _ == 0
我尝试了其他一些集合,但 List 最快(尽管仍然比我们完全避免 Range 和高阶函数慢 7 倍)。
虽然我是 Scala 的新手,但我猜编译器可以轻松地实现快速且显着的性能提升,方法是简单地用最外层范围内的 Range 常量自动替换方法中的 Range 字面量(如上)。或者更好的是,像 Java 中的字符串文字一样实习。
脚注:
数组与 Range 大致相同,但有趣的是,pimping 一个新的forall
方法(如下所示)导致在 64 位上的执行速度提高了 24%,在 32 位上提高了 8%。当我通过将因子数量从 20 减少到 15 来减少计算量时,差异消失了,所以可能是垃圾收集效应。不管是什么原因,在长时间满负荷运行时,这一点很重要。
List 的类似 pimp 也使性能提高了约 10%。
val ra = (1 to 20).toArray
def isDivis(x:Int) = ra forall2 x % _ == 0
case class PimpedSeq[A](s: IndexedSeq[A])
def forall2 (p: A => Boolean): Boolean =
var i = 0
while (i < s.length)
if (!p(s(i))) return false
i += 1
true
implicit def arrayToPimpedSeq[A](in: Array[A]): PimpedSeq[A] = PimpedSeq(in)
【讨论】:
【参考方案6】:关于理解的答案是正确的,但这不是全部。请注意,return
在isEvenlyDivisible
中的使用不是免费的。在for
中使用 return 会强制 scala 编译器生成非本地返回(即在其函数之外返回)。
这是通过使用异常退出循环来完成的。如果您构建自己的控制抽象,也会发生同样的情况,例如:
def loop[T](times: Int, default: T)(body: ()=>T) : T =
var count = 0
var result: T = default
while(count < times)
result = body()
count += 1
result
def foo() : Int=
loop(5, 0)
println("Hi")
return 5
foo()
这只会打印一次“Hi”。
请注意,foo
中的 return
退出 foo
(这是您所期望的)。由于括号中的表达式是一个函数文字,您可以在loop
的签名中看到它,这会强制编译器生成非本地返回,即return
强制您退出foo
,而不仅仅是@ 987654331@.
在 Java(即 JVM)中,实现这种行为的唯一方法是抛出异常。
回到isEvenlyDivisible
:
def isEvenlyDivisible(a:Int, b:Int):Boolean =
for (i <- 2 to b)
if (a % i != 0) return false
return true
if (a % i != 0) return false
是一个有返回值的函数字面量,所以每次返回时,运行时都必须抛出并捕获异常,这会导致相当多的 GC 开销。
【讨论】:
【参考方案7】:作为后续,我尝试了 -optimize 标志,它将运行时间从 103 秒减少到 76 秒,但这仍然比 Java 或 while 循环慢 107 倍。
然后我在看“功能”版本:
object P005 extends App
def isDivis(x:Int) = (1 to 20) forall x % _ == 0
def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
println (find (2))
并试图弄清楚如何以简洁的方式摆脱“forall”。我悲惨地失败了,然后想出了
object P005_V2 extends App
def isDivis(x:Int):Boolean =
var i = 1
while(i <= 20)
if (x % i != 0) return false
i += 1
return true
def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
println (find (2))
因此,我巧妙的 5 行解决方案已膨胀到 12 行。但是,这个版本的运行时间0.71秒,与原始Java版本的速度相同,比上面使用“forall”的版本(40.2秒)快56倍! (请参阅下面的编辑了解为什么这比 Java 更快)
显然,我的下一步是将上面的内容转换回 Java,但 Java 无法处理它并抛出一个 ***Error,n 在 22000 左右。
然后我摸了摸头,用更多的尾递归替换了“while”,这节省了几行,运行速度一样快,但让我们面对现实吧,阅读起来更加混乱:
object P005_V3 extends App
def isDivis(x:Int, i:Int):Boolean =
if(i > 20) true
else if(x % i != 0) false
else isDivis(x, i+1)
def find(n:Int):Int = if (isDivis(n, 2)) n else find (n+2)
println (find (2))
所以 Scala 的尾递归赢得了胜利,但令我惊讶的是,像“for”循环(和“forall”方法)这样简单的东西基本上被破坏了,必须用不优雅和冗长的“whiles”代替,或尾递归。我尝试 Scala 的很多原因是因为语法简洁,但如果我的代码运行速度要慢 100 倍,那就不好了!
编辑:(已删除)
EDIT OF EDIT:之前运行时间 2.5 秒和 0.7 秒之间的差异完全是由于使用的是 32 位还是 64 位 JVM。命令行中的 Scala 使用 JAVA_HOME 设置的任何内容,而 Java 使用 64 位(如果可用)无论如何。 IDE 有自己的设置。这里有一些测量:Scala execution times in Eclipse
【讨论】:
isDivis 方法可以写成:def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)
。请注意,在 Scala 中 if-else 是一个始终返回值的表达式。这里不需要返回关键字。
您的最新版本 (P005_V3
) 可以通过以下方式变得更短、更具声明性和恕我直言:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
@Blaisorblade 否。这会破坏尾递归,这是在字节码中转换为 while 循环所必需的,从而加快执行速度。
我明白你的意思,但我的例子仍然是尾递归的,因为 && 和 ||使用短路评估,通过使用@tailrec 确认:gist.github.com/Blaisorblade/5672562【参考方案8】:
问题很可能是在方法isEvenlyDivisible
中使用了for
推导式。用等效的 while
循环替换 for
应该可以消除与 Java 的性能差异。
与 Java 的 for
循环相反,Scala 的 for
推导式实际上是高阶方法的语法糖;在这种情况下,您在 Range
对象上调用 foreach
方法。 Scala 的for
很笼统,但有时会导致痛苦的性能。
您可能想在 Scala 2.9 版中尝试-optimize
标志。观察到的性能可能取决于使用的特定 JVM,以及 JIT 优化器是否有足够的“预热”时间来识别和优化热点。
最近在邮件列表上的讨论表明,Scala 团队正在努力提高 for
在简单情况下的性能:
这是错误跟踪器中的问题: https://issues.scala-lang.org/browse/SI-4633
5/28 更新:
作为一个短期解决方案,ScalaCL 插件 (alpha) 会将简单的 Scala 循环转换为等效的while
循环。
作为潜在的长期解决方案,来自 EPFL 和斯坦福大学的团队 collaborating on a project 启用 "virtual" Scala 的运行时编译以获得非常高的性能。例如,多个惯用的功能循环可以fused at run-time 转换为最佳 JVM 字节码,或转换为 GPU 等其他目标。该系统是可扩展的,允许用户定义 DSL 和转换。查看publications 和斯坦福course notes。初步代码可在 Github 上获得,预计在未来几个月内发布。
【讨论】:
太好了,我用 while 循环替换了 for 理解,它的运行速度与 Java 版本完全相同(+/- 值得注意的是尾递归函数也和while循环一样快(因为两者都被转换为非常相似或相同的字节码)。 这也吸引了我一次。由于速度慢得令人难以置信,不得不将算法从使用集合函数转换为嵌套的 while 循环(第 6 级!)。这是需要重点针对的东西,恕我直言;如果我在需要不错的(注意:不是极快的)性能时不能使用它,那么好的编程风格有什么用? 那么for
什么时候合适?
@OscarRyz - scala 中的 for 在大多数情况下与 java 中的 for ( : ) 相同。以上是关于如何优化 Scala 中的 for-comprehensions 和循环?的主要内容,如果未能解决你的问题,请参考以下文章
尝试使用 ProGuard 优化 Java+Scala 时出现 java.lang.***Error