Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

あなたのScalaを爆速にする7つの方法(日本語版)

10,568 views

Published on

ScalaMatsuri2016でお話しさせて頂いた、あなたのScalaを爆速にする7つの方法の英語版です。
7つの角度でベンチマークを取っています。

Published in: Engineering
  • Be the first to comment

あなたのScalaを爆速にする7つの方法(日本語版)

  1. 1. あなたのScalaを爆速に する7つの方法 x1(井上 ゆり) For ScalaMatsuri 2016 7 ways to make your Scala RedHot high velocity
  2. 2. 井上 ゆり 株式会社サイバーエージェント アドテクスタジオ AMoAd. twitter: @iyunoriue GitHub: x1- HP: バツイチとインケンのエンジニアブログ http://x1.inkenkun.com/ profile I m Yuri Inoue.
  3. 3. • このスライドに出てくる速度やパフォーマンスは並列実行 を考慮していません。 • このセッションはScalaビギナーを対象としています。 • ベンチマークを取得した環境は次ページに表示します。 • パフォーマンスについて@Keenからいくつかのアドバイスを 頂きました。ありがとう! acknowledgement ack. Parallel execution is not taken into account.
  4. 4. environment of benchmark インスタンス Amazon EC2 m3.2xlarge vCPU 8 memory 30GB ディスク 汎用SSD OS CentOS6.7-Final jdk jdk1.8.0u65(oracle, server vm) scala 2.11.7 ビルドツール sbt ベンチマークツール sbt-jmh 0.2.5 使用ライブラリ com.bionicspirit.shade:1.6.0 net.debasishg.redisclient:3.0 org.scalatest.scalatest:2.2.4 environment of benchmark.
  5. 5. No.1 ランダム・リード - ファイル vs KVS -
  6. 6. Q1 2GBのデータの中に、 ある文字列がいくつ存在す るか数えます。 次のうちどちらの方が速い でしょう? When searching for string, which one would be faster?
  7. 7. SSD上の 2GBファイル A memcache上の 2GBデータ (チャンク分割) B 前提条件 前提条件 * SSDはAmazon EC2の 汎用SSDです。 * RamDiskは使ってい ません。 * memcacheのバージョ ンは1.4.0です。 * memcacheはローカル に存在します。 A:from 2GB file on SSD B:2GB data on memcache
  8. 8. Answer A. SSD上の2GBファイル Ans. A. from 2GB file on SSD.
  9. 9. benchmark 上記のグラフは、下記のような2GBのデータから”SUIKA” という文字列を探したときの平均タイムです。 • ファイルの場合は8秒未満で完了します。 • memcacheの場合は19秒近くかかりました。 • memcacheと同じKVSのRedisでは14秒弱でした。 '''<code>&amp;h</code>''' を用い、<code>&amp;h0F</code> (十進で15)のように表現する。 nn[[Standard Generalized Markup Language|SGML]]、[[Extensible Markup Language|XML]]、 [[HyperText Markup Language|HTML]]では、アンパサンドをSUIKA使って[[SGML実体]]を参照する。 the average for the time searching string - SUIKA .
  10. 10. Why is file faster than memcache? • ファイルの読み込みに Memory Mapped File(MMF) を利用しまし た。 • Memory Mapped File は、ファイルをメモリの連続領域にマッピ ングして操作する機能です。 • ユーザ空間での不必要なファイルI/O操作なしにディスク上の ファイルの内容をバッファリングすることができます。 • java.nioパッケージには Memory Mapped File にダイレクト・ アクセスするMappedByteBufferがあります。 物理 ファイル メモリ空間にマッピング This is Memory Mapped File(MMF) power.
  11. 11. • memcache(<1.4.2)は下記の制約があります。 1キーに格納できる最大サイズは1MBです. • 同一ホスト上のmemcacheだとしても、何度もデータを取得する のはそれなりにコストがかかります。 • 1キーに1GBまで格納できるRedisを使って、2GBのデータを500MB づつ4キーに分割し、検索したときのベンチマークが下記です。 • 8秒強で完了しますが、それでもMMFには勝てませんでした。 Why is memcache slower than file? memcache(<1.4.2) has 1key=1MB limitation.
  12. 12. • しかし、JVM環境でのMemory Mapped Fileは2GBまでしか マッピングできません。 http://stackoverflow.com/questions/8076472/why-does- filechannel-map-take-up-to-integer-max-value-of-data • Apache Spark MLlib開発責任者のReynold XinさんがGistに ByteBufferシリーズのパフォーマンス計測を記述していま す。 これはScalaで書かれていてとても参考になります。 https://gist.github.com/rxin/5087680 more information MMF can map only map up to 2GB file on JVM.
  13. 13. 爆速 No.1 Memory Mapped Fileで 高速ファイル操作が可能に! No.1 using MMF, you can operate on files at high speed!
  14. 14. No.2 for内包表記 vs flatMap & map
  15. 15. Q2 for内包表記とflatMap & map は結果同じ挙動をとりますが どちらが速いでしょう? for comprehension and flatMap&map which is faster?
  16. 16. A B code 同じ flatMap & map for {
 a <- data
 b <- a
 } yield {
 b
 } for内包表記 data.flatMap( a => a.map( b => b ) ) flatMap & map
  17. 17. Answer A. 同じ Ans. A.same.
  18. 18. benchmark Throughput、Average、Sample のいずれも for内包表 記 と flatMap & map で優位差が見られませんでした。 10,000回 The benchmark doesn t show significant difference.
  19. 19. • for内包表記 と flatMap & map は論理的に同じです。 • 下記はデコンパイルしたコードですが全く同じです。 Why are they the same speed? for comprehension and flatMap&map are logically same. public Option<String> forComprehension()
 {
 data().flatMap(new AbstractFunction1()
 {
 public static final long serialVersionUID = 0L;
 
 public final Option<String> apply(Some<String> a)
 {
 a.map(new AbstractFunction1()
 {
 public static final long serialVersionUID = 0L;
 
 public final String apply(String b)
 {
 return b;
 }
 });
 }
 });
 } public Option<String> flatMapAndMap()
 {
 data().flatMap(new AbstractFunction1()
 {
 public static final long serialVersionUID = 0L;
 
 public final Option<String> apply(Some<String> a)
 {
 a.map(new AbstractFunction1()
 {
 public static final long serialVersionUID = 0L;
 
 public final String apply(String b)
 {
 return b;
 }
 });
 }
 });
 } for comprehension flatMap & map
  20. 20. 爆速 No.2 for内包表記 と flatMap & map は バイト・コードのレベルで同じです。 No.2 for comprehension and flatMap&map are same.
  21. 21. No.3 追加と挿入 - collection -
  22. 22. ScalaにおけるCollectionの性能特性 引用: http://docs.scala-lang.org/ja/overviews/collections/performance-characteristics.html Collections Performance Characteristics at Scala.
  23. 23. Q3-1 可変な “var xs: Vector” と 不変な “val xs: ArrayBuffer” 末尾追加が速いのはどちらでしょう? When appending Vector or ArrayBuffer, which is faster?
  24. 24. var Vector A val ArrayBuffer B code var xs = Vector.empty[Int] xs = xs :+ a var xs: Vector val xs: ArrayBuffer val xs = ArrayBuffer.empty[Int] xs += a
  25. 25. Answer B. val ArrayBuffer Ans. B.val ArrayBuffer.
  26. 26. benchmark ArrayBuffer の方が Vector よりも速いです。 ArrayBuffer is faster than Vector. n回追加したときのThroughput 上記のグラフはN個の要素を追加したときのスループットを表しています。 例えばtypeがVector, timesが10kの場合では、10,000個の要素を空のVectorに追 加したときのスループットを意味しています。
  27. 27. benchmark 他のimmutableオブジェクトのベンチマークは下記のとおり です。 Vector is faster than List and Stream. n回追加したときのThroughput Vector は同じimmutableオブジェクトの List や Stream よりは速いです。
  28. 28. Why is ArrayBuffer faster than Vector? 新しく要素を追加するときのコードを比較してみます。 These processes are absolutely different. 新しいインスタンスに既存の 要素をコピーしてから新しい 要素を追加します。 インスタンスのリサイズを行っ てから末尾の要素を新要素で 更新します。 var Vector val ArrayBuffer val b = bf(repr)
 b ++= thisCollection
 b += elem
 b.result() ensureSize(size0 + 1)
 array(size0) = elem.asInstanceOf[AnyRef]
 size0 += 1
 this Vector と ArrayBuffer でプロセスが全く異なります。
  29. 29. Q3-2 可変な “var xs: List” と 不変な “val xs: ListBuffer” 先頭挿入が速いのはどちらでしょう? When inserting into List or ListBuffer, which is faster?
  30. 30. var List A val ListBuffer B code var xs = List.empty[Int] xs = a :: xs var xs: List val xs: ListBuffer val xs = ListBuffer.empty[Int] a +=: xs
  31. 31. Answer A. var List Ans. A.var List
  32. 32. benchmark List の方が ListBuffer よりも速いです。 List is faster than ListBuffer. n回先頭挿入したときのThroughput 上記のグラフはN個の要素を先頭に追加したときのスループットを表しています。 例えばtypeがList, timesが1kの場合では、1,000個の要素を空のListの先頭に挿 入していったときのスループットを意味しています。
  33. 33. benchmark 他のオブジェクトのベンチマークは下記のとおりです。 List is enormously faster than the others. List だけ飛び抜けて速いです。 n回先頭挿入したときのThroughput
  34. 34. Why is List faster than the others? 要素を先頭挿入するときのコードを比較してみます。 List calculate a little, in case of inserting. Listは先頭とその他の要素を 別管理しているため、新しい 要素をすぐに作れます。 ListBufferはListとほとんど 一緒なのですが内部変数の再 代入が行われます。 var List val ListBuffer new scala.collection.immutable .::(x, this) if (exported) copy()
 val newElem = new :: (x, start)
 if (isEmpty) last0 = newElem
 start = newElem
 len += 1
 this List ではほとんど計算せずに先頭挿入が行えます。
  35. 35. 爆速 No.3 末尾追加のときは ArrayBuffer や ListBuffer を使うと効率が良いです。 しかし先頭挿入のときは List を使うと 良いパフォーマンスが得られます。 No.3 appending => **Buffer, inserting => List are nice.
  36. 36. No.4 削除 - collection -
  37. 37. Q4 可変な “var xs: List” と 不変な “val xs: ListBuffer” 削除が速いのはどちらでしょう? When removing from List or ListBuffer, which is faster?
  38. 38. var List A val ListBuffer B code var xs: List val xs: ListBuffer var xs = List( 1 to n: _* ) // head xs = xs.tail // tail xs = xs.dropRight(0) var xs = ListBuffer( 1 to n: _* ) // head xs.remove(0) // tail xs.remove(xs.size - 1)
  39. 39. Answer B. val ListBuffer Ans. B.val ListBuffer
  40. 40. benchmark ListBuffer は List よりも相当速いです。 ListBuffer is much faster than List. n回削除したときのThroughput 上記のグラフはN個の要素をサイズNのコレクションから削除したときのスループットを 表しています。 例えば、Benchmark:removeTail, type:List, times:1kの場合では サイズ1,000のList の末尾から1,000回要素削除を行ったときのスループットを意味します。
  41. 41. benchmark 他のオブジェクトのベンチマークは下記のとおりです。 They are too slow, expecting Buffer family. Buffer 系以外はどれも超絶遅いです。 n回削除したときのThroughput
  42. 42. Why is ListBuffer faster than List? 要素を削除するときのコードを比較してみます。 The operation dropRight of List takes time O(n). ListのdropRightの操作はO(n) の時間がかかります。 ListBufferのremove操作は定 数時間です。 var List val ListBuffer def dropRight(n: Int): Repr = {
 val b = newBuilder
 var these = this
 var lead = this drop n
 while (!lead.isEmpty) {
 b += these.head
 these = these.tail
 lead = lead.tail
 }
 b.result()
 } def remove(n: Int): A = { : var cursor = start
 var i = 1
 while (i < n) {
 cursor = cursor.tail
 i += 1
 }
 old = cursor.tail.head
 if (last0 eq cursor.tail) last0 = cursor.asInstanceOf[::[A]]
 cursor.asInstanceOf[::[A]].tl = cursor.tail.tail
  43. 43. benchmark of List’s dropRight このベンチマークは10倍づつサイズを増やしているので、ス ループットも底10で対数表示します。 Throughput is just only lowered to linear. スループットはただ線形に下降しているだけです。 操作時間線形増加の恐怖です。 Q3で出てきたVectorの末尾追加も線形増加なのですが、同じ線形増加でも削除のほうが 増加率が大きいので、パフォーマンスがすごく落ちているように感じます。 List - dropRightのlog(Throughput)
  44. 44. reference benchmark Comparing dropRight with take. dropRight(1) と take(n-1) で同じ機能を実現できる ので、dropRight と take を比較しました。 take は dropRight より少しだけ速いです。 Throughput of List’ dropRight and take
  45. 45. 爆速 No.4 たくさんの要素を削除するときは ListBuffer や ArrayBuffer を使うと 効率が良いです。 No.4 When removing element, **Buffer are good. 削除に限らずなんですが、基本的にはimmutableなオブジェクトよりもmutableな Buffer系の方が書き込み操作は速いです。
  46. 46. No.5 ランダム・リード - collection -
  47. 47. Q5 Vector と ListBuffer ランダム・リードが速いのは どちらでしょう? When reading from Vector or ListBuffer,which is faster?
  48. 48. Vector A ListBuffer B code val data = Vector( 1 to n: _* )
 
 ( 1 to data.size ) map { _ => data( Random.nextInt( n ) ) } val data = ListBuffer( 1 to n: _* )
 
 ( 1 to data.size ) map { _ => data( Random.nextInt( n ) ) }
  49. 49. Answer A. Vector Ans. A.Vector
  50. 50. benchmark Vector は ListBuffer より速いです。 Vector is faster than ListBuffer. n個の要素をreadしたときのThroughput 上記のグラフはサイズNのコレクションからランダムにN回要素を取得したときの スループットを表しています。 例えば、 type:ListBuffer, times:1k の場合、サイズ1,000のListからランダム に1,000回要素を取得することを意味します。
  51. 51. benchmark 他のオブジェクトのベンチマークは下記のとおりです。 Array, ArrayBuffer, Vector are fast. 速いのは Array, ArrayBuffer, Vector です。 n個の要素をreadしたときのThroughput
  52. 52. Why Array, ArrayBuffer, Vector are fast? Such as Vector uses Array - constant time - internal. • Array のランダム・リードは定数時間です。 • ArrayBuffer と Vector は内部的に Array を使っています。 protected var array: Array[AnyRef] = new Array[AnyRef](math.max(initialSize, 1)) : def apply(idx: Int) = {
 if (idx >= size0) throw new IndexOutOfBoundsException(idx.toString)
 array(idx).asInstanceOf[A]
 } 例) ArrayBuffer
  53. 53. 爆速 No.5 Array の仲間は ランダム・リードが速いです。 No5. It s cool using Array family when reading randomly.
  54. 54. No.6 フィボナッチ数列
  55. 55. Q6 上記を踏まえて、 Stream と Array どちらがより速く フィボナッチ数列をつくる ことができるでしょうか? Stream or Array, which is producing fibonacci faster?
  56. 56. Stream A Array B * 再帰的に計算します。 * 計算量O(n)のロジック です。 code def fibonacci( h: Int = 1, n: Int = 1 ): Stream[Int] =
 h #:: fibonacci( n, h + n ) val fibo = fibonacci().take( n ) def fibonacci( n: Int = 1 ): Array[Int] = if ( n == 0 ) {
 Array.empty[Int]
 } else {
 val b = new Array[Int](n)
 b(0) = 1
 for ( i <- 0 until n - 1 ) {
 val n1 = if ( i == 0 ) 0 else b( i - 1 )
 val n2 = b( i )
 b( i + 1 ) = n1 + n2
 }
 b
 } val fibo = fibonacci( n )
  57. 57. Answer A. Stream. Ans. A.Streamです。
  58. 58. benchmark Stream は Array よりも圧倒的に速いです。 Stream is overwhelmingly faster than Array. 長さnのフィボナッチ数列をつくったときのスループット フィボナッチ数列: ( 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, .. )
  59. 59. Why is Stream so much faster? Stream implements lazy list evaluated when needed. • Stream は遅延評価リストを実装しているので必要なときに だけ計算されます。 • 実際fibonacciメソッド(前ページ)を呼び出した時点ではフィ ボナッチ数列はまだ計算されていません。 • これが遅延評価の力です。 • しかし数列が具現化されるときにはそれなりの計算時間を 必要とします。 → 次ページをごらんください。
  60. 60. Why is Stream so much faster? if the sequence is materialized, it takes calculating time. Stream の toList を実行したときのベンチマークです。 Stream で toList を実行すると、Array が最も速いです。 これは Array で使っている計算ロジックが再帰による計算 よりも速いことを示します。 長さnのフィボナッチ数列をつくったときのスループット
  61. 61. 爆速 No.6 遅延評価は便利です。 しかし具現化するときにはそれなりの コストがかかるのをお忘れなく。 No.6 lazy evaluation is very useful.
  62. 62. No.7 正規表現
  63. 63. Showing the benchmark of w+w 正規表現はCPUリソースをたくさん消費します。 特に、“ww..w”のようなバック・トラッキングが発生 する表現では計算量O(n^2)となります。 Regular Expression consumes CPU resource a lot. 下のグラフは正規表現のパフォーマンスです。 各正規表現1,000回実行時のスループット
  64. 64. 最終問題. Q7 ある表現(w+/)の後方を探したいです。 “findAllIn & 肯定的後読み(?<=)” と “findPrefixOf & 量指定子(+)” どちらが速いでしょう? 次の文字列から探します→ abcdef..0123../abc.. (1024byte) ある文字列の後方を探したいです。どちらが速いでしょう?
  65. 65. 同じ A findPrefixOf B code val re = """(?<=w+)/.+""" r val ms = re findAllIn data
 if ( ms.isEmpty ) None
 else Some( ms.next ) val re = “""w+/""" r
 re findPrefixMatchOf data map( _.after.toString ) findAllIn と 肯定的後読み findPrefixOf と 量指定子
  66. 66. Answer B. findPrefixOf と量指定子(+) Ans. B.findPrefixOf & quantifier(+).
  67. 67. benchmark findPrefixOf and quantifier is faster than findAllIn. findPrefixOf と 量指定子(+) の組み合わせの方が findAllIn より速いです。 1,000回実行時のスループット 上記のグラフは、findAllInと正規表現“(?<=w+)/.+”、findPrefixOfと正規表現 “w/”をN回実行したときのスループットを表しています。
  68. 68. Why are findPrefixOf and quantifier faster? findAllIn returns all matches, findPrefixOf returns head. • 上記の表現はバック・トラッキングを引き起こします。 • 肯定的後読みは問題ではありません。 • 加えて、このケースでは findPrefixOf は findAllIn より速 く動作します。 • findAllIn は対象文字列中でマッチした部分を全て返します。 • findPrefixOf は対象文字列の先頭が正規表現にマッチした場 合のみマッチ部分を返します。 (?<=w+)/.+ 同じ文字列にマッチしてしまう...!
  69. 69. benchmark of various regular expressions Even if given same regex, findPrefixOf is fastest. 同じ正規表現を実行しても findPrefixOf が一番速いです。 findPrefixOf と 肯定的後読みの組み合わせは良いパフォーマンスが 得られます。 1,000回実行時のスループット
  70. 70. findPrefixOf usage in famous library ルーティング・ツリーはURIパスの先頭から構築するので、 findAllIn よりも findPrefixOf が適しています。 findPrefixOf is used in spray-routing. implicit def regex2PathMatcher(regex: Regex): PathMatcher1[String] = regex.groupCount match {
 case 0 ⇒ new PathMatcher1[String] {
 def apply(path: Path) = path match {
 case Path.Segment(segment, tail) ⇒ regex findPrefixOf segment match {
 case Some(m) ⇒ Matched(segment.substring(m.length) :: tail, m :: HNil)
 case None ⇒ Unmatched
 }
 case _ ⇒ Unmatched
 }
 } : https://github.com/spray/spray/blob/master/spray-routing/src/main/scala/spray/routing/PathMatcher.scala PathMatcher.scala line:211 下記は spray-routing のコードを一部抜粋したものです。
  71. 71. 爆速 No.7 findPrefixOf は結構便利です。 正規表現を使うときは、その計算量を 考えましょう。 No.7. When using Regex, be mindful of the computation.
  72. 72. No. 1 Memory Mapped File で高速ファイル操作が可能に! No. 2 for内包表記 と flatMap & map はバイト・コードのレベルで 同じです。 No. 3 コレクションを沢山更新するようなときは ArrayBuffer や ListBuffer などの Buffer系 を使うと効率が良いです。 No. 4 List は 先頭挿入で最も良いパフォーマンスを示すので、List を使うときは先頭挿入した後にソートして使うとよいです。 No. 5 Array の仲間はランダム・リードが速いので何度も読みだすと きは大活躍です。 No. 6 遅延評価は便利です。しかし具現化するときにはそれなりのコ ストがかかるのをお忘れなく。 No. 7 findPrefixOf は結構便利です。そして正規表現を使うとき は、その計算量を考えましょう。
  73. 73. https://github.com/x1-/scala-benchmark ベンチマーク計測に使ったソースコードは こちらに公開しています。
  74. 74. Thank you for listening!

×