为啥我的 scala 期货效率不高?

Posted

技术标签:

【中文标题】为啥我的 scala 期货效率不高?【英文标题】:Why aren't my scala futures more efficient?为什么我的 scala 期货效率不高? 【发布时间】:2011-04-07 08:13:00 【问题描述】:

我在 32 位四核 Core2 系统上运行此 scala 代码:

def job(i:Int,s:Int):Long = 
  val r=(i to 500000000 by s).map(_.toLong).foldLeft(0L)(_+_)
  println("Job "+i+" done")
  r


import scala.actors.Future
import scala.actors.Futures._

val JOBS=4

val jobs=(0 until JOBS).toList.map(i=>future job(i,JOBS))
println("Running...")
val results=jobs.map(f=>f())
println(results.foldLeft(0L)(_+_))

(是的,我确实知道有很多更有效的方法来对一系列整数求和;这只是让 CPU 有事可做。

根据我设置的 JOBS,代码会在以下时间运行:

JOBS=1 : 31.99user 0.84system 0:28.87elapsed 113%CPU
JOBS=2 : 27.71user 1.12system 0:14.74elapsed 195%CPU
JOBS=3 : 33.19user 0.39system 0:13.02elapsed 257%CPU
JOBS=4 : 49.08user 8.46system 0:22.71elapsed 253%CPU

令我惊讶的是,这并不能真正扩展到“正在运行”的 2 个期货之外。我编写了很多多线程 C++ 代码,毫无疑问,如果我使用 Intel 的 TBB 或 boost::threads 编写这种东西,我会很好地扩展到 4 个内核,并且看到 >390% 的 CPU 利用率(它会更多当然是冗长的)。

那么:发生了什么事,我如何才能扩展至我希望看到的 4 个内核?这是否受到 scala 或 JVM 中的某些东西的限制?我突然想到,我实际上并不知道 scala 的期货在“哪里”运行……是每个未来产生的线程,还是“期货”提供专用于运行它们的线程池?

[我在带有 sun-java6 (6-20-0lennny1) 的 Lenny 系统上使用来自 Debian/Squeeze 的 scala 2.7.7 软件包。]

更新:

正如 Rex 的回答中所建议的,我重新编码以避免创建对象。

def job(i:Long,s:Long):Long = 
  var t=0L
  var v=i
  while (v<=10000000000L) 
    t+=v
    v+=s
  
  println("Job "+i+" done")
  t

// Rest as above...

这快得多了,我不得不显着增加迭代次数才能运行任意时间!结果是:

JOBS=1: 28.39user 0.06system 0:29.25elapsed 97%CPU
JOBS=2: 28.46user 0.04system 0:14.95elapsed 190%CPU
JOBS=3: 24.66user 0.06system 0:10.26elapsed 240%CPU
JOBS=4: 28.32user 0.12system 0:07.85elapsed 362%CPU

这更像是我希望看到的(尽管 3 个工作的案例有点奇怪,其中一个任务始终比其他两个任务早几秒钟完成)。

再进一步,在四核超线程 i7 上,带有JOBS=8 的后一个版本与 JOBS=1 相比实现了 x4.4 的加速,CPU 使用率为 571%。

【问题讨论】:

你只是不耐烦,想要今天的未来!说真的,雷克斯一针见血,你是在衡量垃圾收集,而不是期货效率。 嘿……太真实了。当我提交这个问题时,我使用 Scala 的时间并不长,并且可能对围绕它的一些极端炒作有点过于轻信了。 想用 akka.dispatch.Future 重新运行测试吗? @Viktor:这是在我访问 2.8 之前提交的。现在 2.8 出现了并行集合,并行计算的未来已经过时(当然,它们在管理异步方面仍然有很好的用途)。上次我查看 scala 并行性能时,我在这张海报的“性能”框中得到了一些(好的)结果:timday.bitbucket.org/project_euler-poster.pdf 正如下面 Rex 所指出的,真正的问题是对象流失是否会杀死垃圾收集器。有什么特别的原因 Akka 会在那里做得更好吗? 我听说 Akka 2.0 Futures 在某些情况下可以击败 ParCol。 Akka Futures 也使用 0 锁并且非常小。 【参考方案1】:

我的猜测是垃圾收集器比加法本身做的工作更多。所以你受到垃圾收集器可以管理的限制。尝试使用不会创建任何对象的东西再次运行测试(例如,使用 while 循环而不是 range/map/fold)。如果您的实际应用程序会严重影响 GC,您也可以使用并行 GC 选项。

【讨论】:

是的,看起来确实如此;请参阅第二版代码和问题更新结果。该问题最初出现在一些大量使用 BigInts 的代码中,因此没有太多机会消除那里的对象创建。没有意识到这种东西可能会产生多大的影响...... scala 似乎消除了对代码中大量显式 new-ing 的需要,因此很容易忘记它仍然存在。 编译器不应该优化这个new's away吗? @Elazar - 最终通过专业化,它可能可以在不创建对象的情况下运行(或类似的东西)。不过,就目前而言,这是不可避免的:代码是通用的,因此即使它只是对原语的包装,您也必须为其创建对象才能工作。【参考方案2】:

试试

(i to 500000000 by s).view.map(_.toLong).foldLeft(0L)(_+_)

view 的应用程序应该(据我了解 id)通过提供简单的包装器来避免重复迭代和对象创建。

另请注意,您可以使用 reduceLeft(_+_) 代替 fold。

【讨论】:

我还在 2.7.7; view 对我来说不是 Range 的成员(希望 2.8 有一天会出现在 Debian 存档中;我懒得从源代码构建,而我拥有的所有 Scala 书籍都大约是 2.7)。很高兴看到在这方面有一些改进。 (是的,除非有充分的理由,否则我通常更喜欢减少折叠;在这种情况下,这是因为无法保证函数的某些极端 i/s 参数不会导致减少操作的一个或零个元素)。 是的,我也注意到了。您可以在 tgz 中下载 Scala 2.8.1,您只需在任何地方解压即可(例如 /usr/share/)。然后在子文件夹bin 中创建指向脚本的符号链接,你就是黄金。我真的很感兴趣这三个变体在您的机器上的 2.8.1 上的表现如何(我自己没有四核)。

以上是关于为啥我的 scala 期货效率不高?的主要内容,如果未能解决你的问题,请参考以下文章

scala-cats EitherT:链接期货

如何将 Scala ARM 与期货一起使用?

Scala:带有地图的期货,用于 IO/CPU 绑定任务的 flatMap

期货未终止的Scala主类

Scala 期货 - 如何在完成时结束?

Scala 捕获一系列期货引发的错误