Symfony ORM のパフォーマンス比較 (2) Symfony2(PR4) & Doctrine2

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

さて、今回はいきなりですが、Symfony2 と Doctrine2 を使って計測してみます。
この計測を行うには、以下のような準備が必要なので、それは日を改めて記事にしたいと思います。

  • Symfony2/Doctrine2 環境でのエンティティクラスの準備
  • Symfony2 でコマンドの作成

今回は、上記準備は整っているものとして、計測に使ったコード部分のみを掲載します。

環境

使用したバージョンは以下です。

  • Symfony2 PR4
  • Doctrine2 ORM BETA4

※Symfony2 Sandbox に付属している Doctrine2 ORM は BETA4 でやや古いですが、そのままで。

Symfony2 & Doctrine2 ORM

INSERT 1万回

Doctrine2 で INSERT を行うには、エンティティオブジェクトを作って EntityManager で persist する方法と、DBAL から Connection オブジェクトを取得して、Connection オブジェクトの insert() メソッドを使う方法があります。

【処理A】 - エンティティをpersist

<?php
        $startTime = microtime(true);

        $em = $this->container->get('doctrine.orm.entity_manager');

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

            $em->persist($address);
        }
        $em->flush();

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

【処理B】 - DBAL の Connection オブジェクトで insert()

<?php
        $startTime = microtime(true);

        $conn = $this->container->get('doctrine.dbal.default_connection');

        for ($i = 0; $i < 10000; ++$i)
        {
            $conn->insert('address', array(
                'pref_name' => '岐阜県',
                'zip1'      => '111',
                'zip2'      => '2222',
                'addr1'     => '山奥の村一丁目一番地',
            ));
        }

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

これらの処理時間とメモリ使用量は次のようになりました。(参考までに、symfony 1.4/Doctrine 1.2の時の数値も並べてあります)

なんだか極端な結果がでましたね・・・。爆速ですが、メモリも激食いです・・・・。
ちなみに、コマンドの処理開始時点での memory_get_peak_usage() の結果は7.5MB程度です。Connection オブジェクトからの insert() の方は、もっとメモリを節約する方法があるような気がしますが・・・。

ところで、処理Aについては、メモリを大量に使うのは当然で、これは1万件すべてを一旦 EntityManager の UnitOfWork にためこんで、最後の flush() の時点で実際に DB に INSERT クエリーを発行する方式だからです。(だからと言って、Pure PHP Objectなのに 70MB かよ! とツッコみたくなりますが・・・)
このようなバッチ処理では、全部を UnitOfWork にためるのではなく、ある程度の件数ごとに flush() するような方法が Doctrine のドキュメントに書いてありました。

この方法で、バッチサイズを 1000、100、10、1 と変えて試してみました。コードは以下のようになります。

【処理A】 - エンティティをpersist - 特定の件数ごとに flush()

<?php
        $startTime = microtime(true);

        $em = $this->container->get('doctrine.orm.entity_manager');

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

            $em->persist($address);
            if ($i % 1000)
            {
                $em->flush();
                $em->clear();
            }
        }
        $em->flush();

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

結果は以下のようでした。

なんだかよく分からない結果ですが、メモリの消費量は抑えられるようです。

主キーランダム SELECT 1万回

ランダムな主キーで 1 件のレコードを取得します。
EntityManager の find() メソッドを使う方法と、DQL を使う方法で試してみます。

【処理A】- EntityManager の find() メソッド

<?php
        $startTime = microtime(true);

        $em = $this->container->get('doctrine.orm.entity_manager');

        for ($i = 0; $i < 10000; ++$i)
        {
            $address = $em->find('Application\HelloBundle\Entity\Address', (int)rand(1, 10000));
        }

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

【処理B】 - DQL から

<?php
        $startTime = microtime(true);

        $em = $this->container->get('doctrine.orm.entity_manager');

        $query = $em->createQuery('select a from Application\HelloBundle\Entity\Address a where a.id =:id');
        $query->setMaxResults(1);
        for ($i = 0; $i < 10000; ++$i)
        {
            $query->setParameter('id', (int)rand(1, 10000));
            $address = $query->getSingleResult();
        }

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

処理Bでは、ハイドレーションしない、というオプションも使えます。

<?php
            $address = $query->getSingleResult(Query::HYDRATE_ARRAY);

※処理Aの方式で、ハイドレーションオプションを指定する方法は、調べていません・・・。

これらの処理の実行時間と、メモリ使用量は次のようになりました。

メモリ使用量は Doctrine1 と比べてやや増えていますが、速度は Doctrine1 でハイドレーションしないときと比べても相当速いですね。

まとめ(にはあいかわらずなっていませんが)
  • Doctrine2 はパフォーマンスは向上しているっぽい
  • けど使い方によってはメモリ消費はきつい

とりあえずいくつかやってみましたが、まだまだ Doctrine2 の使い方をよく分かっていません!
みなさんも実際に使ってみて、「こう書くといい」というやり方など、教えてください!


次回は、同じ検証を、Symfony2 & DoctrineODM(MongoDB) でやったものを掲載予定です。
また、この記事で使った Command の作り方と、Doctrine2 で MySQL を使う場合の注意点のようなことも、別記事でアップします。

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好きな有志で集まったチームです。