今回はKMP法の回で少しだけ話題に出たアレについて話します。大きなファイルを小出しに処理する際に重要なテクニックです。
それでは、今回の要点です。
では、いってみましょう。
大きなファイルを取り扱うとします。メモリの大きな環境であればその内容を一気にメモリに読み込むことができるかもしれませんが、そういう場合は小出しに読み出していくのが普通でしょう。
メモリの少ないパソコンでは全く使えないプログラムになる可能性もあります。また、大きなメモリを確保できたとしても、そのせいで他のプログラムのメモリが足らなくなってしまうこともあります。関連して、OS自体の動作が不安定になることもあるかもしれません。このように、大きなファイルは小出しに扱うのが普通なのです。
例えば、大きなファイルをBM法(第39章参照)で検索していくことを考えましょう。もちろん大きなメモリを確保することはよくないので、小出しにバッファに読み出すことにします。普通は数千バイトくらいとるのですが、ここでは例のために8バイトとします。
| t | o | m | a | t | u | \r | \n |
|---|
BM法はほとんどの場合、検索していく際にバッファ内を逆方向にすすむことがあります。
| t | o | m | a | t | u | \r | \n | |
|---|---|---|---|---|---|---|---|---|
| t | a | m | a | |||||
| × | ← | ← | ||||||
このように、逆に戻った先もまだバッファ内であればいいのですが、かならずそうなるというわけではありません。小出しに確認していく時には前のバッファの内容を消してしまいます。消さなければメモリの節約になりませんね。ということは、その消した先に戻りたいことが出てくるはずです。
| × | t | o | m | a | t | a | \r | \n | ||
|---|---|---|---|---|---|---|---|---|---|---|
| m | a | t | o | |||||||
| ?? | ← | ← | ||||||||
このときはどうすればいいでしょうか? 簡単にはもう一度前の所を読み出せばいいですね。
| t | o | m | a | t | u | \r | \n | |
|---|---|---|---|---|---|---|---|---|
| m | a | |||||||
| × | ||||||||
で、照合に失敗したのでまた先に進みます。このとき、さっきまで読み出していた部分をもう一度読み出すことになります。しかも、この部分も前に読み出したことのある部分です。何か無駄な感じがしますね。
大きなファイルの検索というと結構時間のかかる作業です。その中でもファイルからの読み出しというのは遅く、この頻度を減らすことは検索速度の向上に繋がります。小出しに読み出しつつもファイルからの読み出し頻度を減らすにはどうすればよいのでしょうか?
バッファのサイズを大きくして頻度を減らすという方法を考える人がいるかもしれません。しかし、メモリの節約がしたいのでこの案はいまいち使えません。しかも、読み直しが起こったときに読み直すサイズが大きいので、読み直しが起こったときのペナルティが大きくなってしまいます。
ここでちょっと上の方に書いた一文をもう一度見てみましょう。
「小出しに確認していく時には前のバッファの内容を消してしまいます」
このように、そもそもファイルの読み直しが起きるのは前のバッファを消してしまうからです。しかし、消さなければバッファの節約はできません。残したいのに消さなければならないわけです。
しかし、1回前のものくらいは残しておいても問題はないのではないでしょうか? これならメモリも充分節約でき、同時にある程度前に戻ることもできますね。上の例を使って見ていきましょう。
| t | o | m | a | t | u | \r | \n | |
|---|---|---|---|---|---|---|---|---|
| m | a | |||||||
| × | ||||||||
| t | o | m | a | t | a | \r | \n | |
| t | o | |||||||
| ← | ← | |||||||
初めはバッファ1に読み出します。そしてバッファ1の先に進む必要が出てくると、バッファ1に上書きするのではなくバッファ2に読み出します。これで、上のように検索の時にバッファ2よりも前に戻りたいときは、バッファ1に戻ればいいようになります。
バッファ1よりまだ戻る必要がある場合は読み戻さなければいけませんが、バッファのサイズを検索文字列よりも大きく取るようにすればそのようなことは起こりません。これで読み直しがなくなるというわけです。
バッファ2の終わりまで来れば、次はバッファ1に上書きします。
| t | o | m | a | t | i | \r | \n | |
|---|---|---|---|---|---|---|---|---|
| m | a | t | o | |||||
| × | ||||||||
| t | o | m | a | t | a | \r | \n | |
あとはこれを続けていくだけですね。
このように、大きなファイルをメモリを節約しながら解析していくような場合などには、バッファを2つ用意すると便利です。このテクニックのことを、バッファを2つ使うのでダブルバッファリングと呼びます。
ダブルバッファリングは、BM法やコンパイルのように逆戻りの必要な作業にしか使わないことを注意してください。KMP法のように、先に進むだけならいくら上書きしても問題ないですね。
では、プログラムを見てみましょう。
| プログラム1 |
|---|
// BMMatch.h
#ifndef __BMMATCH_H__010417_2236_70554750__INCLUDED__
#define __BMMATCH_H__010417_2236_70554750__INCLUDED__
#include <stdio.h>
#include <limits.h>
// 検索文字列は255文字まで
const int BMM_PAT_MAX = 255;
// 再検索
const int BMM_NEXT = -1;
// 見つかりませんでした
const int BMM_NOTFOUND = -1;
// 表を再利用できるように、保存できるようにします
// skip がBM法用の表で、
// pat が前に検索した文字列で、
// nFound が前に検索した箇所で、
// nPatLen が pat の長さです
struct SBMMatch
{
int skip[UCHAR_MAX + 1];
char pat[BMM_PAT_MAX + 1];
int nFound;
int nPatLen;
};
// ファイル内をBM法で検索します
// ファイルはバイナリモードで開かれている必要があります
// nFirst は検索を始める場所で、
// pat は検索文字列で、bm は上の構造体です
// pat に NULL を指定すると、bm.pat を検索文字列として使います
// さらに nFound に BMM_NEXT を指定すると、bm.nFound + 1 から始めます
// 初めて検索する際に、構造体の初期化は必要ありませんが、
// 初期化していない構造体を使って再検索を行ったときの動作は
// 全く保証されないものとします
// 見つかった場合は true を、見つからなかった場合は false を返します
// 位置は bm.nFound に入ります
bool BMMatch(FILE* file, SBMMatch& bm,
int nFirst = BMM_NEXT, const char* pat = NULL);
#endif // #ifndef __BMMATCH_H__010417_2236_70554750__INCLUDED__ |
| プログラム2 |
// BMMatch.cpp
#include <stdio.h>
#include <string.h>
#include "BMMatch.h"
#define numof(array) (sizeof (array) / sizeof *(array))
static void BMMakeSkipTable(SBMMatch& bm);
bool BMMatch(FILE* file, SBMMatch& bm,
int nFirst, const char* pat)
{
if(pat == NULL)
{
if(bm.nPatLen == 0)
return false;
if(nFirst == BMM_NEXT)
nFirst = bm.nFound + 1;
}
else
{
// 最低限の初期化
bm.pat[0] = 0;
bm.nFound = BMM_NOTFOUND;
bm.nPatLen = 0;
// それ以外の初期化
int nPatLen = strlen(pat);
if(nPatLen <= 0 || BMM_PAT_MAX < nPatLen)
return false;
bm.nPatLen = nPatLen;
strcpy(bm.pat, pat);
BMMakeSkipTable(bm);
}
// バッファサイズ
const int BMM_BUFSIZE = (BMM_PAT_MAX + 1) * 4;
// ダブルバッファ
char dblbuf[2][BMM_BUFSIZE];
// どちらのバッファを使っているか
int fBuf = 0;
// バッファへのポインタ
char* src = dblbuf[0];
// 読み出しサイズ
int nRead;
// 読み戻しでバッファが変わったかどうか
bool bSwitched = false;
// 最初以外で読み出した回数
int nCount = 0;
int nPatLen = bm.nPatLen - 1;
char letLast = bm.pat[nPatLen];
char letSkip;
int i = nPatLen;
// ファイルの nFirst バイト目に移動します
fseek(file, nFirst, SEEK_SET);
nRead = fread(src, 1, BMM_BUFSIZE, file);
while(bSwitched || i < nRead)
{
letSkip = src[i];
if(letSkip == letLast)
{
int j = nPatLen;
int k = i;
do
{
// 見つかりました
if(j == 0)
{
bm.nFound = nFirst + nCount * BMM_BUFSIZE + k;
return true;
}
j--; k--;
// k が負になったらバッファを交換
if(k < 0)
{
src = dblbuf[fBuf ^= 1];
bSwitched = true;
k += BMM_BUFSIZE;
nCount--;
}
}
while(src[k] == bm.pat[j]);
}
// バッファが交換されているときは、元に戻す
if(bSwitched)
{
src = dblbuf[fBuf ^= 1];
bSwitched = false;
nCount++;
}
i += bm.skip[(unsigned char)letSkip];
// バッファを超えた時は、次のバッファに読み出す
if(i >= BMM_BUFSIZE)
{
if(nRead != BMM_BUFSIZE)
break;
src = dblbuf[fBuf ^= 1];
i -= BMM_BUFSIZE;
nCount++;
nRead = fread(src, 1, BMM_BUFSIZE, file);
}
}
bm.nFound = BMM_NOTFOUND;
return false;
}
static void BMMakeSkipTable(SBMMatch& bm)
{
int i;
for(i = 0; i < numof(bm.skip); i++)
bm.skip[i] = bm.nPatLen;
for(i = 0; bm.pat[i + 1] != 0; i++)
bm.skip[bm.pat[i]] = bm.nPatLen - 1 - i;
}
|
| プログラム3 |
// DblBuf1.cpp
#include <iostream.h>
#include <stdio.h>
#include <string.h>
#include "BMMatch.h"
static FILE* OpenFile();
static bool Match(FILE* file, SBMMatch& bm);
int main()
{
FILE* file = NULL;
file = OpenFile();
if(file == NULL)
goto ON_ERROR;
SBMMatch bm;
while(Match(file, bm));
ON_ERROR:
if(file != NULL)
fclose(file);
return 0;
}
static FILE* OpenFile()
{
char szPath[512];
cout << "検索したいファイルのパスを入力してください。" << endl
<< "> " << flush;
cin >> szPath;
// $ で終了します
if(strcmp(szPath, "$") == 0)
return NULL;
return fopen(szPath, "rb");
}
static bool Match(FILE* file, SBMMatch& bm)
{
char pat[1024];
bool bRet;
cout << "検索したい文字列を入力してください。" << endl
<< "> " << flush;
cin >> pat;
// $ で終了します
if(strcmp(pat, "$") == 0)
return false;
// / で次を検索します
else if(strcmp(pat, "/") == 0)
bRet = BMMatch(file, bm);
// 普通に検索します
else
bRet = BMMatch(file, bm, 0, pat);
if(bRet)
cout << bm.nFound << " バイト目に見つかりました!" << endl;
else
cout << "見つかりませんでした..." << endl;
return true;
} |
このようにダブルバッファリングを使えば、KMP法に頼らなくてもファイルの読み直しを抑えることができるのです。
では、最後に今回の要点です。
これでアルゴリズムに関することは一通り終わりました。
なお、このアルゴリズム編を作るにあたって、一部「C言語による最新アルゴリズム事典(奥村晴彦著,技術評論社,平成3年,ISBN4−87408−414−1)」を参考にしました(転載はしていません)。氏には心より感謝申し上げます。ちなみに、B木についてもこの本に載っています。
今回でアルゴリズム編は終わったわけですが、第3部はまだ続きます。それでは、次回まで。
Last update was done on 2001.4.18
この講座の著作権はロベールが保有しています