10週間ウェブ開発講座

裏メニュー第七週

簡易ベンチマーク

PHPのmicrotime関数を使用して、プログラムの実行時間に関する簡易的なベンチマークを取ることができます*1
microtime関数は次のような関数です。

microtime: 現在の Unix タイムスタンプをマイクロ秒まで返す
(PHP: microtime - Manual)

ベンチマークを取りたい箇所をmicrotime関数で囲み、差分を取ることで「どのくらい実行に時間がかかっているか」がわかります。
次のサンプルを読めばよく理解できるでしょう。

<?php

// 全く意味のない計算をする関数
function useless_calculations($times) {
    for ($i = 0; $i < $times; $i++) {
        rand() * rand();
    }
}

$start = microtime(true); // 現在の時間を取得
useless_calculations(1000);
echo '1000回の無意味な計算には' . (microtime(true) - $start) . "秒かかりました。\n";

$start = microtime(true); // 現在の時間を取得
useless_calculations(1000000);
echo '1000000回の無意味な計算には' . (microtime(true) - $start) . "秒かかりました。\n";

?>

これを手元の環境で実行すると、次のような結果になりました。

$ php sample.php
1000回の無意味な計算には0.00190210342407秒かかりました。
1000000回の無意味な計算には1.99042391777秒かかりました。

アルゴリズム

アルゴリズムとは何でしょうか?
「デジタル大辞泉」では次のように記述されています。

ある特定の問題を解いたり、課題を解決したりするための計算手順や処理手順のこと。
これを図式化したものがフローチャートであり、
コンピューターで処理するための具体的な手順を記述したものがプログラムである。
イランの数学者・天文学者、アル=フワーリズミーにちなむ。
(アルゴリズム とは - コトバンク)

また、Wikipediaでは次のように記述されています。

アルゴリズムとは、数学、コンピューティング、言語学、あるいは関連する分野において、
問題を解くための効率的手順を定式化した形で表現したものを意味する。
算法(さんぽう)と訳されることもある。

コンピュータにアルゴリズムを指示するための(電子)文書をプログラムという。
人間より速く大量に正しい結果を導くことができるのがコンピュータの強みであるが、
そのためには正しいアルゴリズムに基づくプログラムが必要である。

アルゴリズムとは「問題を解くための効率的手順を定式化した形で表現したもの」であるそうです。
また「コンピュータにアルゴリズムを指示するための(電子)文書をプログラムという」そうです。
何も難しいことはないですね。

先ほど引用した内容から考えると、「良い」アルゴリズムとは次のようなものでしょう。

  • 速い(実行効率)
  • 大量のデータを扱える(メモリ効率)
  • 誤差が小さい

また「コンピュータにアルゴリズムを指示するための(電子)文書をプログラムという」のであれば、
読み易さも「良さ」も評価軸に加えるべきでしょう。

  • 読み易い
    • ロジックが複雑でない
    • プログラム本体のサイズが小さい

以降では、良いアルゴリズムについて「ソート」を例にして考えていきます。

ソート

「ソート」とは「並び換え」のことです。
ここでは特に、整数を昇順にソートするプログラムについて考えていきます。

PHP標準関数sort

PHP標準関数のsortにはクイックソートというアルゴリズムが使用されています。
クイックソートは多くの場合、非常に高速に動作するアルゴリズムです*2

サンプルとして小さいプログラムを示します。

<?php

$array = array(12, 22, 3, 44, 13, 40, 58, 8);
sort($array, SORT_NUMERIC);
print_r($array);

// 実行結果
// Array
// (
//     [0] => 3
//     [1] => 8
//     [2] => 12
//     [3] => 13
//     [4] => 22
//     [5] => 40
//     [6] => 44
//     [7] => 58
// )

?>

シンプルなソート

sort関数と同等の機能を実現したいとしましょう。
こういう実現方法はどうでしょうか?

  1. 配列を始めから終りまで眺めて、「最も小さいもの」を「最も左のもの」と交換する
  2. 配列を二番目から終りまで眺めて、「最も小さいもの」を「左から二番目のもの」と交換する
  3. 配列を三番目から終りまで眺めて、「最も小さいもの」を「左から三番目のもの」と交換する
  4. ...
  5. 配列を終りの手前から終りまで眺めて、「最も小さいもの」を「終りもの」と交換する

すごくシンプルですね。
これをプログラムとして実現するとこうなります。

<?php

// シンプルなソート(名前は知らない)
function simple_sort($array) {
  $size = count($array);
  for ($i = 0; $i < ($size - 1); $i++) {
    $suffix = $i;
    $smallest = $array[$i];

    // 配列(の残り)から最も小さい値を探す
    for ($j = ($i + 1); $j < $size; $j++) {
      if ($smallest > $array[$j]) {
        $smallest = $array[$j];
        $suffix = $j;
      }
    }

    // 配列(の残り)の最も小さい値を交換する
    $tmp = $array[$i];
    $array[$i] = $smallest;
    $array[$suffix] = $tmp;
  }

  return $array;
}

$array = array(12, 22, 3, 44, 13, 40, 58, 8);
$array = simple_sort($array);
print_r($array);

// 実行結果
// Array
// (
//     [0] => 3
//     [1] => 8
//     [2] => 12
//     [3] => 13
//     [4] => 22
//     [5] => 40
//     [6] => 44
//     [7] => 58
// )

?>

バブルソート

「バブルソート」は単純なソートのアルゴリズムとしてよく知られています。
これは「隣り合う要素の大小を比較しながら整列させる」方法です。

Wikipediaには次のように記述されています。

1番目と2番目を比較し、順番が逆であれば入れ換える。
次に2番目と3番目を比較して入れ換える。
これを最後まで行うと、最後の数だけが最小または最大の数として確定するので、
確定していない部分について1つずつ減らしながら繰り返す。
(バブルソート - Wikipedia)

これをプログラムとして実現すると次のようになります。

<?php

// 典型的なバブルソート
function bubble_sort($array) {
  $size = count($array);
  for ($i = 0; $i < ($size - 1); $i++) {
    for ($j = 0; $j < ($size - $i - 1); $j++) {
      if ($array[$j] > $array[$j + 1]) {

        // 値を交換
        $tmp = $array[$j];
        $array[$j] = $array[$j + 1];
        $array[$j + 1] = $tmp;
      }
    }
  }

  return $array;
}

$array = array(12, 22, 3, 44, 13, 40, 58, 8);
$array = bubble_sort($array);
print_r($array);

// 実行結果
// Array
// (
//     [0] => 3
//     [1] => 8
//     [2] => 12
//     [3] => 13
//     [4] => 22
//     [5] => 40
//     [6] => 44
//     [7] => 58
// )

?>

マージソート

「マージソート」は分割統治法を用いたソートのアルゴリズムの一つとしてよく知られています。
このように処理が進行します。

  1. データ列を二分割する (通常、二等分する)
  2. 各々をソートする
  3. 二つのソートされたデータ列をマージする

どこまで分割するかは特に規定されませんが、多くの場合「データ数が2以下になるまで」分割します。

これをプログラムとして実現すると次のようになります。

<?php

// 典型的なマージソート
function merge_sort(&$array, $left, $right) {
  // 配列の要素が一つなら戻る
  if ($left >= $right) {
    return;
  }

  // 配列を真中で分割して左側と右側で再帰的に呼出す
  $median = (int)(($left + $right) / 2);
  merge_sort($array, $left, $median);
  merge_sort($array, $median + 1, $right);

  // $tはテンポラリ
  $t = array();

  // $array[$left] から $array[$median]をテンポラリへ
  for ($i = $left; $i <= $median; $i++) {
    $t[$i] = $array[$i];
  }
  // $array[$median + 1] から $array[$right]をテンポラリへ
  for ($i = $median + 1, $j = $right; $i <= $right; $i++, $j--) {
    $t[$i] = $array[$j];
  }

  $i = $left;
  $j = $right;

  // 小さい方から配列に戻す
  for ($k = $left; $k <= $right; $k++) {
    if ($t[$i] <= $t[$j]) {
      $array[$k] = $t[$i++];
    } else {
      $array[$k] = $t[$j--];
    }
  }
}


$array = array(12, 22, 3, 44, 13, 40, 58, 8);
merge_sort($array, 0, count($array) -1);
print_r($array);

// 実行結果
// Array
// (
//     [0] => 3
//     [1] => 8
//     [2] => 12
//     [3] => 13
//     [4] => 22
//     [5] => 40
//     [6] => 44
//     [7] => 58
// )

?>

それぞれのソートのベンチマーク

<?php

// 引数で指定した数の要素を持つ配列
function init_random_array($size = 2000) {
    $ret = array();
    for ($i = 0; $i < $size; $i++) {
        $ret[] = rand();
    }

    return $ret;
}

// シンプルなソート(名前は知らない)
function simple_sort($array) {
    $size = count($array);
    for ($i = 0; $i < ($size - 1); $i++) {
        $suffix = $i;
        $smallest = $array[$i];

        // 配列(の残り)から最も小さい値を探す
        for ($j = ($i + 1); $j < $size; $j++) {
            if ($smallest > $array[$j]) {
                $smallest = $array[$j];
                $suffix = $j;
            }
        }

        // 配列(の残り)の最も小さい値を交換する
        $tmp = $array[$i];
        $array[$i] = $smallest;
        $array[$suffix] = $tmp;
    }

    return $array;
}

// 典型的なバブルソート
function bubble_sort($array) {
    $size = count($array);
    for ($i = 0; $i < ($size - 1); $i++) {
        for ($j = 0; $j < ($size - $i - 1); $j++) {
            if ($array[$j] > $array[$j + 1]) {
                // 値を交換
                $tmp = $array[$j];
                $array[$j] = $array[$j + 1];
                $array[$j + 1] = $tmp;
            }
        }
    }

    return $array;
}

// 典型的なマージソート
function merge_sort(&$array, $left, $right) {
    // 配列の要素が一つなら戻る
    if ($left >= $right) {
        return;
    }

    // 配列を真中で分割して左側と右側で再帰的に呼出す
    $median = (int)(($left + $right) / 2);
    merge_sort($array, $left, $median);
    merge_sort($array, $median + 1, $right);

    // $tはテンポラリ
    $t = array();

    // $array[$left] から $array[$median]をテンポラリへ
    for ($i = $left; $i <= $median; $i++) {
        $t[$i] = $array[$i];
    }
    // $array[$median + 1] から $array[$right]をテンポラリへ
    for ($i = $median + 1, $j = $right; $i <= $right; $i++, $j--) {
        $t[$i] = $array[$j];
    }

    $i = $left;
    $j = $right;

    // 小さい方から配列に戻す
    for ($k = $left; $k <= $right; $k++) {
        if ($t[$i] <= $t[$j]) {
            $array[$k] = $t[$i++];
        } else {
            $array[$k] = $t[$j--];
        }
    }
}

$numbers = init_random_array();

// 組込み関数でのソート
$start = microtime(true);
// print_r(sort($numbers, SORT_NUMERIC));
sort($numbers, SORT_NUMERIC);
printf("組込み関数 sort では %s 秒かかりました。\n", microtime(true) - $start);

// シンプルなソート
$start = microtime(true);
// print_r(simple_sort($numbers));
simple_sort($numbers);
printf("シンプルなソートでは %s 秒かかりました。\n", microtime(true) - $start);

// バブルソート
$start = microtime(true);
// print_r(bubble_sort($numbers));
bubble_sort($numbers);
printf("バブルソートでは %s 秒かかりました。\n", microtime(true) - $start);

// マージソート
$start = microtime(true);
// merge_sort($numbers, 0, count($numbers) - 1);
merge_sort($numbers, 0, count($numbers) - 1);
printf("マージソートでは %s 秒かかりました。\n", microtime(true) - $start);

?>

これを手元の環境で実行すると、次のような結果になりました。

$ php sample.php
組込み関数 sort では 0.00717711448669 秒かかりました。
シンプルなソートでは 2.22329187393 秒かかりました。
バブルソートでは 3.04475808144 秒かかりました。
マージソートでは 0.114268064499 秒かかりました。

組込み関数が圧倒的な速度であることと、自前の関数でもアルゴリズム次第で大きな差が出ることを確認してください。

追記

優れたアルゴリズムを知ること、または考案することこそプログラミングの醍醐味です。
ソートはほんの一例に過ぎません。

宿題

  • (難しい)クイックソートを実装してください。
  • 「巡回セールスマン問題」について調べてください。
    • なぜ「巡回セールスマン問題」が難しいのか説明してください。
    • どのようなアルゴリズムでもよいので「巡回セールスマン問題」を解くプログラムを記述してください。
    • (オプション)前記の「巡回セールスマン問題」を解くプログラムを最適解により近づけるために改良してください。

参考資料


*1 PHPでベンチマークを取るための手段は幾つも存在します。microtime関数を使用するのは最も簡易な方法の一つです。
*2 宿題を参照されたい。

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2010-02-04 (木) 18:13:55 (213d)