高速なソートアルゴリズムを
書こう!!
最速のソートアルゴリズムを目指して
JJUG CCC 2017 Fall #ccc_m8
松原 正和
https://github.com/m-matsubara/sort
松原正和 - 自己紹介
A5:SQL Mk-2の作者
・SQL&ER図ツール
双子のパパ
・もうすぐ5歳
https://github.com/m-matsubara/sort
https://a5m2.mmatsubara.com
本人
Java の標準ソートは Arrays.sort() メソッド
・プリミティブ(基本)型のソートは Dual-pivot quicksort
ピボット値が2つあるクイックソート (非安定)
・オブジェクト型のソートはTimSort
Tim Petersさんの書いた非再帰マージソート (安定)
とりあえずは、比較ソートで。
基数ソートとかバケットソートと
かは今回は考えないことにします。
https://github.com/m-matsubara/sort
安定(Stable)ソートとは…おさらい
安定(Stable)ソートとは、ソートキーが同じ要素が複数あった場合、
元配列の出現順序が保たれるソートのこと。
安定なソート 非安定なソート
同じ身長同じ身長 同じ身長同じ身長
ソート前
ソート後
https://github.com/m-matsubara/sort
速いソート・遅いソート
ソートアルゴリズムの速い・遅いを語るときにビッグ・オー表記を用
いる。
遅いソート … O(N2)
要素数が10倍になると実行時間は100倍くらいになる。
アルゴリズムの例:バブルソート、選択ソート、挿入ソート
速いソート … O(N log N)
log…ロガリズム(logarithm)…対数
要素数が10倍になっても実行時間は100倍までならない
(10件から100件で20倍、10万件から100万件で12倍くらい)
アルゴリズムの例:クイックソート、ヒープソート、マージソート
N はソートの
要素数
https://github.com/m-matsubara/sort
1.クイックソートを高速化するお話
https://github.com/m-matsubara/sort
クイックソートのおさらい(1/2)
• 身長順に並べる
①ピボット値の選択
②ピボット値以下を左に、ピボット値以上を右に集める
配列
ピボット!!
小さいサブリスト
配列
大きいサブリスト
https://github.com/m-matsubara/sort
クイックソートのおさらい(2/2)
③小さいサブリスト・大きいサブリストそれぞれで再度ピボット値
を決め、さらに小さいサブリスト・大きいサブリストを作っていく
…これを繰り返す。
配列
こっちも繰り返す!
https://github.com/m-matsubara/sort
Dual-pivot Quicksortをベースに
安定ソート化!!
mmsSort (matsubara masakazu stable Sort)
① ピボット値を2つにする
② マージソートのように外部メモリを使うことで安定ソート化
③ 場合によっては3-way partition Quicksortに切り替え
④ CPUキャッシュを有効活用
https://github.com/m-matsubara/sort
① ピボット値を2つに
再帰の深さが約2/3になる。CPUキャッシュも効果的に利用できる。
配列
※ピボット値の選び方
1) 配列中から8個候補を選ぶ
2) 候補をソート
3) 3番目と6番目の要素を
ピボット値として選ぶ
pivot1
比較回数が減るわ
けではない。
https://github.com/m-matsubara/sort
pivot2
配列の先頭からピボット1・ピボット2と比較してパーティション
操作する。
配列
② マージソートのように外部メモリを
使うことで安定ソート化(1/2)
ワーク
メモリ
pivot1 < 値 < pivot2 pivot2 ≦ 値
(作業領域に後ろから詰めていく)(作業領域に前から詰めていく)
値 ≦ pivot1
配列と同じサイズの
外部メモリが必要
https://github.com/m-matsubara/sort
ワークメモリから元の配列に書き戻す。
配列
ワーク
メモリ
② マージソートのように外部メモリを
使うことで安定ソート化(2/2)
コピー 反転コピー
pivot2 ≦ 値値 ≦ pivot1 pivot1 < 値 < pivot2
pivot1 < 値 < pivot2 pivot2 ≦ 値
https://github.com/m-matsubara/sort
③ 場合によっては
3-way partition Quicksortに切り替え
選んだPivot1とPivot2が等しいとき…
→ピボット値未満・ピボット値と等しい・ピボット値より大きい
の3つに分ける
→ピボット値と等しいグループは再帰不要
再帰でソート 再帰でソートソート
要らない
配列 pivot < 値値 < pivot 値 = pivot
https://github.com/m-matsubara/sort
④ CPUキャッシュを有効活用
→先頭から分割操作したとき、後ろ側のサブリストから再帰すると
ちょっと速い
①最初に
再帰ソート
②次に
再帰ソート
③最後に
再帰ソート
分割操作では配列の先頭から値を詰めていく。
再帰の処理は後ろのサブリストから処理する。
配列 pivot2 ≦ 値値 ≦ pivot1 pivot1 < 値 < pivot2
https://github.com/m-matsubara/sort
2.マージソートを高速化するお話
https://github.com/m-matsubara/sort
マージソートのおさらい(1/2)
• 身長順に並べる
①配列を半分ずつに分割し、各々ソート(マージソート)しておく。
②配列の前半をワークメモリに移動する。
配列
配列
ワーク
メモリ
何か騙され
た気持ちに
なる。
配列の半分のワークメモリが必要。
https://github.com/m-matsubara/sort
マージソートのおさらい(2/2)
③マージを行う。
配列
ワーク
メモリ
https://github.com/m-matsubara/sort
3-way Mergesortの少し変わった実装(1/2)
一般的なオンメモリマージソートは2分割する実装が多いけど、3
分割とかすれば速くならない?
…まじめに書くと意外と面倒、ヒープやトーナメントツリーを用い
て実装される。
トーナメントツリーの場合
5 3 6
3
どのサブリスト(の先頭)
から値を取り出すか管理
しなければならない。
https://github.com/m-matsubara/sort
3-way Mergesortの少し変わった実装(2/2)
MasSort (Masakazu Sort)
①1個の整数でデータの状態を管理
②大量のif文で状態遷移を管理
※3分割版と4分割版を作ったが3分割版の方が速かった。ここで
は3分割版を基本として説明をする。
実はC++だと
4分割版のほうが速い…。
https://github.com/m-matsubara/sort
①1個の整数でデータの状態を管理
int state で各サブリストの先頭値の大小順を表現
state = 123となる
2 3 6① ② ③
state = 312となる
8 9 6① ② ③
https://github.com/m-matsubara/sort
②大量のif文で状態遷移を管理
if (state < 0x200) { // state = 0x123 or 0x132
array[idx] = workArray[pos1++];
if (pos1 >= p1to)
break;
if (state == 0x123) {
if (comparator.compare(workArray[pos1], workArray[pos2]) <= 0)
; // モード変更なし
else if (comparator.compare(workArray[pos1], array[pos3]) <= 0)
state = 0x213;
else
state = 0x231;
} else { // state = 0x132
各stateの取りうる値(15種類)
ごとに処理をハードコード
https://github.com/m-matsubara/sort
3.省メモリなマージソート
https://github.com/m-matsubara/sort
省メモリマージソート(1/2)
• 一般的なマージソートは同じ大きさの2つのサブリストに分割す
るけど、あえてバランスを崩してみる。
配列
ワーク
メモリ
ワークメモリ小さい!!。
https://github.com/m-matsubara/sort
省メモリマージソート(2/2)
MatSort (Matsubara Sort)
• 要素数の1/4のワークメモリにした場合
※ワークメモリサイズは速度とメモリ領域のトレードオフで決定する。
※全体の計算量は O(N log N)のままとなる。
配列
① ¼の範囲ごとに
mmsSort または
MasSort等で
ソートしておく
② 後半2つマージ
③ もひとつマージ
④ 最後もマージ
https://github.com/m-matsubara/sort
4.ベンチマーク
https://github.com/m-matsubara/sort
ベンチマーク
• ソート対象のオブジェクトは以下のような感じ
class SortItem {
public int key;
public String keyStr;
public int orginalOrder;
public int filler1;
…
public int filler13;
}
• Java version 1.8.0_40 で実行
• Intel Core-i7 3770K で実行
• 10回繰り返した平均値で比較
整数のソートキー
文字列のソートキー
ソート前の配列上での位置
(安定ソートできているかチェック用)
13個のダミー項目
https://github.com/m-matsubara/sort
※比較回数はArrays.sortが一番少なくなる
※ソート済みや重複値が多い場合、Arrays.sortが一番速くなる
…こともある。
ベンチマーク
(乱数・整数キー・重複なし)
アルゴリズム 10,000,000
mmsSort (dual pivot stable Sort) 3.471
MatSort(1/5) 3.819
MasSort 3.849
Quick Sort (Median of 3) 4.006
Arrays.sort 4.580
実行時間
要素数1000万個で
Arrays.sort()より
25%ほど速い。
単位(秒)
https://github.com/m-matsubara/sort
ベンチマーク総括(1/2)
mmsSortの特徴
• 乱数配列でArrays.sort()より20~30%ほど速い
• 作業領域は対象配列と同じサイズ必要
MasSortの特徴
• 乱数配列でArrays.sort()より10%ほど速い
• 作業領域は対象配列の2/3ほど必要
MatSortの特徴
• MergeSortの作業領域サイズを対象配列サイズの数分の1から
100分の1ほどに減らせる。
• 苦手データが少なめ(ソート済みが遅い部類に入るくらい)
https://github.com/m-matsubara/sort
Dual-pivot Quicksort ベース
3-way Mergesort ベース
省メモリマージソート
ベンチマーク総括(2/2)
Arrays.sort()の特徴
• ほとんどの条件で比較回数は最小になる。
• ソート済みや重複値が多い場合、急激に速くなる。
• 重複の少ない乱数配列は実際のところそれほど速くない
(MergeSortより遅いくらい)
QuickSortの特徴
• メジャーなソートアルゴリズムでは最速とされる…がJavaだとそ
こまで速くないことも。
• 比較回数多め(乱数配列の場合、Arrays.sort()の1.5倍くらい)
• 苦手なデータがある(最悪、O(N2)のパフォーマンス。HeapSortを
併用して克服!!)
https://github.com/m-matsubara/sort
速いソートを書いて目指すところ
• Javaの将来のバージョンの標準ソートに取り込まれたい。
• Javaじゃなくても何かの言語で採用してくれないかなあ…。
• 皆さんにも速いソートにチャレンジしてみてほしい。
https://github.com/m-matsubara/sort
Copyright © 2017 松原正和
ご清聴ありがとうございました。
https://github.com/m-matsubara/sort

高速なソートアルゴリズムを書こう!!

Editor's Notes

  • #2 10s プログラミングを習い始めの頃、皆一回くらいはソートアルゴリズムを学んだことがあるはず…。
  • #3 30s
  • #4 40s プリミティブとは intとかfloatとか。 とりあえず、TimSortを超えるのが目標。
  • #5 40s 安定なソートはマージソートやバブルソート 安定でないソートはクイックソートやヒープソート
  • #6 30s クイックソートは最悪ケースで O(N2) になることがある。…ヒープソートを併用して回避したりする。 マージソートは速いソートの中で唯一の安定ソート。その代わり外部メモリが必要だったりする。Arrays.sortはマージソートの一種。
  • #7 10s
  • #8 30s
  • #9 30s
  • #10 50s CPUキャッシュってなんぞや…最近アクセスしたメモリ(つまりオブジェクト)を高速にアクセスできる「キャッシュメモリ」に覚えておく。
  • #11 40s 再帰の深さが2/3になるということは、メモリの転送回数も2/3になる。
  • #12 30s ソート対象と同じ大きさの外部メモリが必要。
  • #13 30s コピー処理にSIMDを使えるともっと高速化されるのだけど…。
  • #14 40s
  • #15 30s ここまでがクイックソート高速化のお話。
  • #16 10s
  • #17 30s
  • #18 30s
  • #19 30s K-way Mergesortは…誰でも一度は考える。
  • #20 30s
  • #21 30s
  • #22 30s state変数の取りうる値は15種類…全体で66個ものif文、4-way mergesortにすると213個ものif文 ここまでがマージソート高速化のお話
  • #23 10s
  • #24 30s ワークメモリが要らない、インプレースマージソートってのもあるけどあんまり速くない。 Inplace Mergesortの例 O(N log N)のもの WikiSort … MergeSortの倍近く…ただし、ソースに改善の余地ありと思う。  GrailSort … C++のみ … Javaに移植して試したいが…。
  • #25 40s ¼の範囲ごとのソートは安定ソートなら何でもいい。ソート範囲サイズと同じワークメモリが使える。 ②~④の実行コストは1/Kのワークメモリの場合、O(N・K)に比例する。つまり、Kが固定なら、O(N)ということ。 ここまでが省メモリマージソートのお話。
  • #26 10s
  • #27 20s 整数のキータイプと文字列のキータイプでベンチマークした。 CPUのアーキテクチャもそうだし、ダミー項目の増減でもパフォーマンスは変わる。 自分の環境はメモリが若干遅め(安物)ってところも影響はあるかも。
  • #28 20s
  • #29 50s
  • #30 30s
  • #31 20s 敷居はそんなに高くないよ。アイデア次第。 …突き詰めると離散数学とか出てくるけど…
  • #32 10s