読者です 読者をやめる 読者になる 読者になる

file_get_contents() 互換の高速関数を書いたが使わなかったので供養する

とあるプロジェクトの一括処理系のチューニングを行った際に、file_get_contents()でのリモート画像の読み込みが O ( n ) の計算量で実行されてる箇所があったので、file_get_contents()互換の高速な関数を作成しました。

こちらのサイトによるとfsockopen()が一番速いらしいので、cURLバージョンとfsockopen()バージョンの2つを作成してみました。

cURLを使ったバージョン

  • file_get_contents()比で処理時間20%減
  • メモリ使用量は変わらず
//********************************
//   CURLを使ったfile_get_contents
//   file_get_contentsと同様に読み込んだデータを返す
//   file_get_contentsと同様に失敗した場合はWarningを発生しfalseを返す
//********************************
function curl_get_contents($url, array $setoptArray = null)
{
    $ch = curl_init($url);
    $options = $setoptArray;
    if ($options === null) {
        // デフォルトオプション
        $options = array(
            CURLOPT_HEADER         => false, // HTTPヘッダを出力しない
            CURLOPT_RETURNTRANSFER => true,  // curl_exec()の戻り値として受け取る
            CURLOPT_SSL_VERIFYPEER => false, // 証明書を検証しない
            CURLOPT_SSL_VERIFYHOST => false, // ホストを検証しない
            CURLOPT_TIMEOUT        => 3,     // cURLの最大実行時間(秒)
            CURLOPT_CONNECTTIMEOUT => 3,     // HTTP接続の最大実行時間(秒)
        );
    }
    curl_setopt_array($ch, $options);

    $result = curl_exec($ch);

    if (curl_errno($ch)) {
        trigger_error("curl_get_contents error: " . curl_errno($ch), E_USER_WARNING);
        return false;
    }
    curl_close($ch);

    return $result;
}

fsockを使ったバージョン

  • file_get_contents()比で処理時間37%減!
  • メモリ使用量は変わらず
//********************************
//   fsockを使ったfile_get_contents
//   file_get_contentsと同様に読み込んだデータを返す
//   file_get_contentsと同様に失敗した場合はWarningを発生しfalseを返す
//********************************
function fsock_get_contents($url) {
    $timeout = 3;
    if (!$purl = parse_url($url)) {
        trigger_error("Invalid URL: $url", E_USER_WARNING);
        return false;
    }
    if (empty($purl['port'])) {
        $purl['port'] = 80;
    }
    if ($purl['scheme'] == 'https') {
        $purl['port'] = 443;
        $fp = fsockopen('tls://' . $purl['host'], $purl['port'], $errNo, $errStr, $timeout);
    } else {
        $fp = fsockopen($purl['host'], $purl['port'], $errNo, $errStr, $timeout);
    }

    if (!$fp) {
        trigger_error("$errStr ($errNo)", E_USER_WARNING);
        return false;
    }
    socket_set_timeout($fp, $timeout);

    if (!empty($purl['query'])) {
       $purl['path'] .= '?' . $purl['query'];
    }
    if (substr($purl['path'], 0, 1) !== '/') {
        $purl['path'] = '/' . $purl['path'];
    }
    $request = "GET {$purl['path']} HTTP/1.1\r\n"
             . "Host: {$purl['host']}\r\n"
             . "User-Agent: PHP-Script/1.0\r\n"
             . "Content-Type: application/x-www-form-urlencoded\r\n"
             . "Connection: Close\r\n\r\n";
    fwrite($fp, $request);

    $response = '';
    while (!feof($fp)) {
        $response .= fgets($fp);
    }

    fclose($fp);

    list($head, $body) = explode("\r\n\r\n", $response, 2);
    // リダイレクトがあれば追跡する
    if (preg_match('/location\: (.*)\r\n/i', $head, $matches)) {
        return fsock_get_contents($matches[1]);
    }

    return $body;
}

開発環境ではかなり速くなったのですが、production環境で試すとなぜか速くならなかったので実際にはproductionに投入していません。 おそらくCPUやディスクの状況が開発環境とは違うのでしょう。

せっかく書いたのでまたいつかどこかのプロジェクトで役に立ってくれるといいなと思いつつここに供養しようと思います。