プログラムの改良 他

※本ページは,いくつかの発展課題に関する参考資料となっている.

ソートのパフォーマンスチューニング

以下の例では,struct profileは,168バイトの大きさだと分かります:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

#define MAX_STR_LEN  69

struct date {
  int y;
  int m;
  int d;
};

struct profile {
  int         id;
  char        name[MAX_STR_LEN+1];
  struct date birthday;
  char        address[MAX_STR_LEN+1];
  char        *comment;
};

int main()
{
  printf("sizeof(struct profile) = %ld\n", sizeof(struct profile));
}

実行結果(注:計算機環境によって異なる結果が得られる場合がある):

sizeof(struct profile) = 168

swap()関数内では,3回の代入が起こっていますから,1回のswap()で504バイトの転送です.頻繁に2要素の入れ替え(swap)が起こるソートでは,実行時間に影響が出るかもしれません.

これを回避するにはどうしたらいいでしょうか.答えは,やはりポインタにあります.構造体の実体を移動させるのではなく,それを指すポインタを並べることでソートを実現する方法があります.ポインタどうしの入れ替えは,int型どうしの入れ替えとほぼ同じなので,非常に高速です.

QUIZ(参考):構造体のサイズ,および,ポインタのサイズは,自分で調べましたね?(ヒント:資料を鵜呑みにしてはいけませんよ.演習1のレポートで,「プログラムは,・・・で動作を確認しているが,・・・」という記述をしていますよね?)

以下の例では,配列要素1つ1つに対応するポインタの配列を用意しています.(参考:教科書K&R「5.6節 ポインタ配列:ポインタへのポインタ」)

ポインタの配列は,これまでの実装でも見た記憶がありますね.覚えてますか?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* これには構造体の実体が入っているのでサイズ大 */
struct profile profile_data_store[MAX_PROFILES];

/* これは ポインタの配列なのでサイズ小 */
struct profile *profile_data_store_ptr[MAX_PROFILES];

void make_profile_shadow(struct profile data_store[],
                         struct profile *shadow[],
                         int size)
{
  int i;
  for (i = 0; i < size; i++)
    shadow[i] = &data_store[i];
}

int main()
{
  /* 初期化 */
  make_profile_shadow(profile_data_store,
                      profile_data_store_ptr,
                      MAX_PROFILES);

  /* 以降 profile_data_store_ptr を使った処理に変更 */
}

こうしておいて,以降では, ポインタの配列profile_data_store_ptrをソートし,ポインタ経由で,実体の配列要素の操作(表示や検索)をするといった方法が考えられます.両者の動作や構造を比較するのは,いいレポートのネタになるでしょう.

バイナリ入出力

※教科書K&Rでは,「バイナリファイル (Binary file)」が「2進ファイル」という表現になっています.

前項で構造体のサイズを考えましたが,ファイルサイズの観点から,さらに考えられないでしょうか.

CSVデータは,1行あたり,1つのデータが含まれているはずです.では,1つのCSVデータを格納したファイルのファイルサイズは,構造体のサイズと同じ・・・でしょうか?

ファイル入出力の際に,バイナリ形式での入出力を検討してみることで,これらの問いに対する考えが思い浮かぶことでしょう.

ファイルサイズの調べ方は,緑教科書の『4.1.2 ディレクトリ操作のコマンド』を読めば,書かれています.(注:緑教科書とは,工学基礎実験実習の「大テーマ1」の教科書です.)

バイナリ入出力する際に,最低限必要となることは以下の2つです.

  • fopen()で開く際,"r""w"ではなく,"rb""wb"を指定する.

    • 教科書K&R 7.5節「ファイルオープン」 p.196冒頭

    • 教科書K&R 付録B1「入力と出力」 pp. 302-303

  • fread()fwrite()を利用して,指定アドレス(=ポインタ)を起点とした指定バイト数の読み書きをおこなう.

    • 教科書K&R 付録B1.5「直接入出力関数」 pp. 310-311

各関数の使い方は,教科書やmanコマンドを参照してください.

バイナリファイルを閲覧する場合は,hexdumpコマンドを使うとよいでしょう.

二分探索

探索にも目を向けてみましょう.

ソートされたデータ構造に対しては,より高速な探索方法が知られています.例えば,二分探索 (Binary search) は,他の講義(浅野ら著, アルゴリズム論, 2.5節 2分探索法)で履修しているのではないでしょうか.

C言語による実装としては:

  • 教科書K&R pp.70-71 に記載されたプログラム

  • 教科書K&R pp.162-163 に記載されたプログラム

が参考になるでしょう.

ライブラリ関数によるクイックソートqsort()関数と同様,bsearch()関数(教科書K&R p.319)も存在します.これらのライブラリ関数を活用すれば,自作の二分探索との比較により,動作テストができそうですね.

なお,qsort()bsearch()には,関数のポインタ(教科書K&R 5.11節「関数へのポインタ」)を理解する必要があります.

注意点として,二分探索を用いると,名簿管理プログラムの基本仕様を満たした%Fの実装が困難となります.従って,%Fの拡張ではなく,新コマンドとして,適切な仕様を与えたうえで作成した方がよいでしょう.

最後に,表現を変えて,もう一度繰り返します.

「適切にソートされている」ことが前提です.例えば,「id でソートされたデータに対して,name に対する二分探索をする」事はできません.