バブルソート--gccとVisual C++、Java、C#、さらにはAndroidでベンチマーク

更新:2014年10月23日 作成:2014年9月23日


古典的なアルゴリズム「バブルソート」を様々な環境でベンチマークしてみた。C,Java,C#等の言語による違いや、PCとAndroidの比較、ハイパースレッディングの効果など、いろいろ調べて遊んでみた。

バブルソートでシンプルなベンチマーク

たくさんの数値を、大きさに応じて並べ替える処理を「ソート」という。

ソートの方法には、古くからひじょうに多くのやり方(アルゴリズム)が提案されているが、古典的で代表的な方法に、「バブルソート」がある。

バブルソートは、きわめてシンプルな考え方なので、短いプログラムで表現できる。

このバブルソートを、パソコンやタブレットで実行し、処理にかかる時間を比べて、コンピュータの性能を比較してみた。

隣同士比べながらベストな値を探す

バブルソートの手順を紹介する。

バブルソートで、数値を左から小さい順に並べ替えていくことを考える。

バブルソートでは、最初から完全な並び替えを行うのではなく、まずは、全体から最も大きな値を探し、一番右に来るようにする。

そのために、以下の手順で、値を比べたり入れ替えたりする。

全体の最大値を探す
図:全体の最大値を探す

このように、「隣り合う値同士を比べて、左側のほうが大きければ、左右を入れ替える」という処理を、左側から順番に行うと、処理を行った対象の中で、最も大きな値を、一番右側に寄せることができる。

対象を1つずつ減らしながら繰り返す

これまでの処理で、5つの数値の中から、最大のものを取り出す処理が終わったので、次は、残りの4つの数値から最大のものを取り出す。

方法は簡単で、先ほどの方法を、左側の4つの数値だけを対象に行う。

4つの中から最大値を探す
図:4つの数値から最大値を探す

ここで得られた最大値は、最初に求めた最大値を超えることはないので、全体としては2番めに大きな値となる。

同じように、残り3つの値を並べ替えると、以下のようになる。

3つの中から最大値を探す
図:3つの数値から最大値を探す

最後に、残った2つを並べ替えると、大きな値ほど右側にある、昇順の並べ替えが完成する。

2つの中から最大値を探して完成
図:2つの数値から最大値を探して完成

このように、バブルソートでは、全体から最大値を取り出し、次に、残った中から最大値を取り出し・・・という処理を繰り返すことで、多くの数値を小さい順(昇順)に並べ替えることができるのだ。

いくつかのプログラミング言語でバブルソートを試す

計算を延々と繰り返すのが得意なコンピュータは、このようなソートの処理をガンガンこなしてくれる。

手持ちのパソコンを使って、いくつかのプログラミング言語で、バブルソートのプログラムを書いてみた。

パソコンのプログラムは、同じような処理内容でも、用いるプログラミング言語により、性能が変わることがあるので、いくつかのプログラミング言語でバブルソートを実装して、その処理速度を比べることにする。

手持ちのパソコンは、CPUはCore i3-350M 2.26GHz 2コア/4スレッドで、メモリは4GB(2GB×2)だ。もう4年前のモデルだが、とくに不調を訴えることもなく順調に稼働中だ。

プログラムが出来上がったら、あらかじめ降順で用意した20万個の整数配列を、バブルソートを使い昇順に並べ替える処理を実行し、所要時間を測る。その時間が短いほど、計算の効率がよい処理系ということになる。

C言語(Linuxのgcc)

手持ちのパソコンには、Linuxの最新版であるUbuntu 14.04 LTS (32bit版)をインストールしてある。

Linuxには、フリーのCコンパイラ等で有名なgccという処理系がある。

まずは、C言語でバブルソートを行うプログラムのソースコード(test104.c)を用意し、gcc(4.8.2)でコンパイル・実行することにした。

プログラムは、コンソールで動作するもので、以下の順に動作する。

ソースコードをgcc 4.8.2でコンパイルして実行した結果は、以下のとおりだ。

gcc(4.8.2)でコンパイルして実行
図:gcc(4.8.2)でコンパイルして実行

実行にかかった時間は、約45秒だった。コンパイルするとき、"gcc -O2 test104.c"と入力したので、最適化を行っている。

実行するときは、timeコマンドを先頭に指定して、処理の所要時間をパソコン自身で計測した。

ちなみに、最適化を行わずにコンパイルすると、実行にものすごく時間がかかる。

上記の20万個の数値を並べ替える処理では、249秒もかかってしまった。

Java言語(Linux)

Linuxには、Javaの環境も用意されている。

Oracle社のjdk1.8.0_05を使って、先ほどと同様、指定した個数の数値をバブルソートするソースコード(test104.java)を作ってみた。

20万個の数値をソートすると、処理時間は約54秒だった。

java(jdk 1.8.0_05)でコンパイルして実行
図:java(jdk 1.8.0_05)でコンパイルして実行

C言語と比べ、20%ほど余計に時間がかかっている。

Javaの処理系では、コンパイル後のプログラムを、仮想マシン上で実行しているが、仮想マシンを使うことによる処理の増加量としては、わずか2割というのは優秀かもしれない。さらにJavaでは、配列を使うときに、メモリを確保していない領域までアクセスしようとしていないかを監視し、違反した場合はその場でプログラムを止める機能も持っている。このような機能を持っている割には、速いと思う。

ちなみに、Javaには、フリーソフトウェアとしての実装(Open JDK)もある。Open JDKの1.7.0_65を使って、先ほどと同じソースコードをコンパイルして実行してみた結果が、こちらだ。

javaでコンパイルして実行(Open JDK 1.7.0_65)
図:Javaでコンパイルして実行(Open JDK 1.7.0_65)

処理にかかった時間は、約54秒だった。メーカー製の処理系とフリーの実装には、性能の差はほとんどないようだ。

C++言語(Visual Studio 2013 express Edition の Visual C++)

先ほどのパソコンには、Windows 7 SP1 (64bit) も入っている。

マイクロソフトの、無料の開発環境(Visual Studio 2013 express Edition)を使って、バブルソートを実装し、実行にかかる時間を比べてみた。

まずは、Visual C++言語だ。

C++といっても、用意したソースコード(vc001.cpp)は、C言語のソースコードからほとんど変更していない。

Windowsで、コマンドの実行にかかる時間を測るには、Windows PowerShellのMeasure-Commandコマンドを使うと便利だ。

Releaseモードでコンパイルしたプログラムを使い、20万個の数値をソートすると、実行にかかった時間は、約45秒だった。Linux上のgccとほとんど変わらない。

Visual Studio 2013 express EditionのVisual C++は、無料バージョンではあるが、最適化の機能が入っていて、フリーソフトウェアのgccと同程度の性能を持っているようだ。

ちなみに、Visual C++を使ってDebugモードでコンパイルすると、最適化が行われないので、バブルソートの実行に214秒もかかった。最適化を行わない場合は、最適化を行う場合と比べ、処理にかかる時間が数倍になる。この様子はgccと似ているが、gccよりも若干処理が速いようだ。

Visual C# 言語(Visual Studio 2013 express Edition)

マイクロソフトは、C#という独自の言語を開発し、Visual Studioで提供している。

実用的な処理系がVisual Studio一択しかなく、早く廃れるリスクがあるものの、JavaとC++を掛け合わせたような独特の世界観には、それなりの人気がある。

C#で、バブルソートのソースコード(Program.cs)を用意して、実行速度を調べてみた。

20万個の整数を並べ替えるのにかかった時間は、コンパイルの設定により、次のように変わった。

x64 CPU向けにリリースモードでビルドした場合に限り、Javaよりも速い結果になっている。C言語と比べた処理時間の増加は、約9%にとどまっている。

C#には、配列でメモリを確保していない領域にアクセスしようとすると、実行を止める機能など、Javaに似たメモリ管理機能が入っているが、それでもJavaよりも速いというのは興味深い。

一方で、高速実行の恩恵が受けられるのはx64向けにリリースモードでビルドした場合に限られ、x86やany CPUなど、他のCPUに対応したモードでは、ひどく遅くなってしまうことも分かった。

64ビットで動かさないと、本気を出さないのかねえ?

Visual Basic (Visual Studio 2013 express Edition)

Visual Basicでも、コンソールアプリを作ることができる。

先ほどと同じように、バブルソートで数値を並べ替えるソフトのソースコード(Module1.vb)を用意した。

こちらをビルドして、コマンドラインで20万個の数値を並べ替えてみたところ、実行にかかる時間は以下のようになった。

x64 CPU向けにビルドしたほうが、any CPU向けにビルドするよりも、高速に実行できる傾向は、C#言語と同様だった。

最速となるx64 CPU向けのリリースモードでも、C言語やJava言語、C#言語と比べ遅い結果となった。Basic言語は遅いという先入観は、それほど外れていないようだ。

ただし、BASIC言語とはいえ、他の言語で最適化を行わない場合と比べれば、速い結果となっている。BASIC言語といっても、純粋なインタプリタではなく、最終的に実行される段階では、動的にコンパイルされているのかもしれない。

ちなみに、最初にこのページを公開した段階では、並べ替えるデータをInteger型の配列ではなく、デフォルトのObject型の配列としてしまう不具合があった。そのときの実行時間は、約2167秒にも及んでいた。型名を明記しなかったせいで、コンピュータは約30倍も余計な計算をしてしまったようだ。

間違い:Dim a()

修正後:Dim a() As Integer

まとめ

いろいろな処理系で、バブルソートを実行してみた。処理にかかった時間は、次のとおりだ。

バブルソート20万個処理時間まとめ
図:バブルソート20万個処理時間まとめ

処理時間には、こんな傾向があった。

Androidのタブレットでバブルソートを試す

最近は、情報端末と言えばスマホやタブレットが主役で、パソコンは脇役に追いやられている。

そこで、タブレットの計算性能を調べるために、手持ちのAndroid タブレット(Nexus 7 2012年モデル)で、数値のバブルソートを行ってみた。

そのために、数値配列のソートを行うだけのアプリケーションを作成した。

GUIをフリーズさせないためにAsyncTaskを使う

重い計算を伴うAndroidアプリを作る上で重要なのは、ユーザインターフェースをフリーズさせないように、バブルソートを実装することだ。

ボタンクリックの際などに実行されるスレッドは、アプリ全体のユーザインターフェースを制御する「メインスレッド」だ。

メインスレッドでは、ユーザがGUIを操作した場合のリアクションを行っている。

従って、メインスレッドでバブルソートを実行すると、しばらくの間制御がOSに戻らなくなるので、ユーザの操作に端末が反応しなくなり、一時的にフリーズ状態となってしまう。

ワーカースレッドを用いない場合
図:ワーカースレッドを用いない場合

「今はソートで忙しいのだから、ユーザの処理など受け付けなくて当然」と考えるのは8ビットパソコンの時代。今は、マルチスレッドが当たり前の時代なので、忙しい処理を行っている最中も、操作はできるようにしておきたい。

そこで、ユーザインターフェースを処理するメインスレッドとは別に、バブルソートを実行するための専用のスレッド(ワーカースレッド)を用意して、メインスレッドと同時平行で実行する。そうすれば、バブルソートを行っている最中も、メインスレッドは画面の操作に対応できる。

ワーカースレッドを用いる場合
図:ワーカースレッドを用いる場合

重い処理を専用のスレッドで実行するために便利なのが、 AsyncTask だ。これを使えば、裏のスレッドで重い処理を実行できる。重い処理の実行が終わると、完了後の処理をメインスレッドで実施する機能もあり、便利だ。

ソースコードをはじめとした、Eclipseのプロジェクト(Test007.tar.bz2)(ライセンスはGNU CPL2)を用意したので、持ち帰って確認してみるのもよいだろう。なお、アプリケーションはAndroid 4.0以上で実行することを前提としている。

ちなみに、重い処理を行うアプリなので、スマホやタブレットが発火炎上して、家が全焼するなどのリスクがあるかもしれないが、作者は何の保証もしない。その旨了解いただける場合に限り、ダウンロードされたい。(普段使っているスマホやタブレットで実行するのはやめましょう。実験専用の端末が必要です。)

なお、上記のアプリは、計算中にさらに計算開始ボタンを押すと、処理時間を正しく計測できない。また、計算中に他のアプリに移ってしまうと、計算時間が正しく記録されない可能性がある。かなりテキトーに作っているので、テキトーに楽しんでほしい。

バブルソートで20万個の数値を並び替えた結果

Nexus 7 (2012)にて、200000個の数値をバブルソートしてみた。

端末はそれほど熱くならず、重い数値計算は余裕、とでも言っているようだ。

一方で、やたらと時間がかかった。

処理が終わるまでに、約11分(656秒)を要した。

Androidの実行結果(Nexus 7(2012))
図:Androidの実行結果(Nexus 7(2012))

PCでC言語を使って実行した場合と比べ、15倍近く時間がかかっている。

Android用のアプリはJava言語で開発したので、PCでJava言語を使った場合と比較すると、約12倍の時間を要している。

Nexus 7 (2012)の計算能力は、手元のパソコン(Core i3-350M)と比べ1桁低いと考えてよさそうだ。

ちなみに、パソコン上でAndroid端末を模擬するエミュレータ上でも、バブルソートのアプリを実行してみた。

エミュレータでの処理にかかった時間は、289秒で、5分を切っている。

驚くべきことに、PC上のエミュレータのほうが、実際の端末Nexus 7(2012)よりも、単純計算の速度は速いのだ。

Androidの実行結果(PCでのエミュレーション)
図:Androidの実行結果(PCでのエミュレーション)

エミュレータは、atomプロセッサを搭載したAndroid端末を想定して実行している。

PCはインテルのプロセッサ(Core i3-350M)を搭載するので、atomプロセッサのエミュレーションは高速にできるのかもしれない。

同時にたくさん実行してみる--マルチコアとハイパースレッディングを評価

最近のCPUは、複数の「コア」を内蔵している。

コアというのは、命令を実行するCPUのユニットのことで、下の図のように模式的に書くと、命令やデータを受け取って情報を処理し、結果を出力するコンピュータの心臓部だ。

CPUのコア
図:CPUのコア

最近のコアは、「ハイパースレッディング」の機能を持つので、1つのコアが同時に2つのプログラムを同時並行で実行できる。

ハイパースレッディングに対応したコア
図:ハイパースレッディングに対応したコア

最近のCPUは、内部に計算を行う機能を複数持っているので、スケジュールの都合がよけば、複数の命令を一度に処理できることがある。このため、複数のプログラムを同時並行で処理すると、一度に処理を行って効率を高められることがあるのだ。

ただし、同時に2つのプログラムを実行すると、互いのプログラムでスケジュールが合わないときは、譲り合って待たされることもあるので、1つ1つのプログラムの実行は遅くなってしまう。それでも、コア全体としては多くの情報を処理できる。

上の図では、プログラムの速度を、コアが出力した球の数で表している。

球の合計個数は、1つのプログラムを実行すると4、2つのプログラムを実行する(ハイパースレッディング)と5なので、ハイパースレッディングありのほうが、全体として処理できる量が多い。

一方で、1つ1つのプログラムに注目すると、青い球は1つのプログラムを実行すると4つ出ていたのに、2つを同時に実行すると2つしか出なくなっている。従って、個別のプログラム1つ1つだけを見ると、ハイパースレッディングを適用すると、かえって遅くなることもある。

手持ちのCore i3-350Mは、ハイパースレッディングに対応したコアを2つ内蔵するので、合わせて4つのプログラムを同時に実行できる。

同時並行に実行できるプログラムの総数を、スレッド数と呼ぶことがあるが、Core i3-350は4スレッドとなる。

2コア4スレッドの例
図:2コア4スレッドの例

2コア4スレッドのCore i3-350Mは、本当に、同時に4つのプログラムを高速に実行できるのだろうか?

試してみた。

C言語で同時に2つ、3つ、4つと実行する

LinuxなどのOSは、マルチタスクに対応しているので、複数のプログラムを同時に実行できる。

同時に実行する方法は簡単で、複数のコンソールのウインドウを開いて、それぞれでバブルソートのプログラムを起動すればよい。

コマンドが実行されるのは、キーボードのEnterキーを押したタイミングなので、複数のウィンドウで次々とエンターキーを押してプログラムを起動すると、タイムラグが発生するが、気にしないことにする。

パソコン(Ubuntu)を使って、C言語で書いたバブルソートのプログラムを、同時に複数実行してみた。

結果は、こんな感じだ。なお、処理時間は同時に実行したプログラムのそれぞれで、別々に計測している。

同時に複数実行した結果(PC, C言語)
図:同時に複数実行した結果(PC, C言語)

結果には、以下のポイントがある。

ハイパースレッディングがあることで、性能がどれくらい改善しているかを考えてみる。

バブルソートを合わせて4回実行するとき、ハイパースレッディングがある場合とない場合で、どう違うだろうか?

もしもハイパースレッディングがなければ、バブルソートは同時に2つしか実行できない。

同時に2つ実行すると、46秒かかる。合わせて4回実行するためには、もう一度同時に2つ実行する必要があるので、合わせて92秒かかるはずだ。

ハイパースレッディングを有効にすると、4つを全て同時に実行するので、全体として76秒あればよい。

従って、処理に必要な時間を25%削減できたことになる。

見方を変えれば、ハイパースレッディングにより、計算能力が33%増加したととらえることもできる。

2コア4スレッドは、2コア2スレッドよりも33%高性能だが、3コアや4コアには及ばないことが分かる。

ちなみに、インテルのCPUには、Pentiumシリーズは2コア2スレッド、Core i3シリーズは2コア4スレッド、Core i5シリーズの一部は4コア4スレッドなど、様々な種類がある。価格の違いと性能の違いをはかりにかけて、ニーズにあった製品を選ぶとよい。

もう一つ考慮すべき点がある。キャッシュメモリの量だ。

Core i3-350Mは、CPU内部に3MBのキャッシュメモリを搭載している。

バブルソートでは、20万個のデータを処理するが、整数のデータは1件4バイトなので、並べ替えるデータを全て合わせると、大きさは約800キロバイトだ。

バブルソートを行っているプロセスが消費するメモリの量は、プログラム1つあたり約1068キロバイトだった。

同時に3つ弱までならキャッシュメモリにおさまるが、4つになると、それなりにはみ出ることが分かる。

はみ出た分は、外部のRAMを頻繁にアクセスして処理する必要があるので、その分だけ処理が遅くなる。

ハイパースレッディングによる性能の向上率は、約33%と算出されたが、この数値は単純なCPU内部の計算能力だけでなく、同時に多くのデータを処理することで、キャッシュメモリがヒットしにくくなり、性能が頭打ちになったことも反映しているように思われる。

なお、バブルソートでは、処理が進むにつれて、アクセスするメモリの量が減っていくので、20万個の整数値を並べ替える処理を、同時に4つ実行した場合であっても、ある程度時間がたてば、全てのデータをキャッシュメモリに収められる状態になるようである。

Javaで同時に2つ、3つ、4つと実行する

次に、同じようなことをJava(Oracleの1.8.0_05)でも行ってみた。

Java言語のバブルソートのプログラムを、パソコン(Ubuntu)で、複数同時に実行してみた。

20万個の整数データを並べ替えるのにかかった時間は、次のとおりだ。なお、処理時間は同時に実行したプログラムのそれぞれで、別々に計測している。

同時に複数実行した結果(PC, Java)
図:同時に複数実行した結果(PC, Java)

C言語で実行した場合と比べ、同時に実行するプログラムの数が多くなると、計算にかかる時間が劇的に長くなっていることが分かる。

例えば、同時に2つ実行した場合の処理時間の長いほうを2倍しても、同時に4つ実行した場合の処理時間よりも短くなっている。

ハイパースレッディングの恩恵はなさそうに見える。

C言語と結果に違いが出たのは、Javaが消費するメモリが多いからだ。

Javaによるバブルソートは、プログラム1つあたり、約16メガバイトを消費する。処理の対象となるデータは800キロバイトしかないのに、このような大きなサイズとなるのには、理由がある。Javaのプログラムは、そのままではCPUが実行できない中間コードなので、CPU本来の命令に置き換えながら実行するための制御が必要となり、複雑だからだ。

こうなると、たとえ1つだけであってもプログラムがキャッシュメモリ(3MB)に収まらないので、次々とRAMにアクセスしながら実行する。

CPUにはコアが2つ入っているが、RAMは1系統しか存在しないので、同時に複数のプログラムを実行すると、RAMにアクセスが殺到し、処理全体に対してボトルネックとなってしまうようだ。

従って、同時に実行するプログラムの数が多くなるほど、プログラム1つあたりの実行にかかる時間(処理を終えるまでの所要時間)が、どんどん長くなってしまったのだ。

ちなみに、今回の比較は「バブルソート」という、ごく限られた処理を対象にしたので、処理の内容を変えれば違った結果になる可能性があることをお断りしておく。

クイックソートは速い

ちなみに、バブルソートを速く実行しようと努力することには、数値の並べ替えを速く実行することが目的であれば、ほとんど意味がない。

並べ替えのアルゴリズムそのものを、別の方法に置き換えたほうが、断然速くなるのだ。

「クイックソート」というアルゴリズムで、20万個の数値を並べ替える処理を、Android (Nexus 7(2012))で実行してみた。

かかった時間は、わずかに0.092秒だ。バブルソートよりも、7130倍も速い。

Nexus 7(2012)でクイックソートを実行した結果
図:Nexus 7(2012)でクイックソートを実行した結果

このように、アイデアそのものを根本的に変えることで、速く実行できるコンピュータや処理系を探すよりも、劇的な効果を得ることができる。

ちなみに、アイデアを変えれば劇的に改善することがあるという話は、データ処理の世界だけではない。狭い価値感の中で行われる受験競争や、出世競争などに明け暮れているよりも、目指す職業を変えて、新しい道を選んだほうが、劇的にうまくいく人も多い。物事を狭くとらえていると、努力を重ねても限界をむかえてしまうが、柔軟な思考ができる人は、限界を超えて先へ進めるのだ。

ちなみに、数値を並べ替えるだけでは、コンピュータに、役に立つ処理をさせたとはいえない。趣味としては面白いかもしれないけれども、その先につながるものがない。メールを書いたり、ゲームで遊んだり、動画を見たりして、うれしいことや楽しいことをたくさんしたほうが、生産的かもしれない。

やってみましょう


トップページなにげなく自由研究(もくじ) → バブルソート--gccとVisual C++、Java、C#、さらにはAndroidでベンチマーク
著者のメールアドレス・Twitterアカウントは、トップページからご覧ください。

製作・著作:杉原 俊雄(すぎはら としお)
(c)2014 Sugihara Toshio. All rights reserved.