Ken Wakita (https://prg1-2019.github.io/lecture/web/)
2019.11.4
並行計算と並列実行
スレッド
Future 計算
データ並列処理:並列コレクション
並行 (concurrent) | 並列 (parallel) |
---|---|
論理的な概念 | 物理的な概念 |
計算の間にデータ依存関係がないこと | 同時に実行すること |
処理を分離して表現できること | 高速実行を目的とする |
スレッド (thread): 並列実行の単位。
複数のスレッドを並列実行する方式をマルチスレッドと呼ぶ
Future オブジェクト:どこかで計算を実行し、いずれ計算が終わった暁には、その答えをくれるオブジェクト。
計算結果の型が T
のとき、Future オブジェクトの型は Future[T]
f()
を実行する。
g()
を計算するための Future オブジェクトを生成する。
計算資源に余裕がある(つまり、暇なプロセッサがある)場合はFuture オブジェクトはすぐに g()
の計算を開始する。このとき、f()
とg()
の計算は並列実行される。
(Future オブジェクトの計算の完了を待たずに)h()
の計算を始める。
計算資源に余裕があり、g()
とh()
の計算が重い場合はこれらの二つの計算は並列実行される。
System.nanoTime()
実行開始からの経過時間をナノ秒単位で取得
Thread.sleep(Xミリ秒)
スレッドの実行を引数で指定したミリ秒間だけ停止
sbt:lx10> runMain Bakery loop
sbt:lx10> runMain Bakery seq
店主:店を開けて、時間が来たら店を閉じる
こね方:3単位時間ごとにパン生地をこねる
焼き方:5単位時間ごとにパンを焼く
売り方:7単位時間ごとにパンを店に出す
配達:10単位時間ごとにレストランにパンを届ける
sbt:lx10> runMain Bakery conc
sbt:lx10> runMain Bakery conc
sbt:lx10> runMain Bakery conc
list.par
list
: 普通のリスト。長さ 1,000 で、要素として 1, 2, …, 1,000 を持っている
plist
: 並列リスト。list
と同じ長さ、同じ内容だが、多くのメソッドが並列化されている。
vec.par
vec
: 普通の配列。長さ N で、要素として 0, 1, …, N-1 を持っている
pvec
: 並列配列。vec
と同じ大きさ、同じ内容だが、多くのメソッドが並列化されている。
val c = 100; var a = 0
val t_start = System.nanoTime()
for (i <- 1 to c) {
val vecfib = vec.map((v: Int) => fib(v % 1000, 1, 1, 1))
a = a + vecfib(Random.nextInt(vecfib.length))
}
println(f"${(System.nanoTime() - t_start) * 1e-9}%2.2fsec")
list
の各要素 \(v\) についてフィボナッチ数を計算した結果を収集
a
: 配列から無作為に選択した要素の値。scalaコンパイラの最適化器がフィボナッチ数の計算結果がどこでも使用されていないことに気づいた場合は、計算を省略する最適化を施す可能性がある。一見、無駄なa
を計算取得することでそのような最適化を抑制している。
1.55 sec (1.7 GHz Intel Core i7, 2 cores)
1.37 sec (1.6 GHz Intel Core i5, 2 cores)
1.05 sec (4.0 GHz Intel Core i7, 4 cores)
上のふたつの PC に対して 4-5倍の性能があるはずだが、実行速度はほとんど差がでない。
val c = 100; var a = 0
val t_start = System.nanoTime()
for (i <- 1 to c) {
val vecfib = pvec.map((v: Int) => fib(v % 1000, 1, 1, 1))
a = a + vecfib(Random.nextInt(vecfib.length))
}
println(f"${(System.nanoTime() - t_start) * 1e-9}%2.2fsec")
pvec.map
) を使う点だけ。1.55 → 0.73 sec (1.7 GHz Intel Core i7, 2 cores) – 2.1倍の高速化
1.37 → 0.67 sec (1.6 GHz Intel Core i5, 2 cores) – 2.0倍の高速化
1.05 → 0.31 sec (4.0 GHz Intel Core i7, 4 cores) – 3.4倍の高速化
2 cores の機械に対しても2倍以上の速度が出ている(本当は4倍以上の性能が出て欲しいところだけど)
文字列のリストに対する並列map
sbt:lx10> runMain Par map
val lastNames = List("Smith","Jones","Frankenstein","Bach","Jackson","Rodin").par
print(lastNames.map((name: String) => name.toUpperCase))
数値配列上の並列fold
sbt:lx10> runMain Par fold
並列計算の最中に共有変数を更新するのは危険
sbt:lx10> runMain Par sideeffect
// 1.7 GHz Intel Core i7 2 cores
sum = 500500, 498710, 500500
// 4.0 GHz Intel Core i7 4 cores
sum = 489624, 498601, 495584
正しい結果は 500,500
並列スレッド群が共有変数 sum
に同時に代入するときに一方の代入が無視される可能性がある
Out of order 実行:並列計算の順序が逐次実行の時と異なること
並列配列、並列リスト等への並列計算は out of order 実行
reduce 処理では結合律 \(x \oplus (y \oplus z) = (x \oplus y) \oplus z\) が必須
sbt:lx10> runMain par assoc
for (i <- 1 to 3) println(plist.reduce((accu: Int, v: Int) => accu + v))
// 結果: (500500, 500500, 500500)
// 結合律が成立しない演算: x - (y - z) != (x - y) - z
for (i <- 1 to 3) println(plist.reduce((accu: Int, v: Int) => accu - v))
// 結果は滅茶苦茶: (0, -144890, 497564)
// 交換率は成立しないが: s1 ++ s2 != s2 ++ s1
// 結合律は成立する場合: s1 ++ (s2 ++ s3) == (s1 ++ s2) ++ s3
val strings = List("abc","def","ghi","jkl","mno","pqr","stu","vwx","yz").par
println(f"${strings.reduce((s1: String, s2: String) => s1 ++ " " ++ s2)}")