[PHP]stream_set_read_bufferで読み取りバッファを最適化する|I/Oパフォーマンス向上の実践ガイド

PHP

はじめに

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

パラメータ

パラメータ説明
$streamresource対象のストリームリソース
$sizeintバッファサイズ(バイト数)。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_bufferstream_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_bufferstream_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://memoryphp://tempcompress.zlib:// など一部のラッパーでは、設定しても効果が出ない(または無視される)場合があります。

// php://temp は常にメモリ上なのでバッファ設定の効果は限定的
$stream = fopen('php://temp', 'r+');
stream_set_read_buffer($stream, 65536); // 無視される可能性あり

4. デフォルトのバッファサイズは環境依存

PHPおよびOSの設定によってデフォルト値は異なります。本番環境で明示的に設定することで、環境差異を排除できます。


バッファサイズ選択の目安

ユースケース推奨バッファサイズ
ログファイルのリアルタイム監視0(アンバッファード)
小〜中サイズのテキストファイル処理8192(8KB)
大容量ファイルの一括読み取り65536262144(64KB〜256KB)
ネットワークストリーム(低遅延)40968192
ネットワークストリーム(高スループット)65536131072

まとめ

項目内容
関数名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_bufferstream_set_chunk_size と組み合わせてストリームをトータルチューニングしてみてください。

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