Vectorについて調べてみた / Beginnig-of-Vector

C4b9a181d7fb82642b4c8c1df6ae2b87?s=47 t4i_n5i
September 15, 2020

Vectorについて調べてみた / Beginnig-of-Vector

Vectorについて調べてみた

C4b9a181d7fb82642b4c8c1df6ae2b87?s=128

t4i_n5i

September 15, 2020
Tweet

Transcript

  1. Scala関西勉強会(2020/09/15) Chatwork株式会社 都志 典晃 Vectorについて調べてみた

  2. 自己紹介 - Tsushi Noriaki - Twitter:@louvre2489 - GitHub:louvre2489 - Chatwork株式会社

    - 入社半年なので色々と修行中 - 前職はSIerでExcel職人をしていました - 週末はフットサル 2
  3. 今回お話する内容 背景 - 『ScalaのVectorはスゴい(語彙力…)!』と聞いたので、何がスゴいのかを調べてみました 対象 - 『Vectorって何?』『Vectorの中身まで気にして使ってない』というScala初学者 今回お話するコト - Vectorの性能の特徴

    - Vectorの内部構造の特徴(Scala 2.13.2で見ていきます) 今回お話しないコト - ミュータブルなコレクションAPIについては話題に含めていません - ※Arrrayは登場しますが、Arrayはscala.collection.mutable配下ではない、ということで・・・ 3
  4. 00 <はじめに>

  5. Vectorとは? Javaのイメージだと・・・ JavaにおけるVectorは歴史の遺産 https://docs.oracle.com/javase/jp/8/docs/api/java/util/Vector.html  このクラスは、Java 2プラットフォームv1.2の時点でListインタフェースを実装するように改良された結果、 Java Collections Frameworkのメンバーとなりました。 (〜略〜)

    スレッドセーフな実装が必要ない場合は、 Vectorの代わりにArrayListを使用することをお薦めします。 一般的なWebアプリケーションのユースケースでは、Vectorではなく ArrayListを選択しておけば概ねOK 5
  6. Vectorとは? Scalaでは? 公式ドキュメント(作成時期は少し古め)を見てみる https://docs.scala-lang.org/ja/overviews/collections/concrete-imm utable-collection-classes.html#ベクトル ベクトル (Vector) は、ランダムアクセス時の非効率性を解決するために Scala 2.8

    から導入された新しいコレクション型だ。 ScalaにおけるVectorは「歴史の遺産」どころか、Scala2.8でわざわざ新しく 設けられたコレクション 6
  7. Vectorとは? What is Vector??? 引き続き公式ドキュメントを見てみると、以下のように記載されている 1. Vectorはランダムアクセス時の非効率性を解決する a. 不変添字付き列 (immutable.IndexedSeq)

    トレイトのデフォルトの実装 2. Vectorはどの要素の読み込みも「事実上」定数(実質定数)時間でおこなう a. 関数型更新も「事実上定数時間」で実行 3. Vectorは分岐度の高い木構造で表される 上記3点について確認していきます 7
  8. 目次 1. ランダムアクセス時の「非効率性を解決」 2. 読み込みは「実質定数時間」 3. 「木構造」で表される 4. まとめ 8

  9. 01 <ランダムアクセス時の「非効率性を解決」>

  10. Vectorはランダムアクセス時の非効率性を解決する Vectorの性能を確認したい 「効率」を確認するために、他のデータ構造との性能比較をおこなってみる - IndexedSeqであるVectorに対して、LinearSeqであるList - Vectorの内部で使用されているArray 10

  11. Vectorはランダムアクセス時の非効率性を解決する VectorはIndexedSeqの実装の代表 IndexedSeqとは? - ランダムアクセスを効率化する方針のデータ構造 - 当然ランダムアクセスが得意 11

  12. Vectorはランダムアクセス時の非効率性を解決する 一方、ListはLinearSeqの実装の代表 LinearSeqとは? - headとtailの組み合わせによる先頭要素への アクセスを効率化する方針のデータ構造 - それを繰り返すことで、先頭から順次 データを見ていくような処理が得意 12

  13. Vectorはランダムアクセス時の非効率性を解決する Vectorの性能を計測する(1/3) 計測環境 - OS:MacOS Catalina - CPU:Corei9(2.4GHz 8コア) -

    メモリー:64GB - Scalaバージョン:2.13.2 ←ここ重要(理由は後ほど) 13
  14. Vectorはランダムアクセス時の非効率性を解決する Vectorの性能を計測する(2/3) 以下の頻繁に使用する操作で計測する - 要素の更新/追加 - 先頭/末尾両要素への操作を計測する - Arrayの更新は破壊的更新をさせることで、最速タイムを目指す -

    頻繁に使用するコレクション操作として、合わせてmapも計測する - 要素のアクセス - 線形アクセス/ランダムアクセスを計測する - 線形アクセスは `foreach` を使用することで、各実装に最適化された 方法で実行する 14
  15. Vectorはランダムアクセス時の非効率性を解決する Vectorの性能を計測する(3/3) sbt-jmh(sbtからJMHを使用できるようにするプラグイン)を使用する 計測用コードはこちら 15 // plugin.sbt addSbtPlugin( "pl.project13.scala" %

    "sbt-jmh" % "0.4.0") @BenchmarkMode (Array(Mode.SingleShotTime )) @OutputTimeUnit (TimeUnit.MICROSECONDS ) // 計測はマイクロ秒単位で実施 class VectorPerformance { // @Benchmark アノテーションのついたメソッドを計測する @Benchmark def linearAccess_1000 (): Unit = { ... } } # 実行方法 # -i 反復回数 -wi ウォームアップ回数 -fフォーク数 -tスレッド数 sbt clean "jmh:run -i 5 -wi 20 -f1 -t1" https://github.com/louvre2489/vector-performance
  16. Vectorはランダムアクセス時の非効率性を解決する Vector vs List vs Array 要素を変更する操作の性能比較 16 Vector List

    Array - Vectorはすべてにおいて安定 している - Listは先頭要素の追加 /更新は常に一定だが、末尾操作は件数が多くなると遅くなる - Arrayは更新操作は常に一定だが、その他の操作は件数が多くなると遅くなる - 更新操作は破壊的変更なので高速! - 追加操作、mapは件数によっては遅い(非破壊的操作で実装されている ArrayOpsのメソッドを利用)
  17. Vectorはランダムアクセス時の非効率性を解決する Vector vs List vs Array 線形アクセスの性能比較 17 - 線形アクセスはListが速いと思いきや、件数が多くなってくると遅くなる

    - 何度か計測してがだいたい同じかんじ - Arrayの線形アクセスは緩やかにだが指数的に遅くなっている - Vectorは一定件数からガタッと遅くなるが、指数的に遅くなるわけではない - 遅くなった以降も 遅くなったなりに安定した性能
  18. Vectorはランダムアクセス時の非効率性を解決する Vector vs List vs Array ランダムアクセスの性能比較 18 - Listのランダムアクセスは途中で計測打ち切り

    - いつまで待っても処理が終わらない・・・ - Arrayの方がやや高速ではあるが、 Vectorも十分に安定した性能となっている
  19. Vectorはランダムアクセス時の非効率性を解決する Vectorの安定感 スゴい 公式の性能特性をまとめたページからも全体的な安定感が伺える https://docs.scala-lang.org/ja/overviews/collections/performance-c haracteristics.html 19 Vectorは基本操作がすべて 「実質定数」 全体的に非効率性が解決されている (どんな操作も大体

    効率が良い)ことがわかる Listは効率の良い/悪いがハッキリしてる
  20. 02 <読み込みは「実質定数時間」>

  21. 読み込みは「実質定数時間」 「実質定数」とは? 先ほどの性能特性のページに以下のように記載されている > 演算は実質定数時間で完了するが、ベクトルの最大長やハッシュキーの分布 など何らかの前提に依存する。 格納要素数による影響は受けるが、要素数に対して線形的/指数的に処理時間 が延びるのではなく、要素数に応じて一定の時間で処理が完了することを指す ※ここから先で「効率の良さ」の理由を探っていきますが、  すべての操作を確認すると膨大になるので

    「apply/更新/先頭追加」に着目して見ていきます 21
  22. 読み込みは「実質定数時間」 Vectorのデータ管理方法を確認する まずはscaladocを見てみる(データ構造に関する説明を抜粋) > Vectors are implemented by radix-balanced finger

    trees of width 32. > There is a separate subclass for each level (0 to 6, with 0 being the > empty vector and 6 a tree with a maximum width of 64 at the top level). Vectorは,幅32の基数平衡フィンガーツリー(訳あやしい)によって実装されま す。各レベルに個別のサブクラスがあります(0 から 6 まであり、0 は空のVector で、6 は最上位レベルの最大幅 64 の木です)。 大事そうなところに下線を引いてみましたが、何のこっちゃわからん・・・ 22
  23. 読み込みは「実質定数時間」 実際にVectorの構造を見て理解する scaladocにあったようにVector0〜Vector6というサブクラスを発見! 一番シンプルそうなVector1を見てみる - apply 23 ③Array[AnyRef]の要素番号を指定して取得している  (実質Arrayのランダムアクセスと同じ処理 )

    ②Vectorは内部的ではArrayでデータを管理している ①内部的には_data1とprefix1は同じモノ  ここにVectorの各要素の値が格納される
  24. 読み込みは「実質定数時間」 実際にVectorの構造を見て理解する - 更新 24 ①prefix1のクローンを作成して対象要素を更新 ②新しいVectorを生成

  25. 読み込みは「実質定数時間」 実際にVectorの構造を見て理解する - 追加 25 ①len1 < WIDTH(=32) を満たすうちは、元の Arr1の

     クローンに新規要素を追加して Vectorを再生成 ②要素が32以上になった時にVector2に切り替わる  ただ、定義を見てもよくわからない・・・
  26. 読み込みは「実質定数時間」 Vector2のデータ管理方法を紐解く(1/3) Vector2に切り替わる際に、どのようにVector2を生成しているのか? (要素データの格納先となるプロパティのみに着目) 26 追加する要素を Arr1でラップ 空のArr2 Vector1に格納していたArr1 (1〜31の要素)

    この3つに着目します N次元Arrayに別名を付与
  27. 読み込みは「実質定数時間」 Vector2のデータ管理方法を紐解く(2/3) 要素追加時、Vector2の内部データは以下のように変化する ※(N-1)要素目から変化があるところは黄色、変化のない部分はグレー 27 プロパティ 32要素目の追加 33要素目の追加 … 63要素目の追加

    64要素目の追加 65要素目の追加 _prefix1: Arr1 1〜31要素の値 1〜31要素の値 1〜31要素の値 1〜31要素の値 1〜31要素の値 data2: Arr2 空のArr2 空のArr2 空のArr2 32〜63要素目の値 32〜63要素目の値 _suffix1: Arr1 32要素目の値 32〜33要素目の値 32〜63要素目の値 64要素目の値 64〜65要素目の値 _suffix1がいっぱいになったらdata2に移す
  28. 読み込みは「実質定数時間」 Vector2のデータ管理方法を紐解く(3/3) 最終的には以下のようになります 28 Vector2 _prefix1 _suffix1 data2 … …

    ここがいっぱいになったら 丸ごとdata2配下に付け替える Vector1から 持ち越した要素 … … … _suffix1がいっぱいになったら、どんどんこちらに付け替えられる 各Arrayの全要素が埋まったらVector3へ
  29. 読み込みは「実質定数時間」 これがVector6になると・・ 最終的には以下のようになります 29 Vector6 _prefix1:Arr1 _suffix1:Arr1 data6:Arr6 _prefix2:Arr2 _prefix3:Arr3

    _prefix4:Arr4 _prefix5:Arr5 _suffix5:Arr5 _suffix4:Arr4 _suffix3:Arr3 _suffix2:Arr2 ←より先頭要素のデータが格納 より末尾要素のデータが格納 → Arr1〜Arr6はN次の多次元配列 Arr1〜Arr5は幅32、Arr6のみ幅64
  30. 読み込みは「実質定数時間」 これがなぜapplyの安定につながるのか? Vectorの要素は最大要素数であっても 6次元のArrayで管理されている つまり、要素が多くなっても最大で6回の 多次元配列呼び出しで要素を取得できる これは1回(Arr1時)〜6回(Arr6時)で 要素にアクセスできることを意味する =「実質定数」でアクセスが可能といえる 30

    Vector6 _suffix1 _suffix2 data6 _suffix5 _suffix4 _suffix3 … … … … … … 1回 2回 3回 4回 5回 6回 ※data/suffixを抜粋
  31. 読み込みは「実質定数時間」 これがなぜ更新の安定につながるのか? 要素の更新は1要素ずつしか実施しないので、 変更に関係のないブランチ/リーフは再利用可能 (イミュータブルなコレクションだからこそ) 変更する箇所が特定ブランチ配下のみになる ため、1回(Arr1時)〜6回(Arr6時)で 要素が更新できることを意味する =「実質定数」で更新が可能といえる 31

    Vector6 _suffix1 _suffix2 data6 _suffix5 _suffix4 _suffix3 … … … … … … ※data/suffixを抜粋 更新要素 この配列を再生成 再利用可能
  32. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 追加には2つのパターンがある 1. _suffix1に要素を追加する 2. 要素追加により_suffixNがいっぱいになったので _suffixNのデータを_suffixN+1に移す 32

  33. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 1. _suffix1に要素を追加する 33 _suffix1 … _suffix1 … 追加要素

    Vector6 _suffix1 _suffix2 data6 _suffix5 _suffix4 _suffix3 … … … … … … ※data/suffixを抜粋 この配列を再生成 再利用可能
  34. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 2. 要素追加により_suffixNがいっぱいになったので _suffixNのデータを_suffixN+1に移す 34 Vector6 _suffix1 _suffix2 data6

    _suffix5 _suffix4 _suffix3 … … … … … … ※data/suffixを抜粋 この配列を再生成 _suffix1 _suffix2 data6 _suffix5 _suffix4 _suffix3 _suffix1がいっぱい →_suffix2に付け替え _suffix2がいっぱい →_suffix3に付け替え _suffix3がいっぱい →_suffix4に付け替え _suffix4がいっぱい →_suffix5に付け替え _suffix5がいっぱい →data6に付け替え 最悪の場合はこれだけの要素付け替えが発生する なお、付け替え後の _suffix1〜5には空のArr1〜6を格納 図では省略しているが、 _prefixは再利用可能
  35. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 最小の場合は_suffix1の変更のみが発生する 最悪の場合は_suffix1〜5、data6の変更が発生する  →実装的には、6回のArrayのクローンと要素書き換えで処理が完了する つまり、上記処理の範囲内で要素が追加できることを意味する =「実質定数」で追加が可能といえる 35

  36. 03 <「木構造」で表される>

  37. 「木構造」で表される 多次元のArray = 木構造 Vectorのデータ管理の方法は、「radix-balanced finger trees」 という木構造が元となっている(22ページに抜粋したscaladocより) https://ja.wikipedia.org/wiki/2-3_フィンガーツリー 37

    Arr1 Arr2 Arr3 Arr4 Vector5 木構造自体の解説は こちらで確認してください
  38. 「木構造」で表される ここまでの話はScala2.13.1では通用しない Scala2.13.2でVectorの大規模変更が取り込まれている  https://github.com/scala/scala/pull/8534 このP-RによってVectorは「radix-balanced finger trees」で実装されるよう になった 38

  39. 「木構造」で表される ここまでの話はScala2.13.1では通用しない Scala2.13.1以前のVectorは「bit-mapped vector trie」というデータ構造で 構築されていたが、このP-Rでデータ構造を「radix-balanced finger trees」 にすることで大幅に性能向上している 39

    例) 要素追加時のsize:1Kあたりの処理時間が 1 / 20 以下になっている 以下で詳細を確認できる http://szeiger.de/tmp/vector2/vector-results.html
  40. 04 <まとめ>

  41. まとめ なぜVectorが存在するのか? イミュータブルなデータをいずれの操作も安定して高速に扱うため (そして目立ったデメリットがない) Arrayの特徴 - メリット:高速なapply/更新 - デメリット:破壊的変更による操作が可能 Listの特徴

    - メリット:イミュータブルであることが担保されている、      高速なhead/tail、先頭要素の操作が高速 - デメリット:先頭要素の操作以外は遅い 41
  42. まとめ Vectorスゴい! 1. ランダムアクセスも線形アクセスも安定して効率が良い 2. なぜかというと、各種データ操作が実質定数時間になるよう実装されてい るため 3. そのために、内部データが木構造で管理され、最小手数で処理が完了する ように設計されている

    Vectorは平均点の高い優等生 積極的に使っていこう 42
  43. We are Hiring!!! 43 for Scalaエンジニア for スクラムマスター https://hrmos.co/pages/chatwork/jobs/1020001 https://hrmos.co/pages/chatwork/jobs/1020007

  44. 働くをもっと楽しく、創造的に