通过关联和&折叠/减少期货清单交换算子

时间:2014-03-30 04:05:12

标签: scala parallel-processing future reduce fold

请考虑以下事项:

import scala.concurrent._
import scala.concurrent.duration.Duration.Inf
import scala.concurrent.ExecutionContext.Implicits.global

def slowInt(i: Int) = { Thread.sleep(200); i }
def slowAdd(x: Int, y: Int) = { Thread.sleep(100); x + y }
def futures = (1 to 20).map(i => future(slowInt(i)))

def timeFuture(fn: => Future[_]) = {
  val t0 = System.currentTimeMillis
  Await.result(fn, Inf)
  println((System.currentTimeMillis - t0) / 1000.0 + "s")
}

以下两个print~2.5s:

// Use Future.reduce directly (Future.traverse is no different)
timeFuture { Future.reduce(futures)(slowAdd) }

// First wait for all results to come in, convert to Future[List], and then map the List[Int]
timeFuture { Future.sequence(futures).map(_.reduce(slowAdd)) }

据我所知,其原因是Future.reduce/traverse是通用的,因此使用关联运算符不会运行得更快,但是,有一种简单的方法来定义折叠/减少的计算只要至少有2个值可用(或fold的情况下为1),就会启动,这样当列表中的某些项目仍在生成时,已经生成的项目已经在计算?

3 个答案:

答案 0 :(得分:3)

Scalaz有一个期货的实现,包括一个chooseAny组合器,它收集期货的集合并返回第一个已完成元素和期货其余部分的元组的未来:

def chooseAny[A](h: Future[A], t: Seq[Future[A]]): Future[(A, Seq[Future[A]])]

Twitter的期货实施称为select。标准库不包含它(但请参阅Som Snytt上面指出的Viktor Klang的implementation)。我会在这里使用Scalaz的版本,但翻译应该是直截了当的。

根据需要运行操作的一种方法是从列表中提取两个已完成的项目,将其未来的总和推回到列表中,然后递归(请参阅this gist以获取完整的工作示例) :

def collapse[A](fs: Seq[Future[A]])(implicit M: Monoid[A]): Future[A] =
  Nondeterminism[Future].chooseAny(fs).fold(Future.now(M.zero))(
    _.flatMap {
      case (hv, tf) =>
        Nondeterminism[Future].chooseAny(tf).fold(Future.now(hv))(
          _.flatMap {
            case (hv2, tf2) => collapse(Future(hv |+| hv2) +: tf2)
          }
        )
    }
  )

在你的情况下你会打这样的话:

timeFuture(
  collapse(futures)(
    Monoid.instance[Int]((a, b) => slowAdd(a, b), 0)
  )
)

这在我的双核笔记本电脑上只需1.6秒即可完成,所以它按预期工作(即使slowInt所用的时间不同,也会继续做你想做的事。)

答案 1 :(得分:1)

为了获得类似的时间,我必须使用像(from here)这样的本地ExecutionContext:

implicit val ec = ExecutionContext.fromExecutor(Executors.newCachedThreadPool())

在那之后,我通过分配列表并在每个列表上开始工作来获得更好的性能,将它们分配给vals(基于记住for-comprehenion中的未来按顺序处理,除非它们被分配到vals之前换comprehenion)。由于列表的关联性质,我可以将它们与对同一函数的一次调用重新组合。我修改了timeFuture函数以获取描述并打印添加的结果:

def timeFuture(desc: String, fn: => Future[_]) = {
  val t0 = System.currentTimeMillis
  val res = Await.result(fn, Inf)
  println(desc + " = " + res + " in " + (System.currentTimeMillis - t0) / 1000.0 + "s")
}

我是Scala的新手,所以我还在努力在最后一步重复使用相同的功能(我认为应该可行)所以我作弊并创建了一个辅助函数:

def futureSlowAdd(x: Int, y: Int) = future(slowAdd(x, y))

然后我可以做以下事情:

timeFuture( "reduce", { Future.reduce(futures)(slowAdd) } )

val right = Future.reduce(futures.take(10))(slowAdd)
val left = Future.reduce(futures.takeRight(10))(slowAdd)
timeFuture( "split futures", (right zip left) flatMap (futureSlowAdd _).tupled)

使用here的最后一个拉链等。

我认为这是平行工作并重新组合结果。当我运行那些时,我得到:

reduce = 210 in 2.111s
split futures = 210 in 1.201s

我已经使用了一对硬编码的拍摄,但我的想法是整个列表拆分可以放入一个函数中并实际重新使用传递给左右分支的关联函数(允许略微不平衡的树)由于剩余的原因,最后。


当我将slowInt()slowAdd()函数随机化时,如:

def rand(): Int = Random.nextInt(3)+1
def slowInt(i: Int) = { Thread.sleep(rand()*100); i }
def slowAdd(x: Int, y: Int) = { Thread.sleep(rand()*100); x + y }

我仍然认为“分裂期货”比“减少”更早完成。启动时似乎有一些开销,这会影响第一次timeFuture呼叫。以下是一些运行它们的例子,启动惩罚超过“拆分期货”:

split futures = 210 in 2.299s
reduce = 210 in 4.7s

split futures = 210 in 2.594s
reduce = 210 in 3.5s

split futures = 210 in 2.399s
reduce = 210 in 4.401s

在比我的笔记本电脑更快的计算机上,在问题中使用相同的ExecutionContext我没有看到如此大的差异(没有慢*函数中的随机化):

split futures = 210 in 2.196s
reduce = 210 in 2.5s

这里的“拆分期货”看起来只是一点点。


最后一次。这是一个功能(又称憎恶),它扩展了我上面的想法:

def splitList[A <: Any]( f: List[Future[A]], assocFn: (A, A) => A): Future[A] = {
    def applyAssocFn( x: Future[A], y: Future[A]): Future[A] = {
      (x zip y) flatMap( { case (a,b) => future(assocFn(a, b)) } )
    }
    def divideAndConquer( right: List[Future[A]], left: List[Future[A]]): Future[A] = {
      (right, left) match {
        case(r::Nil, Nil) => r
        case(Nil, l::Nil) => l
        case(r::Nil, l::Nil) => applyAssocFn( r, l )
        case(r::Nil, l::ls) => {
          val (l_right, l_left) = ls.splitAt(ls.size/2)
          val lret = applyAssocFn( l, divideAndConquer( l_right, l_left ) )
          applyAssocFn( r, lret )
        }
        case(r::rs, l::Nil) => {
          val (r_right, r_left) = rs.splitAt(rs.size/2)
          val rret = applyAssocFn( r, divideAndConquer( r_right, r_left ) )
          applyAssocFn( rret, l )
        }
        case (r::rs, l::ls) => {
          val (r_right, r_left) = rs.splitAt(rs.size/2)
          val (l_right, l_left) = ls.splitAt(ls.size/2)
          val tails = applyAssocFn(divideAndConquer( r_right, r_left ), divideAndConquer( l_right, l_left ))
          val heads = applyAssocFn(r, l)
          applyAssocFn( heads, tails )
        }
      }
    }
    val( right, left ) = f.splitAt(f.size/2)
    divideAndConquer( right, left )
  }

将Scala中的所有内容递归以非递归方式将列表拆分并尽快将期货分配给值(启动它们)。

当我测试它时:

timeFuture( "splitList", splitList( futures.toList, slowAdd) )

我使用newCachedThreadPool()

在笔记本电脑上获得以下时间
splitList = 210 in 0.805s
split futures = 210 in 1.202s
reduce = 210 in 2.105s

我注意到“拆分期货”时间可能无效,因为期货是在timeFutures区块之外开始的。但是,应在splitList函数内正确调用timeFutures函数。对我来说,最重要的是选择最适合硬件的ExecutionContext。

答案 2 :(得分:1)

下面的答案将在一台20核机器上运行700毫秒,这给出了需要按顺序运行的内容以及可以在任何具有任何实现的机器上执行的操作(20个并行200ms slowInt调用后跟5嵌套100ms slowAdd次调用)。它在我的4核机器上运行1600ms,这和那台机器一样。

展开slowAdd来电时,f代表slowAdd

f(f(f(f(f(x1, x2), f(x3, x4)), f(f(x5, x6), f(x7, x8))), f(f(f(x9, x10), f(x11, x12)), f(f(x13, x14), f(x15, x16)))), f(f(x17, x18), f(x19, x20)))

您提供的使用Future.sequence的示例将在20核计算机上运行2100毫秒(20个并行200毫秒slowInt个呼叫,然后是19个嵌套的100毫秒slowAdd个呼叫。它在我的4核机器上运行2900ms。

展开slowAdd来电时,f代表slowAdd

f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(f(x1, x2), x3), x4), x5), x6), x7), x8), x9), x10), x11), x12), x13), x14), x15) x16) x17) x18) x19) x20)

Future.reduce方法调用Future.sequence(futures).map(_ reduceLeft op),因此您提供的两个示例是等效的。

我的回答使用combine函数,该函数获取期货列表和op,这是一个将两个期货合并为一个参数的函数。该函数返回应用于所有期货对和成对货币对的op,依此类推,直到剩下一个未来,并返回:

def combine[T](list: List[Future[T]], op: (Future[T], Future[T]) => Future[T]): Future[T] =
  if (list.size == 1) list.head
  else if(list.size == 2) list.reduce(op)
  else list.grouped(2).map(combine(_, op)).reduce(op)

注意:我修改了一些代码以符合我的风格偏好。

def slowInt(i: Int): Future[Int] = Future { Thread.sleep(200); i }
def slowAdd(fx: Future[Int], fy: Future[Int]): Future[Int] = fx.flatMap(x => fy.map { y => Thread.sleep(100); x + y })
var futures: List[Future[Int]] = List.range(1, 21).map(slowInt)

以下代码为您的案例使用combine函数:

timeFuture(combine(futures, slowAdd))

以下代码更新了我的修改Future.sequence示例:

timeFuture(Future.sequence(futures).map(_.reduce{(x, y) => Thread.sleep(100); x + y }))