Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

t4i_n5i
September 15, 2020

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

Vectorについて調べてみた

t4i_n5i

September 15, 2020
Tweet

More Decks by t4i_n5i

Other Decks in Programming

Transcript

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

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

    - Vectorの内部構造の特徴(Scala 2.13.2で見ていきます) 今回お話しないコト - ミュータブルなコレクションAPIについては話題に含めていません - ※Arrrayは登場しますが、Arrayはscala.collection.mutable配下ではない、ということで・・・ 3
  3. 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
  4. Vectorとは? What is Vector??? 引き続き公式ドキュメントを見てみると、以下のように記載されている 1. Vectorはランダムアクセス時の非効率性を解決する a. 不変添字付き列 (immutable.IndexedSeq)

    トレイトのデフォルトの実装 2. Vectorはどの要素の読み込みも「事実上」定数(実質定数)時間でおこなう a. 関数型更新も「事実上定数時間」で実行 3. Vectorは分岐度の高い木構造で表される 上記3点について確認していきます 7
  5. Vectorはランダムアクセス時の非効率性を解決する Vectorの性能を計測する(2/3) 以下の頻繁に使用する操作で計測する - 要素の更新/追加 - 先頭/末尾両要素への操作を計測する - Arrayの更新は破壊的更新をさせることで、最速タイムを目指す -

    頻繁に使用するコレクション操作として、合わせてmapも計測する - 要素のアクセス - 線形アクセス/ランダムアクセスを計測する - 線形アクセスは `foreach` を使用することで、各実装に最適化された 方法で実行する 14
  6. 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
  7. Vectorはランダムアクセス時の非効率性を解決する Vector vs List vs Array 要素を変更する操作の性能比較 16 Vector List

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

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

    - いつまで待っても処理が終わらない・・・ - Arrayの方がやや高速ではあるが、 Vectorも十分に安定した性能となっている
  10. 読み込みは「実質定数時間」 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
  11. 読み込みは「実質定数時間」 実際にVectorの構造を見て理解する - 追加 25 ①len1 < WIDTH(=32) を満たすうちは、元の Arr1の

     クローンに新規要素を追加して Vectorを再生成 ②要素が32以上になった時にVector2に切り替わる  ただ、定義を見てもよくわからない・・・
  12. 読み込みは「実質定数時間」 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に移す
  13. 読み込みは「実質定数時間」 Vector2のデータ管理方法を紐解く(3/3) 最終的には以下のようになります 28 Vector2 _prefix1 _suffix1 data2 … …

    ここがいっぱいになったら 丸ごとdata2配下に付け替える Vector1から 持ち越した要素 … … … _suffix1がいっぱいになったら、どんどんこちらに付け替えられる 各Arrayの全要素が埋まったらVector3へ
  14. 読み込みは「実質定数時間」 これが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
  15. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 1. _suffix1に要素を追加する 33 _suffix1 … _suffix1 … 追加要素

    Vector6 _suffix1 _suffix2 data6 _suffix5 _suffix4 _suffix3 … … … … … … ※data/suffixを抜粋 この配列を再生成 再利用可能
  16. 読み込みは「実質定数時間」 これがなぜ追加の安定につながるのか? 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は再利用可能