Symfony ORM のパフォーマンス比較 (1) symfony 1.4 & Doctrine 1.2

この記事は、Symfony アドベントカレンダー 2010 に参加しています。


以前、DoctrineとPropelのパフォーマンス比較 - しんふぉにゃんという記事を書きました。その記事が2009年9月なので、1年以上前ですね。それまでPropel派だった私はその記事以降、Doctrine が(パフォーマンス的にも)結構使えるということが分かり、すっかり Doctrine 使いになってしまっています。その後 Doctrine も Propel も、また symfony 本体もバージョンアップしていますので、比較結果に変化があるかもしれません。
また、現在最も気になるのは、Symfony 2 と Doctrine2 のパフォーマンスじゃないでしょうか。
このあたりを確かめるために、新しいバージョンで同じような処理を行って比較してみたいと思います。

環境

環境は以下のようになっています。前回よりパワーアップしています。

  • CPU Core i7 920
  • OS Ubuntu 10.10 (デスクトップ版 32bit)
  • ノーマルなHDD(SSDではありません)
  • メモリ 3GB
  • PHP 5.3.3、APC有効
  • MySQL 5.1.49

また、前回は触れていませんでしたが、MySQLのmy.cnfで以下の設定をしています。

  • innodb_flush_log_at_trx_commit=0

(この設定には注意が必要ですが、insertのテストでは大きく性能に関わりますので0にしています)

スキーマ

今回テストに使うスキーマは以下のようになっています。後でDoctrine2での比較もあるため、Timestampableなフィールドは省いています。

symfony 1.4 & Doctrine 1.2

今回は、あまり面白みがないかもしれませんが、現役でバリバリ活躍しているこの構成からです。

symfony 1.4.8のソースパッケージをインストールし、frontendアプリケーションを作り、上記のスキーマをconfig/doctrine/schema.ymlに記述してDBとモデルを作成しました。

INSERT 1万回

まずはINSERTからです。前回の記事で、ORMで素直にやる方法と、Doctrineのコネクションオブジェクトを取得してinsertメソッドを使う方法です。
適当なタスクを作成し、タスクの処理本体部分に以下のように記述します。

【処理A】

<?php
    $startTime = microtime(true);

    for ($i = 0; $i < 10000; ++$i)
    {
      //echo $i . PHP_EOL;
      $address = new Address();
      $address->setPrefName('岐阜県');
      $address->setZip1('111');
      $address->setZip2('2222');
      $address->setAddr1('山奥の村一丁目一番地');
      $address->save();
      $address->free();
    }

    echo "proc time:" . (microtime(true) - $startTime) . 'ms' . PHP_EOL;
    echo "memory usage:" . (memory_get_peak_usage(true) / 1048576) . 'MB' . PHP_EOL;

次に、コネクションオブジェクト(Doctrine_Connection)を取得して、コネクションオブジェクトのinsert()メソッドを使います。

【処理B】

<?php
    $startTime = microtime(true);
    
    $table = Doctrine::getTable('Address');
    $conn = $table->getConnection();

    for($i=0; $i<10000;++$i){
      $conn->insert($table,array(
        'zip1'=>'111',
        'zip2'=>'2222',
        'pref_name'=>'岐阜県',
        'addr1'=>'山奥の村一丁目一番地',
      ));
    }
    
    echo "proc time:" . (microtime(true) - $startTime) . 'ms' . PHP_EOL;
    echo "memory usage:" . (memory_get_peak_usage(true) / 1048576) . 'MB' . PHP_EOL;

処理Aと処理Bのそれぞれの実行時間とメモリ使用量は以下のようになります。

主キーランダム SELECT 1万回

次に、ランダムな主キーで1件のレコードを取得する処理を実行してみます。
データベースは、テーブルのオートインクリメント値をクリアした後に、INSERT 1万回のテストでデータを投入した状態なので、IDが1から10000のレコードがあります。

Doctrineのクエリーキャッシュのあり/なしとクエリーの書き方の違い、それにハイドレーションあり/なしを組み合わせて比較してみます。

【処理A】クエリーキャッシュなし、ハイドレーションなし

<?php
    $startTime = microtime(true);

    $q = AddressTable::getInstance()->createQuery('a')->where('a.id = ?');
    for ($i = 0; $i < 10000; ++$i)
    {
      $rec = $q->fetchOne(array((int)rand(1, 10000)), Doctrine_Core::HYDRATE_NONE);
      unset($rec);
    }
    
    echo "proc time:" . (microtime(true) - $startTime) . 'ms' . PHP_EOL;
    echo "memory usage:" . (memory_get_peak_usage(true) / 1048576) . 'MB' . PHP_EOL;

※ハイドレーションありは、Doctrine_Core::HYDRATE_NONEを削除


【処理B】クエリーキャッシュあり、ハイドレーションなし

<?php
    $startTime = microtime(true);
    $manager = Doctrine_Manager::getInstance();
    $manager->setAttribute(Doctrine::ATTR_QUERY_CACHE, new Doctrine_Cache_Apc());

    $q = AddressTable::getInstance()->createQuery('a')->where('a.id = ?');
    for ($i = 0; $i < 10000; ++$i)
    {
      $rec = $q->fetchOne(array((int)rand(1, 10000)), Doctrine_Core::HYDRATE_NONE);
      unset($rec);
    }
    
    echo "proc time:" . (microtime(true) - $startTime) . 'ms' . PHP_EOL;
    echo "memory usage:" . (memory_get_peak_usage(true) / 1048576) . 'MB' . PHP_EOL;


【処理C】クエリーキャッシュなし、ハイドレーションなし、クエリーをループ内で生成

<?php
    $startTime = microtime(true);

    for ($i = 0; $i < 10000; ++$i)
    {
      $q = AddressTable::getInstance()->createQuery('a')->where('a.id = ?');
      $rec = $q->fetchOne(array((int)rand(1, 10000)), Doctrine_Core::HYDRATE_NONE);
      $q->free();
      unset($rec, $q);
    }
    
    echo "proc time:" . (microtime(true) - $startTime) . 'ms' . PHP_EOL;
    echo "memory usage:" . (memory_get_peak_usage(true) / 1048576) . 'MB' . PHP_EOL;

これらの3つの処理の、それぞれの実行時間とメモリ使用量は以下のようになります。

まとめ(にはあまりなっていませんが)
  • Doctrine1は、ハイドレーションのコストは高い
  • 今回のテストのような使い方(バッチの先頭のみでクエリーを作成している)では、クエリーキャッシュの恩恵はない
    • ループ内で、同じクエリーオブジェクトを使い回せるため、再生成の必要がない
  • クエリーオブジェクトの作成コストは無視できる程度?
  • 大量のINSERTでは特に、コネクションオブジェクト経由でinsertしたときの速度改善が大きい

このまとめだけでは、なんだかよく分かりませんね・・・。(検証が正しいのかもちょっと自信がないので、みなさんもやってみてツッコミお願いします)
ただ、今回のテストのように1回の処理で扱うデータの件数がある程度大きくなる場合は、ハイドレーションのオーバーヘッドが顕著になりますね。


次回は、同じ検証を、Symfony2 & Doctrine2 でやったものを掲載予定です。

Symfony Advent 2010であなたの記事を公開してみませんか?

Symfony Advent 2010では12月1日から12月24日までを使って日替わりでsymfonyでイイなと思った小さなtipsから内部構造まで迫った解説などをブログ記事にし て公開していくイベントです。
参加についてはATNDで参加表明の上、Google
GroupのSymfony Advent 2010に追加リクエストを送信ください。
Symfony Advent 2010チーム一同、あなたの参加をお待ちしております。
日本Symfonyユーザー会
Symfony アドベントカレンダー2010
※Syfony Advent 2010はsymfony好きな有志で集まったチームです。