C言語でバイナリファイルを出力する時、高速な処理を求められるならfflush関数を上手に使った方がよいです。ただし、どういうコードを組めば、どういう振る舞いをするのか分かりづらい、と感じていたため、サンプルコードでfflush関数の動作を確認していきます。
実行環境は下表の通りです。
開発環境 | Microsoft Visual Studio Community 2019 Version 16.11.7 |
OS | Windows 10 バージョン21H1 |
fflush関数とは
fflush関数 インターフェース
fflush関数は、ストリームに紐づけられたバッファからデータを明示的に吐き出す関数です。インターフェースは、以下の通りです。
int fflush ( FILE *stream ) ;
何のための関数?
通常、fwrite関数でファイルへの書き込みを行った場合、1バイトのデータ書き込みだけで、毎回I/Oアクセスが発生してしまうと、性能のボトルネックとなります。よって、fwrite関数はストリーム上のバッファにある程度データを書き込み、しきい値以上のデータがたまった時点で初めて、バッファのデータをファイルに書き込む仕様となっています。
fflushの働きを確認
通常のファイル書き込み
通常のファイル書き込みを行うプログラムが以下の通りです。
#include <stdio.h>
#include <string.h>
#include <time.h>
void main(void)
{
FILE* fp = NULL; // ファイルポインタ
size_t size = 0; // サイズ計算
size_t rt = 0; // fwrite関数地
errno_t err; // エラーコード
clock_t st, ed; // 処理時間測定
char buf[16]; // バッファ
int i; // ループカウンタ
memset(buf, 0, sizeof buf); // 初期化
size = 1; // 1バイト指定
st = clock();
err = fopen_s(&fp, "c:\\temp\\data.bin", "wb"); // ファイルオープン
if (err != 0)
{
printf("fopen_s error\n");
exit(1);
}
for (i = 0; i < 1024; i++)
{
rt = fwrite(buf, size, 1, fp); // ファイル書き込み
if (rt != 1)
{
printf("fwrite error\n");
fclose(fp);
exit(1);
}
}
fclose(fp); // ファイルクローズ
ed = clock();
printf("result : %f 秒\n", (double)(ed - st) / CLOCKS_PER_SEC);
return;
}
実行結果は以下となります。この例では、1バイトずつ1024回に分け、ファイルに書き込みますが、検証環境では、バッファサイズが512バイト(*1)ですので、I/Oアクセスは2回に抑えられているはずです。そのため、処理は高速です。

(*1) fwrite関数のバッファサイズは、stdio.h内の定義されています。 define BUFSIZ 512
fflush関数を実行する場合
次は、fflush関数の実行を追加してみます。(35行目)
#include <stdio.h>
#include <string.h>
#include <time.h>
void main(void)
{
FILE* fp = NULL; // ファイルポインタ
size_t size = 0; // サイズ計算
size_t rt = 0; // fwrite関数地
errno_t err; // エラーコード
clock_t st, ed; // 処理時間測定
char buf[16]; // バッファ
int i; // ループカウンタ
memset(buf, 0, sizeof buf); // 初期化
size = 1; // 1バイト指定
st = clock();
err = fopen_s(&fp, "c:\\temp\\data.bin", "wb"); // ファイルオープン
if (err != 0)
{
printf("fopen_s error\n");
exit(1);
}
for (i = 0; i < 1024; i++)
{
rt = fwrite(buf, size, 1, fp); // ファイル書き込み
if (rt != 1)
{
printf("fwrite error\n");
fclose(fp);
exit(1);
}
fflush(fp); // フラッシュ
}
fclose(fp); // ファイルクローズ
ed = clock();
printf("result : %f 秒\n", (double)(ed - st) / CLOCKS_PER_SEC);
return;
}
実行結果は以下の通りです。上記の例では、fwrite関数の後に、fflush関数を実行するようにしています。予想では、通常は、ある程度、バッファにデータが溜まってから、ではなく、fwite関数の実行の度、I/Oアクセスを行うようになるので、性能が劣化すると考えました。しかし、実際の劣化は、わずかです。fflushがイマイチ効いていないのでしょうか…。

ファイルオープンモードにモード修飾子”c”を指定
次は、fopen_s関数のファイルオープンモードに、モード修飾子”c”を指定してみます。(20行目) “c”は、バッファのデータが直接ファイルに書き込まれるようにする指定です。
#include <stdio.h>
#include <string.h>
#include <time.h>
void main(void)
{
FILE* fp = NULL; // ファイルポインタ
size_t size = 0; // サイズ計算
size_t rt = 0; // fwrite関数地
errno_t err; // エラーコード
clock_t st, ed; // 処理時間測定
char buf[16]; // バッファ
int i; // ループカウンタ
memset(buf, 0, sizeof buf); // 初期化
size = 1; // 1バイト指定
st = clock();
err = fopen_s(&fp, "c:\\temp\\data.bin", "wbc"); // ファイルオープン
if (err != 0)
{
printf("fopen_s error\n");
exit(1);
}
for (i = 0; i < 1024; i++)
{
rt = fwrite(buf, size, 1, fp); // ファイル書き込み
if (rt != 1)
{
printf("fwrite error\n");
fclose(fp);
exit(1);
}
fflush(fp); // フラッシュ
}
fclose(fp); // ファイルクローズ
ed = clock();
printf("result : %f 秒\n", (double)(ed - st) / CLOCKS_PER_SEC);
return;
}
実行結果は以下の通りです。性能が著しく劣化しています。1バイト書き込みの度、I/Oアクセスが発生しているものと考えられます。

まとめ
ここで説明に使用したサンプルは、普通に実行すれば高速な処理を、説明のために、あえて性能が劣化するようにした事例です。わかりづらいかもしれませんが、実際の製品に実装するコードの場合は、よく検証して、書き込みの仕様を検討すべきでしょう。私見ですが、大量に書き込みを行うのは、基本的には性能劣化にしかなりません。例えば、メモリ上で書き込みデータを編集し、ファイルへは1回の書き込みで済ませるのがよいと考えられます。
参考リンク

コメント