はじめに
PHPでファイルやネットワークストリームからデータを読み取る際、毎回OSのシステムコールを呼び出すのは非効率です。そこで活躍するのが 読み取りバッファです。
stream_set_read_buffer を使うと、ストリームの読み取りバッファサイズをアプリケーションレベルで制御できます。適切なバッファサイズを設定することで、システムコールの回数を減らし、I/Oパフォーマンスを向上させることができます。
この記事では、バッファの仕組みから実践的なチューニング方法まで、クラスを用いた具体例とともに丁寧に解説します。
stream_set_read_buffer とは
| 項目 | 内容 |
|---|---|
| 関数名 | stream_set_read_buffer |
| PHPバージョン | PHP 5.3.3以降 |
| カテゴリ | ストリーム関数 |
| 返り値 | int(成功時 0、失敗時 0 以外) |
構文
stream_set_read_buffer(resource $stream, int $size): int
パラメータ
| パラメータ | 型 | 説明 |
|---|---|---|
$stream | resource | 対象のストリームリソース |
$size | int | バッファサイズ(バイト数)。0 でバッファリング無効(アンバッファード) |
返り値
| 値 | 意味 |
|---|---|
0 | 成功 |
0 以外 | 失敗(リクエストを処理できなかった) |
注意: 成功時が
trueではなく0であることに注意してください。if (stream_set_read_buffer(...))のように書くと 成功を失敗と判定してしまいます。
読み取りバッファの仕組み
【バッファなし(size=0)】
アプリ PHPランタイム OS / ディスク
fread(100) → システムコール(100) → [ディスク読み取り]
fread(100) → システムコール(100) → [ディスク読み取り]
fread(100) → システムコール(100) → [ディスク読み取り]
→ システムコール 3回
【バッファあり(size=8192)】
アプリ PHPランタイム OS / ディスク
fread(100) → バッファに8192bytes読み込み → [ディスク読み取り]
└ バッファから100bytes返す
fread(100) → バッファから100bytes返す(ディスクアクセスなし)
fread(100) → バッファから100bytes返す(ディスクアクセスなし)
→ システムコール 1回(残りはバッファから供給)
バッファを活用することで、ディスクやネットワークへのアクセス回数を大幅に削減できます。
基本的な使い方
<?php
$stream = fopen('large_file.txt', 'r');
// バッファサイズを64KBに設定
$result = stream_set_read_buffer($stream, 65536);
if ($result === 0) {
echo "バッファサイズを 65536 バイトに設定しました" . PHP_EOL;
} else {
echo "設定に失敗しました" . PHP_EOL;
}
// バッファリングを無効にする(アンバッファード)
stream_set_read_buffer($stream, 0);
fclose($stream);
実践例(クラスを使った実装)
例1:バッファサイズ別の読み取りパフォーマンス計測
さまざまなバッファサイズで大きなファイルを読み取り、処理時間を比較します。
<?php
class ReadBufferBenchmark
{
private string $filePath;
private int $fileSize;
public function __construct(int $fileSizeBytes = 1_048_576) // デフォルト1MB
{
$this->filePath = sys_get_temp_dir() . '/php_buffer_bench_' . getmypid() . '.tmp';
$this->fileSize = $fileSizeBytes;
$this->createTestFile();
}
private function createTestFile(): void
{
file_put_contents($this->filePath, str_repeat('A', $this->fileSize));
}
/**
* 指定バッファサイズで全データを読み取り、経過時間を返す
*/
public function measure(int $bufferSize, int $readSize = 4096): array
{
$stream = fopen($this->filePath, 'r');
$result = stream_set_read_buffer($stream, $bufferSize);
$totalBytes = 0;
$readCalls = 0;
$startTime = microtime(true);
while (!feof($stream)) {
$chunk = fread($stream, $readSize);
if ($chunk !== false) {
$totalBytes += strlen($chunk);
$readCalls++;
}
}
$elapsed = microtime(true) - $startTime;
fclose($stream);
return [
'buffer_size' => $bufferSize,
'read_size' => $readSize,
'total_bytes' => $totalBytes,
'read_calls' => $readCalls,
'elapsed_ms' => round($elapsed * 1000, 3),
'set_result' => $result, // 0=成功
];
}
public function cleanup(): void
{
if (file_exists($this->filePath)) {
unlink($this->filePath);
}
}
}
// 計測実行
$bench = new ReadBufferBenchmark(1_048_576); // 1MB
$bufferSizes = [0, 512, 4096, 8192, 65536, 262144];
echo str_pad("バッファサイズ", 18)
. str_pad("設定結果", 10)
. str_pad("fread呼出回数", 16)
. "処理時間" . PHP_EOL;
echo str_repeat('-', 60) . PHP_EOL;
foreach ($bufferSizes as $size) {
$r = $bench->measure($size);
$label = $size === 0 ? "アンバッファード" : "{$size} bytes";
$status = $r['set_result'] === 0 ? "OK" : "NG";
echo str_pad($label, 18)
. str_pad($status, 10)
. str_pad("{$r['read_calls']} 回", 16)
. "{$r['elapsed_ms']} ms" . PHP_EOL;
}
$bench->cleanup();
出力例:
バッファサイズ 設定結果 fread呼出回数 処理時間
------------------------------------------------------------
アンバッファード OK 256 回 4.821 ms
512 bytes OK 256 回 3.102 ms
4096 bytes OK 256 回 1.234 ms
8192 bytes OK 256 回 0.987 ms
65536 bytes OK 256 回 0.812 ms
262144 bytes OK 256 回 0.798 ms
例2:アンバッファードモードで1行ずつリアルタイム処理する
バッファリングを無効にすることで、ログファイルのようなリアルタイム書き込みに即座に追従できます。
<?php
class UnbufferedLogTailer
{
private $stream;
private int $maxLines;
public function __construct(string $filePath, int $maxLines = 10)
{
if (!file_exists($filePath)) {
throw new RuntimeException("ファイルが存在しません: {$filePath}");
}
$this->stream = fopen($filePath, 'r');
$this->maxLines = $maxLines;
// バッファリングを無効化(アンバッファード)
// → ファイルへの書き込みが即座に読み取りに反映される
stream_set_read_buffer($this->stream, 0);
// ファイル末尾にシーク
fseek($this->stream, 0, SEEK_END);
}
/**
* 新しい行を読み取って返す(ノンブロッキング)
*/
public function tail(float $timeoutSeconds = 2.0): array
{
$lines = [];
$deadline = microtime(true) + $timeoutSeconds;
$buffer = '';
stream_set_blocking($this->stream, false);
while (count($lines) < $this->maxLines && microtime(true) < $deadline) {
$chunk = fread($this->stream, 256);
if ($chunk !== false && $chunk !== '') {
$buffer .= $chunk;
while (($pos = strpos($buffer, "\n")) !== false) {
$lines[] = substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos + 1);
}
} else {
usleep(1000);
}
}
return $lines;
}
public function close(): void
{
if (is_resource($this->stream)) {
fclose($this->stream);
}
}
}
// 使用例(テスト用一時ファイルで動作確認)
$tmpFile = tempnam(sys_get_temp_dir(), 'log_');
// ログを書き込む
$fp = fopen($tmpFile, 'w');
for ($i = 1; $i <= 5; $i++) {
fwrite($fp, date('H:i:s') . " [INFO] ログメッセージ #{$i}\n");
}
fclose($fp);
$tailer = new UnbufferedLogTailer($tmpFile, maxLines: 10);
// ファイル先頭から読む
rewind($tailer->tail); // → tail() で取得
$fp = fopen($tmpFile, 'r');
stream_set_read_buffer($fp, 0);
while (!feof($fp)) {
$line = fgets($fp);
if ($line !== false) {
echo trim($line) . PHP_EOL;
}
}
fclose($fp);
unlink($tmpFile);
出力例:
12:00:01 [INFO] ログメッセージ #1
12:00:01 [INFO] ログメッセージ #2
12:00:01 [INFO] ログメッセージ #3
12:00:01 [INFO] ログメッセージ #4
12:00:01 [INFO] ログメッセージ #5
例3:大容量ファイルを最適バッファで効率的に読み取るクラス
ファイルサイズに応じてバッファサイズを自動選択する実用的なクラスです。
<?php
class OptimalBufferedFileReader
{
private const BUFFER_MAP = [
1 * 1024 * 1024 => 8192, // 1MB未満 → 8KB
10 * 1024 * 1024 => 65536, // 10MB未満 → 64KB
100 * 1024 * 1024 => 262144, // 100MB未満 → 256KB
];
private const DEFAULT_BUFFER = 524288; // 100MB以上 → 512KB
private string $filePath;
private int $bufferSize;
private int $readChunkSize;
public function __construct(string $filePath, ?int $bufferSize = null, int $readChunkSize = 8192)
{
if (!is_readable($filePath)) {
throw new RuntimeException("読み取り不可: {$filePath}");
}
$this->filePath = $filePath;
$this->readChunkSize = $readChunkSize;
$this->bufferSize = $bufferSize ?? $this->selectBufferSize(filesize($filePath));
}
private function selectBufferSize(int $fileSize): int
{
foreach (self::BUFFER_MAP as $threshold => $buf) {
if ($fileSize < $threshold) {
return $buf;
}
}
return self::DEFAULT_BUFFER;
}
/**
* コールバックを渡して行単位で処理する
*/
public function eachLine(callable $callback): int
{
$stream = fopen($this->filePath, 'r');
stream_set_read_buffer($stream, $this->bufferSize);
$lineCount = 0;
while (($line = fgets($stream)) !== false) {
$callback(rtrim($line, "\r\n"), ++$lineCount);
}
fclose($stream);
return $lineCount;
}
/**
* 全データをまとめて読み取る
*/
public function readAll(): string
{
$stream = fopen($this->filePath, 'r');
stream_set_read_buffer($stream, $this->bufferSize);
$content = stream_get_contents($stream);
fclose($stream);
return $content;
}
public function getBufferSize(): int
{
return $this->bufferSize;
}
}
// 使用例
$tmpFile = tempnam(sys_get_temp_dir(), 'reader_');
file_put_contents($tmpFile, implode("\n", array_map(
fn($i) => "Line {$i}: " . str_repeat('x', 80),
range(1, 1000)
)));
$reader = new OptimalBufferedFileReader($tmpFile);
echo "選択されたバッファサイズ: " . $reader->getBufferSize() . " バイト" . PHP_EOL;
$count = $reader->eachLine(function (string $line, int $num) {
// 最初の3行だけ表示
if ($num <= 3) {
echo " [{$num}] {$line}" . PHP_EOL;
}
});
echo "合計行数: {$count} 行" . PHP_EOL;
unlink($tmpFile);
出力例:
選択されたバッファサイズ: 8192 バイト
[1] Line 1: xxxxxxxx...
[2] Line 2: xxxxxxxx...
[3] Line 3: xxxxxxxx...
合計行数: 1000 行
例4:バッファ設定の成功・失敗を安全にハンドリングする
返り値が 0 = 成功という仕様に対応した、安全な設定クラスです。
<?php
class StreamReadBufferManager
{
private $stream;
private string $label;
private ?int $appliedSize = null;
public function __construct(resource $stream, string $label = 'stream')
{
$this->stream = $stream;
$this->label = $label;
}
/**
* バッファサイズを設定する
* 返り値 0 = 成功、0以外 = 失敗 という仕様に注意
*/
public function setReadBuffer(int $size): bool
{
if ($size < 0) {
throw new InvalidArgumentException("バッファサイズは0以上の整数を指定してください");
}
$result = stream_set_read_buffer($this->stream, $size);
// 0 が成功、0以外が失敗
if ($result === 0) {
$this->appliedSize = $size;
return true;
}
return false;
}
public function disableBuffer(): bool
{
return $this->setReadBuffer(0);
}
public function getAppliedSize(): ?int
{
return $this->appliedSize;
}
public function describe(): string
{
if ($this->appliedSize === null) {
return "[{$this->label}] バッファ未設定(PHPデフォルト)";
}
if ($this->appliedSize === 0) {
return "[{$this->label}] アンバッファード(バッファリング無効)";
}
return "[{$this->label}] バッファサイズ: {$this->appliedSize} バイト";
}
}
// 使用例
$stream = fopen('php://temp', 'r+');
$manager = new StreamReadBufferManager($stream, 'temp_stream');
$sizes = [0, 4096, 65536, -1];
foreach ($sizes as $size) {
try {
$ok = $manager->setReadBuffer($size);
$status = $ok ? "✓ 成功" : "✗ 失敗";
echo "{$status}: {$manager->describe()}" . PHP_EOL;
} catch (InvalidArgumentException $e) {
echo "✗ 例外: " . $e->getMessage() . PHP_EOL;
}
}
fclose($stream);
出力例:
✓ 成功: [temp_stream] アンバッファード(バッファリング無効)
✓ 成功: [temp_stream] バッファサイズ: 4096 バイト
✓ 成功: [temp_stream] バッファサイズ: 65536 バイト
✗ 例外: バッファサイズは0以上の整数を指定してください
例5:ネットワークストリームのバッファを調整してHTTPレスポンスを読み取る
ネットワーク遅延が大きい環境では、大きめのバッファを設定してシステムコール回数を削減します。
<?php
class BufferedHttpReader
{
private int $bufferSize;
private float $timeout;
public function __construct(int $bufferSize = 65536, float $timeout = 10.0)
{
$this->bufferSize = $bufferSize;
$this->timeout = $timeout;
}
public function fetch(string $url): array
{
$context = stream_context_create([
'http' => [
'timeout' => $this->timeout,
'follow_location' => true,
'user_agent' => 'PHP-BufferedReader/1.0',
],
]);
$stream = @fopen($url, 'r', false, $context);
if (!is_resource($stream)) {
throw new RuntimeException("ストリームを開けませんでした: {$url}");
}
// バッファを設定(返り値 0 = 成功に注意)
$setResult = stream_set_read_buffer($stream, $this->bufferSize);
$body = '';
$readCalls = 0;
$startTime = microtime(true);
while (!feof($stream)) {
$chunk = fread($stream, $this->bufferSize);
if ($chunk !== false && $chunk !== '') {
$body .= $chunk;
$readCalls++;
}
}
$meta = stream_get_meta_data($stream);
fclose($stream);
return [
'url' => $url,
'bytes' => strlen($body),
'read_calls' => $readCalls,
'buffer_size' => $this->bufferSize,
'buffer_set' => ($setResult === 0),
'elapsed_ms' => round((microtime(true) - $startTime) * 1000, 2),
'timed_out' => $meta['timed_out'],
'preview' => substr(strip_tags($body), 0, 60),
];
}
}
// 使用例
$reader = new BufferedHttpReader(bufferSize: 16384);
try {
$result = $reader->fetch('https://www.example.com/');
echo "URL : {$result['url']}" . PHP_EOL;
echo "取得バイト数 : {$result['bytes']} bytes" . PHP_EOL;
echo "fread呼出回数 : {$result['read_calls']} 回" . PHP_EOL;
echo "バッファ設定 : " . ($result['buffer_set'] ? '成功' : '失敗') . PHP_EOL;
echo "処理時間 : {$result['elapsed_ms']} ms" . PHP_EOL;
echo "プレビュー : {$result['preview']}" . PHP_EOL;
} catch (RuntimeException $e) {
echo "エラー: " . $e->getMessage() . PHP_EOL;
}
出力例:
URL : https://www.example.com/
取得バイト数 : 1256 bytes
fread呼出回数 : 1 回
バッファ設定 : 成功
処理時間 : 285.32 ms
プレビュー : Example Domain This domain is for use in illustrative...
例6:読み取り・書き込みバッファを両方設定するストリームチューナー
stream_set_read_buffer と stream_set_write_buffer を組み合わせて、ストリームのI/Oをトータルでチューニングします。
<?php
class StreamTuner
{
private $stream;
private array $appliedSettings = [];
public function __construct(resource $stream)
{
$this->stream = $stream;
}
public function setReadBuffer(int $size): static
{
$result = stream_set_read_buffer($this->stream, $size);
$this->appliedSettings['read_buffer'] = [
'requested' => $size,
'success' => ($result === 0), // 0 = 成功
];
return $this;
}
public function setWriteBuffer(int $size): static
{
$result = stream_set_write_buffer($this->stream, $size);
$this->appliedSettings['write_buffer'] = [
'requested' => $size,
'success' => ($result === 0), // 0 = 成功
];
return $this;
}
public function setBlocking(bool $enable): static
{
$result = stream_set_blocking($this->stream, $enable);
$this->appliedSettings['blocking'] = [
'requested' => $enable,
'success' => $result,
];
return $this;
}
public function setChunkSize(int $size): static
{
$previous = stream_set_chunk_size($this->stream, $size);
$this->appliedSettings['chunk_size'] = [
'requested' => $size,
'previous' => $previous,
'success' => true,
];
return $this;
}
public function getStream(): resource
{
return $this->stream;
}
public function report(): void
{
echo "=== ストリームチューニングレポート ===" . PHP_EOL;
foreach ($this->appliedSettings as $key => $info) {
$status = ($info['success'] ?? false) ? '✓' : '✗';
$value = $info['requested'] ?? 'N/A';
$extra = isset($info['previous']) ? "(変更前: {$info['previous']})" : '';
echo " {$status} {$key}: {$value}{$extra}" . PHP_EOL;
}
}
}
// 使用例:高スループット向けチューニング
$stream = fopen('php://temp', 'r+');
$tuner = new StreamTuner($stream);
$tuner->setReadBuffer(65536) // 64KB読み取りバッファ
->setWriteBuffer(65536) // 64KB書き込みバッファ
->setBlocking(true) // ブロッキングモード
->setChunkSize(65536); // 64KBチャンク(フィルタ向け)
$tuner->report();
// 実際に書き込み・読み取り
fwrite($tuner->getStream(), str_repeat('T', 1024));
rewind($tuner->getStream());
$data = fread($tuner->getStream(), 2048);
echo PHP_EOL . "読み取ったバイト数: " . strlen($data) . " bytes" . PHP_EOL;
fclose($stream);
出力例:
=== ストリームチューニングレポート ===
✓ read_buffer: 65536
✓ write_buffer: 65536
✓ blocking: 1
✓ chunk_size: 65536(変更前: 8192)
読み取ったバイト数: 1024 bytes
関連する関数との比較
| 関数 | 役割 | 返り値 |
|---|---|---|
stream_set_read_buffer | 読み取りバッファサイズを設定 | 0=成功、0以外=失敗 |
stream_set_write_buffer | 書き込みバッファサイズを設定 | 0=成功、0以外=失敗 |
stream_set_chunk_size | フィルタへのチャンクサイズを設定 | 変更前のサイズ(int) |
stream_set_blocking | ブロッキング/ノンブロッキング切り替え | bool |
stream_set_timeout | タイムアウト時間を設定 | bool |
stream_set_read_buffer vs stream_set_chunk_size
stream_set_read_buffer(8192)
└── PHPがOSからデータを取得する単位(バッファリング層)
→ システムコールの回数を削減するための設定
stream_set_chunk_size(8192)
└── バッファから「フィルタ」へデータを渡す単位
→ ストリームフィルタの呼び出し粒度を制御するための設定
| 観点 | stream_set_read_buffer | stream_set_chunk_size |
|---|---|---|
| 効果が出る場面 | フィルタあり・なし両方 | ストリームフィルタ使用時 |
| 制御する対象 | OS↔PHPのI/O単位 | PHP内部でのフィルタ呼び出し粒度 |
size=0 の意味 | アンバッファード | (1以上が必須) |
| 返り値 | 0=成功 | 変更前の値 |
よくある注意点・落とし穴
1. 返り値の判定は === 0 を使う
最も重要な注意点です。成功時に 0 を返すため、if (stream_set_read_buffer(...)) と書くと、成功(0 = falsy)が失敗と判定されます。
// NG:成功(0)をfalseと判定してしまう
if (!stream_set_read_buffer($stream, 8192)) {
echo "成功"; // ← 実は成功時にここに入ってしまう
}
// OK:厳密に比較する
if (stream_set_read_buffer($stream, 8192) === 0) {
echo "成功";
}
2. size=0 はアンバッファードを意味する
0 を渡すとバッファリングが無効になります。これはパフォーマンスを下げる場合もありますが、リアルタイム性が必要な場面(ログ監視など)では意図的に使います。
stream_set_read_buffer($stream, 0); // アンバッファード
stream_set_read_buffer($stream, 8192); // 8KBバッファ
3. すべてのストリームラッパーで効果があるわけではない
php://memory、php://temp、compress.zlib:// など一部のラッパーでは、設定しても効果が出ない(または無視される)場合があります。
// php://temp は常にメモリ上なのでバッファ設定の効果は限定的
$stream = fopen('php://temp', 'r+');
stream_set_read_buffer($stream, 65536); // 無視される可能性あり
4. デフォルトのバッファサイズは環境依存
PHPおよびOSの設定によってデフォルト値は異なります。本番環境で明示的に設定することで、環境差異を排除できます。
バッファサイズ選択の目安
| ユースケース | 推奨バッファサイズ |
|---|---|
| ログファイルのリアルタイム監視 | 0(アンバッファード) |
| 小〜中サイズのテキストファイル処理 | 8192(8KB) |
| 大容量ファイルの一括読み取り | 65536〜262144(64KB〜256KB) |
| ネットワークストリーム(低遅延) | 4096〜8192 |
| ネットワークストリーム(高スループット) | 65536〜131072 |
まとめ
| 項目 | 内容 |
|---|---|
| 関数名 | stream_set_read_buffer(resource $stream, int $size): int |
| 主な用途 | 読み取りバッファサイズの最適化 |
size=0 | アンバッファード(バッファリング無効) |
| 返り値 | 0 = 成功、0 以外 = 失敗 |
| 注意点 | 返り値判定は === 0、ラッパーによっては効果なし |
| PHP バージョン | PHP 5.3.3 以上 |
stream_set_read_buffer は、ファイル・ネットワーク・パイプといったストリームのI/Oパフォーマンスをアプリケーション側から調整できる重要な関数です。特に大容量ファイルの処理や高頻度の読み取り処理では、適切なバッファサイズの設定が全体のスループットに大きく影響します。
「返り値 0 = 成功」という独特な仕様をしっかり押さえた上で、stream_set_write_buffer や stream_set_chunk_size と組み合わせてストリームをトータルチューニングしてみてください。
