【C言語】fflush関数の動作について

C言語

C言語でバイナリファイルを出力する時、高速な処理を求められるならfflush関数を上手に使った方がよいです。ただし、どういうコードを組めば、どういう振る舞いをするのか分かりづらい、と感じていたため、サンプルコードでfflush関数の動作を確認していきます。

実行環境は下表の通りです。

開発環境Microsoft Visual Studio Community 2019 Version 16.11.7
OSWindows 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回の書き込みで済ませるのがよいと考えられます。

参考リンク

fflush
Learn more about: fflush

コメント

タイトルとURLをコピーしました