scalablitz

100
ScalaBlitz Efficient Collections Framework

Upload: aleksandar-prokopec

Post on 02-Jul-2015

336 views

Category:

Software


0 download

DESCRIPTION

Scala eXchange 2013 presentation of the ScalaBlitz - a highly optimized data-parallel collection framework for Scala.

TRANSCRIPT

Page 1: ScalaBlitz

ScalaBlitzEfficient Collections Framework

Page 2: ScalaBlitz

What’s a Blitz?

Page 3: ScalaBlitz

Blitz-chess is a style of rapid chess play.

Page 4: ScalaBlitz

Blitz-chess is a style of rapid chess play.

Page 5: ScalaBlitz

Knights have horses.

Page 6: ScalaBlitz

Horses run fast.

Page 7: ScalaBlitz

def mean(xs: Array[Float]): Float = xs.par.reduce(_ + _) / xs.length

Page 8: ScalaBlitz
Page 9: ScalaBlitz
Page 10: ScalaBlitz

With Lists, operations can only be executed

from left to right

Page 11: ScalaBlitz

1 2 4 8

Page 12: ScalaBlitz

1 2 4 8

Not your typical list.

Page 13: ScalaBlitz

Bon app.

Page 14: ScalaBlitz
Page 15: ScalaBlitz
Page 16: ScalaBlitz
Page 17: ScalaBlitz
Page 18: ScalaBlitz

Apparently not enough

Page 19: ScalaBlitz
Page 20: ScalaBlitz

No amount of documentation is

apparently enough

Page 21: ScalaBlitz
Page 22: ScalaBlitz

The reduceLeftguarantees operations are executed from left to right

Page 23: ScalaBlitz

Parallel and sequential collections sharing operations

Page 24: ScalaBlitz

There are several problems here

Page 25: ScalaBlitz

How we see users

Page 26: ScalaBlitz

How userssee the docs

Page 27: ScalaBlitz

Bending the truth.

Page 28: ScalaBlitz

And sometimes we were just slow

Page 29: ScalaBlitz

So, we have a new API now

def findDoe(names: Array[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 30: ScalaBlitz

Wait, you renamed a method?

def findDoe(names: Array[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 31: ScalaBlitz

Yeah, par already exists.But, toPar is different.def findDoe(names: Array[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 32: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

implicit class ParOps[Repr](val r: Repr) extends AnyVal { def toPar = new Par(r)}

Page 33: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { ParOps(names).toPar.find(_.endsWith(“Doe”))}

implicit class ParOps[Repr](val r: Repr) extends AnyVal { def toPar = new Par(r)}

Page 34: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { ParOps(names).toPar.find(_.endsWith(“Doe”))}

implicit class ParOps[Repr](val r: Repr) extends AnyVal { def toPar = new Par(r)}

class Par[Repr](r: Repr)

Page 35: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { (new Par(names)).find(_.endsWith(“Doe”))}

implicit class ParOps[Repr](val r: Repr) extends AnyVal { def toPar = new Par(r)}

class Par[Repr](r: Repr)

Page 36: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { (new Par(names)).find(_.endsWith(“Doe”))}

class Par[Repr](r: Repr)

But, Par[Repr] does not have the find method!

Page 37: ScalaBlitz

True, but Par[Array[String]]does have a find method.

def findDoe(names: Array[String]): Option[String] = { (new Par(names)).find(_.endsWith(“Doe”))}

class Par[Repr](r: Repr)

Page 38: ScalaBlitz

def findDoe(names: Array[String]): Option[String] = { (new Par(names)).find(_.endsWith(“Doe”))}

class Par[Repr](r: Repr)

implicit class ParArrayOps[T](pa: Par[Array[T]]) { ... def find(p: T => Boolean): Option[T] ...}

Page 39: ScalaBlitz

More flexible!

Page 40: ScalaBlitz

More flexible!● does not have to implement methods that

make no sense in parallel

Page 41: ScalaBlitz

More flexible!● does not have to implement methods that

make no sense in parallel● slow conversions explicit

Page 42: ScalaBlitz

No standard library collections were hurt doing this.

No standard library collections were hurt doing this.

Page 43: ScalaBlitz

More flexible!● does not have to implement methods that

make no sense in parallel● slow conversions explicit● non-intrusive addition to standard library

Page 44: ScalaBlitz

More flexible!● does not have to implement methods that

make no sense in parallel● slow conversions explicit● non-intrusive addition to standard library● easy to add new methods and collections

Page 45: ScalaBlitz

More flexible!● does not have to implement methods that

make no sense in parallel● slow conversions explicit● non-intrusive addition to standard library● easy to add new methods and collections● import switches between implementations

Page 46: ScalaBlitz

def findDoe(names: Seq[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 47: ScalaBlitz

def findDoe(names: Seq[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 48: ScalaBlitz

But how do I write generic code?

def findDoe(names: Seq[String]): Option[String] = { names.toPar.find(_.endsWith(“Doe”))}

Page 49: ScalaBlitz

def findDoe[Repr[_]](names: Par[Repr[String]]) = { names.toPar.find(_.endsWith(“Doe”))}

Page 50: ScalaBlitz

Par[Repr[String]]does not have a find

def findDoe[Repr[_]](names: Par[Repr[String]]) = { names.toPar.find(_.endsWith(“Doe”))}

Page 51: ScalaBlitz

def findDoe[Repr[_]: Ops](names: Par[Repr[String]]) = { names.toPar.find(_.endsWith(“Doe”))}

Page 52: ScalaBlitz

def findDoe[Repr[_]: Ops](names: Par[Repr[String]]) = { names.toPar.find(_.endsWith(“Doe”))}

We don’t do this.

Page 53: ScalaBlitz

Make everything as simple as possible, but not simpler.

Page 54: ScalaBlitz

def findDoe(names: Reducable[String])= { names.find(_.endsWith(“Doe”))}

Page 55: ScalaBlitz

def findDoe(names: Reducable[String])= { names.find(_.endsWith(“Doe”))}

findDoe(Array(1, 2, 3).toPar)

Page 56: ScalaBlitz

def findDoe(names: Reducable[String])= { names.find(_.endsWith(“Doe”))}

findDoe(toReducable(Array(1, 2, 3).toPar))

Page 57: ScalaBlitz

def findDoe(names: Reducable[String])= { names.find(_.endsWith(“Doe”))}

findDoe(toReducable(Array(1, 2, 3).toPar))

def arrayIsReducable[T]: IsReducable[T] = { … }

Page 58: ScalaBlitz

So let’s write a program!

Page 59: ScalaBlitz
Page 60: ScalaBlitz

import scala.collection.par._

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) {

}

Page 61: ScalaBlitz

import scala.collection.par._

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) { val x = idx % wdt val y = idx / wdt }

Page 62: ScalaBlitz

import scala.collection.par._

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) { val x = idx % wdt val y = idx / wdt pixels(idx) = computeColor(x, y)}

Page 63: ScalaBlitz

import scala.collection.par._

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) { val x = idx % wdt val y = idx / wdt pixels(idx) = computeColor(x, y)}

Scheduler not found!

Page 64: ScalaBlitz

import scala.collection.par._import Scheduler.Implicits.global

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) { val x = idx % wdt val y = idx / wdt pixels(idx) = computeColor(x, y)}

Page 65: ScalaBlitz

import scala.collection.par._import Scheduler.Implicits.global

val pixels = new Array[Int](wdt * hgt)for (idx <- (0 until (wdt * hgt)).toPar) { val x = idx % wdt val y = idx / wdt pixels(idx) = computeColor(x, y)}

Page 66: ScalaBlitz

New parallel collections 33% faster!

Now103 ms

Previously148 ms

Page 67: ScalaBlitz

Workstealing tree scheduler rocks!

Page 68: ScalaBlitz

Workstealing tree scheduler rocks!

But, are there other interesting workloads?

Page 69: ScalaBlitz

Fine-grained uniform workloads are on the opposite

side of the spectrum.

Page 70: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.toPar.fold(0)(_ + _) sum / xs.length}

Page 71: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.toPar.fold(0)(_ + _) sum / xs.length}

Now15 ms

Previously565 ms

Page 72: ScalaBlitz

But how?

Page 73: ScalaBlitz

def fold[T](a: Iterable[T])(z:T)(op: (T, T) => T) = {

var it = a.iterator

var acc = z

while (it.hasNext) {

acc = op(acc, it.next)

}

acc

}

Page 74: ScalaBlitz

def fold[T](a: Iterable[T])(z:T)(op: (T, T) => T) = {

var it = a.iterator

var acc = z

while (it.hasNext) {

acc = box(op(acc, it.next))

}

acc

}

Page 75: ScalaBlitz

def fold[T](a: Iterable[T])(z:T)(op: (T, T) => T) = {

var it = a.iterator

var acc = z

while (it.hasNext) {

acc = box(op(acc, it.next))

}

acc

}

Generic methods cause boxing of primitives

Page 76: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.toPar.fold(0)(_ + _) sum / xs.length}

Page 77: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.toPar.fold(0)(_ + _) sum / xs.length}

Generic methods hurt performanceWhat can we do instead?

Page 78: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.toPar.fold(0)(_ + _) sum / xs.length}

Generic methods hurt performanceWhat can we do instead?

Inline method body!

Page 79: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var it = xs.iterator

var acc = 0

while (it.hasNext) {

acc = acc + it.next

}

acc

} sum / xs.length}

Page 80: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var it = xs.iterator

var acc = 0

while (it.hasNext) {

acc = acc + it.next

}

acc

} sum / xs.length}

Specific typeNo boxing!No memory allocation!

Page 81: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var it = xs.iterator

var acc = 0

while (it.hasNext) {

acc = acc + it.next

}

acc

} sum / xs.length}

Specific typeNo boxing!No memory allocation!

2X speedup

565 ms → 281 ms

Page 82: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var it = xs.iterator

var acc = 0

while (it.hasNext) {

acc = acc + it.next

}

acc

} sum / xs.length}

Page 83: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var it = xs.iterator

var acc = 0

while (it.hasNext) {

acc = acc + it.next

}

acc

} sum / xs.length}

Iterators? For Array?We don’t need them!

Page 84: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var i = 0

val until = xs.size

var acc = 0

while (i < until) {

acc = acc + a(i)

i = i + 1

}

acc

} sum / xs.length}

Use index-based access!

Page 85: ScalaBlitz

def mean(xs: Array[Float]): Float = {

val sum = {

var i = 0

val until = xs.size

var acc = 0

while (i < until) {

acc = acc + a(i)

i = i + 1

}

acc

} sum / xs.length}

19x speedup

Use index-based access!

281 ms → 15 ms

Page 86: ScalaBlitz

Are those optimizations parallel-collections specific?

Page 87: ScalaBlitz

Are those optimizations parallel-collections specific?

No

Page 88: ScalaBlitz

Are those optimizations parallel-collections specific?

No

You can use them on sequential collections

Page 89: ScalaBlitz

def mean(xs: Array[Float]): Float = { val sum = xs.fold(0)(_ + _) sum / xs.length}

Page 90: ScalaBlitz

import scala.collections.optimizer._def mean(xs: Array[Float]): Float = optimize{ val sum = xs.fold(0)(_ + _) sum / xs.length}

Page 91: ScalaBlitz

import scala.collections.optimizer._def mean(xs: Array[Float]): Float = optimize{ val sum = xs.fold(0)(_ + _) sum / xs.length}

You get 38 times speedup!

Page 92: ScalaBlitz

Future work

Page 93: ScalaBlitz

@specialized collections

● Maps● Sets● Lists● Vectors

Both faster & consuming less memory

Page 94: ScalaBlitz

@specialized collections

● Maps● Sets● Lists● Vectors

Both faster & consuming less memory

Expect to get this for free inside optimize{} block

Page 95: ScalaBlitz

jdk8-style streams(parallel views)

● Fast● Lightweight● Expressive API● Optimized

Lazy data-parallel operations made easy

Page 96: ScalaBlitz

Future’s based asynchronous API

val sum = future{ xs.sum }val normalized = sum.andThen(sum => sum/xs.size)

Boilerplate code, ugly

Page 97: ScalaBlitz

Future’s based asynchronous APIval sum = xs.toFuture.sumval scaled = xs.map(_ / sum)

● Simple to use● Lightweight● Expressive API● Optimized

Asynchronous data-parallel operations made easy

Page 98: ScalaBlitz

Current research: operation fusionval minMaleAge = people.filter(_.isMale)

.map(_.age).minval minFemaleAge = people.filter(_.isFemale)

.map(_.age).min

Page 99: ScalaBlitz

Current research: operation fusionval minMaleAge = people.filter(_.isMale)

.map(_.age).minval minFemaleAge = people.filter(_.isFemale)

.map(_.age).min

● Requires up to 3 times more memory than original collection● Requires 6 traversals of collections

Page 100: ScalaBlitz

Current research: operation fusionval minMaleAge = people.filter(_.isMale)

.map(_.age).minval minFemaleAge = people.filter(_.isFemale)

.map(_.age).min

● Requires up to 3 times more memory than original collection● Requires 6 traversals of collections

We aim to reduce this to single traversal with no additional memory. Without you changing your code