2009-12-12[土] ソートはPGの基礎知識...

昔(もう20年ほど前か)、os-9/6809を使っていたときアセンブラで dirコマンドを自作してソート機能をつけたことがある。 このとき最初 単純にかける(O(n^^2^^)系の)ソートで 実装していたのだけど、 公開してみたら、遅いからクィックソートにしてみたら、 と言われたのだった。 実際、差し替えたところ、 十数秒かかっていたのが3秒ぐらいになって、非常に驚いた覚えがある。 アセンブラでカリカリに書いてるから速いつもりだったんだよ。 でもそれ以上にアルゴリズムの性能が段違いにモノをいう、 ってのを身をもって教えられたのでした。

まあ、ものわかりの悪い奴なんで、 実際に経験しないと気づけないという... 本に速いと書かれているのみてても、その性能差を汲み取れてないのよね.

その後、情報処理専門でない理系の大学のプログラムの授業で ソートを習ったりしたので、PGならソートくらいは知ってて当然の つもりだったのだけど...

数年前、作るものの仕様に整列をする部分があって、 やたら見積もり時間がおおかったり難色しめしたりする子がいて、 聞いてみるとソートを知らないという。 ゲーム専門学校とはいえプログラマ・コースだったから てっきり知っていると思ったら習ったことない、と。 初めてコレ聞いたときはかなりがっくりきた。

その後ゲ専出の何人か聞いた範囲ではほとんどが習ってなかった。 習っていた子もいたけれど、 バブルソートを自分で書いたことがある程度で、 でもって、ちゃっかりそのルーチンを仕事の実機で書いてくれて、 非常に残念な気分を味わえたのだった。(己と同じ過ちを...)

これが酷いのか普通なのかはよくわからないけれど...

ソートに関しては、 簡単だけれど性能の出ないルーチンを下手に作らせるよりも (PGは自分が書いたルーチンを使いたがるものだから)、 アルゴリズムの違いの性能差をとっとと見せ付けておくほうが、 そして、標準ライブラリにはそれらを使ったstd::sort(なりqsortなり)が あることを知ってもらっているほうが、いいだろうと思う.


ソート時間を測定してみる.

ということで、かるくソート時間を計測してみとく.

実行環境は Athron64 X2 5600+ 8GB(Vista64) と Crusoe TM5400(600MHz) 256M(Win2k) なマシン.

テストしてみたルーチンはコレ.

元ネタはwikipediaやその他検索ひっかかった所から.(面倒なんでクィックソートとか単純なまま)

バブル,選択,挿入, コム, クィック, クィック+挿入(閾値32), std::sort, qsort の時間をチェック. (std::sortを呼ぶためC++でコンパイルしてるけれど他はCでの記述状態)

適当な個数のソートを何回か繰り返した平均を出している。

テスト環境は普段使っているPCのまま。 なるべく他のソフト立ち上げていないけれど、エディタやファイラは 多少残してたり常駐ソフトも残ってるしで、厳密さはなし。 時間計測も単純にC関数のclock()に任せてるので精度とかは微妙かもだし。 測定しなおすと、細かい数字は結構かわるし、たまに当たり所が悪いと余計に時間 食っていることもありそうだし。

それでも、大雑把な傾向は出てると思う.
(追記:コンパイルはvc2008で32bit版でコンパイル)
以下、結果.

Crusoe TM5400(600MHz) 256M(Win2k)環境でのint値のソート時間.(単位:μ秒)

      個数10個20個30個40個50個100個256個1000個10000個
バブル ソート 1.3624.8263.85324.63238.205110.0731148601283800
選択 ソート 1.0923.5647.37716.14026.64090.04706970825200
挿入 ソート 0.6911.9643.8376.2929.41540.02403064373500
コム ソート 1.2425.8505.0077.4129.46525.07176211000
クィックソート 1.4225.6485.5407.73210.26520.0703008000
クィック+挿入 0.7211.9843.8739.3727.06015.0402407000
std::sort 1.0012.8443.7039.29213.17020.5613008000
qsort 2.9357.61219.76326.68035.85070.017080221000

Athron64X2 5200+(2.6GHz) 8GB(Vista64)環境でのint値のソート時間.(単位:μ秒)

      個数10個20個30個40個50個100個256個1000個10000個
バブル ソート 0.2370.7381.5472.7924.11518.01041566152200
選択 ソート 0.2490.7441.4202.2803.33511.56282278300
挿入 ソート 0.1210.3060.5330.9321.1553.52131029100
コム ソート 0.2320.5540.9601.4121.8104.514761100
クィックソート 0.2800.7201.1901.6882.5305.01468800
クィック+挿入 0.1230.3000.5330.8241.1453.0942500
std::sort 0.1580.3400.5730.9961.3403.51156700
qsort 0.5241.2822.1533.1924.2159.0281302100

上から3つが O(n^^2^^)、次の2つがO(nlogn)。

10,20個程度だと、大差ない雰囲気があるけれど、 1000個とか10000個のソートになってくると O(n^^2^^)ものとO(nlogn)モノとで1~3桁 時間が違ってきてる。 (当然それ以上だとさらに差は拡大するわけで)

数が少ないときは比較的似通っているといっても、 挿入ソートは目だって速く、 上記結果だと、TM5400では int 50個程度までなら、Athron64X2では int 100個程度 までなら、コムソートやクィックソートより速い結果になっている. (もちろんCPU等環境の違いで個数は大きく変わる)

で、クイックソートは、一定個数以下になったら挿入ソートに切り替えることが 可能で、それによって速度を上げることができる.
それが クィック+挿入 の行の結果.

std::sort()も実はだいたい同様でクィック+挿入(少なくともvcのはだったはず.)。 若干遅い結果なのは汎用性等の書き方で多少オーバーヘッドがあるのか. この差が問題になるかどうかは、用途しだいだけど、 普段気にするようなレベルではないだろう.

※追記>std::sort()の実装は(普通?は) 再帰が深くなるとヒープソートに切替えるらしい. (イントロソートらしい?)

ただ、Cライブラリの qsortもアルゴリズム自体は同様と思われるが、 こちらはCプログラムでの汎用性のため (とくにintなんていう単純なデータのソートとしては) オーバーヘッドが大きくなってしまっている模様。

quick+挿入に対して、1桁違うことはないだろうが、 下手すると数倍の時間がかかったりして、結構大きい。

C言語の場合は、性能のためにあえてqsortを使わず、 自前で(コムやクィック)ソート・ルーチン用意するのは、 ありの選択.

(が、性能と手間を思うと、C++環境にできるなら とっととC++に移行してしまうのが吉かも)

※クィックソートから挿入ソートへの切替個数は、 コンパイラ付属のものにあわせて 32個にしている。 アルゴリズムの本(20年位前にかかれたもの)をみると、 CPU性能やコンパイラ性能等でかわってくるが だいたい 10~20 位、って値が示されているのだが、 最近のCPUの性能がいいのかより大きい値のほうが速いようだった.

ついでに他のソート時間を測定してみる.

ついでに、wikipediaのコムソートの説明にあったcomb sort 11 と、 数日前に流行?っていたやねうらお氏挿入ソートも試してみた。 (ちょっとはしょりすぎて100~1000の精度がいまいち...)

Crusoe TM5400(600MHz) 256M(Win2k)環境でのint値のソート時間.(単位:μ秒)

個数 10個20個30個40個50個100個256個1000個10000個
insert sort 0.6911.9643.8376.2929.41540.02403064373500
insert sort(yane)0.6811.9623.8406.36814.22040.02006230425700
comb sort 1.2425.8505.0077.4129.46525.07176211000
comb sort11 1.2823.0840.81310.93613.82025.08058211000
quick+insert sort0.7211.9843.8739.3727.06015.0402407000
quick+yane-insert0.7112.0023.8735.3687.11015.0502406100

Athron64X2 5200+(2.6GHz) 8GB(Vista64)環境でのint値のソート時間.(単位:μ秒)

個数 10個20個30個40個50個100個256個1000個10000個
insert sort 0.1210.3060.5330.9321.1553.52131029100
insert sort(yane)0.1180.3160.5800.9241.3454.52740438800
comb sort 0.2320.5540.9601.4121.8104.514761100
comb sort11 0.2310.5641.0271.4801.9454.514741000
quick+insert sort0.1230.3000.5330.8241.1453.0942500
quick+yane-insert0.1230.3220.5870.8641.1903.0942600

Athron64X2 5200+(2.6GHz) 8GB(Vista64)環境でのdouble値のソート時間.(単位:μ秒)

個数 10個20個30個40個50個100個256個1000個10000個
insert sort 0.1850.4780.8631.3401.9106.53548047100
insert sort(yane)0.1960.5020.9031.4001.9906.53549848600
comb sort 0.3100.8121.3972.1482.8407.0221121700
comb sort11 0.3140.8601.4532.2083.0206.5221081600
quick+insert sort0.1850.5020.8631.3761.9304.51466900
quick+yane-insert0.1940.5100.9071.4241.9205.01468900

comb sort11については、個数が多いときには効果あるようだけど、逆に個数が 少ないときは増えた if 文のペナルティが出るのか、遅くなってしまっている。 その効果もささいな感じなので、無理に使う必要はなさそう.

やねうらお氏の挿入ソートだけど、説明読んだときは効きそうに思ったのだけど、 結果は、よくなったり悪くなったり、で、ちょっと残念な結果でした。 いや、なんか、自分がポカやってる可能性は多いにあるのですが。 (TM5400の50個の列の値は、当たり所悪く他の処理時間が混ざったパターンぽく)

やなう版は、最内側のループの外側とはいえ、外のループの内側に if文が増えているので、そのへんが影響して、 コンパイラの最適化やCPUのキャッシュ具合、分岐予測 等、 何かバランス的にくづれて、メモリー代入のペナルティと同等か それ以上のペナルティになってしまっているのかな?

追記:こっちを書くまで気づいてなかったけど、 比較回数が1回増えてるので、コピーしないですました時間より 比較時間が増えるのが無視できないということかも. (元記事の前提と違う状態で使ってしまったとも)


その他

しらべていないけれど、 (己がかいた)クィック+挿入は、一見性能よさそうにみえるけど、 個数がある数(以上?)だと、急激に時間がふえてしまったりしていた. ちょっと困った.

あとソースをコンパイルしたexeや使ったバッチ等

ダウンロード