タグ別アーカイブ: チューニング

phpのcount関数の実装を見る

ご無沙汰しております。
今日は掲題のようにcount関数の実装を見て行きたいと思います。

概要

phpの開発を行ったことがある方であれば、下記のようなコード見たことあると思います。

$something = array('a', 'b', 'c');

for($i=0; $i<count($something); $i++) {
 // do something
}

また下記のようなコードも目にすることがあると思います。

$something = array('a', 'b', 'c');
$count = count($something);

for($i=0; $i<$count; $i++) {
 // do something
}

これらのコードどちらが効率的なのでしょうか。
なんとなく後者のほうが効率が良いようなことは想像に難しくないと思います。
でも、はっきり前者は悪だと断言できる方は実は少ないのではないでしょうか。

後者にしておけば困ったことにはならないでしょう。ただし、コードが一行増える。微々たるものですが。
どちらかと言うと前者のほうがコードとしてはすっきりしていると思います。

それに何より中身がどうなっているのかわからないのが気分的にすっきりしない。
今日はphpのcount関数の実装を確認してみたいと思います。

調査

php公式からソースコードをダウンロードして調査します。
バージョンは現在の最新安定版である5.6.14としました。

zval構造体

まずphpのzend構造体から確認します。
zend.hをみてみましょう。

typedef struct _zval_struct zval;
struct _zval_struct {
    /* Variable information */
    zvalue_value value;       /* value */
    zend_uint refcount__gc;
    zend_uchar type;          /* active type */
    zend_uchar is_ref__gc;
};

typedef union _zvalue_value {
    long lval;        /* long value */
    double dval;      /* double value */
    struct {
        char *val;
        int len;
    } str;
    HashTable *ht;    /* hash table value */
    zend_object_value obj;
    zend_ast *ast;
} zvalue_value;

phpでは変数はzval構造体で表されます。
zval構造体は参照カウントや、変数実体を保持するzvalue_valueという共用体を更に内部で保持します。
union構造体とは宣言された値の内どれか一つを保持する構造体のことです。

phpではlongやdoubleやstringなどの値に応じてzvalue_value共用体が保持する実体を切り替えるようになっています。
配列変数の場合はHashTable構造体を保持することになります。

Hashtable構造体

さらにzend_hash.hファイルを確認してその構造を確認してみましょう。

struct _hashtable;
typedef struct _hashtable {
    uint nTableSize;
    uint nTableMask;
    uint nNumOfElements;
    ulong nNextFreeElement;
    Bucket *pInternalPointer;       /* Used for element traversal */
    Bucket *pListHead;
    Bucket *pListTail;
    Bucket **arBuckets;
    dtor_func_t pDestructor;
    zend_bool persistent;
    unsigned char nApplyCount;
    zend_bool bApplyProtection;
 #if ZEND_DEBUG
    int inconsistent;
 #endif
} HashTable;

hashtable構造体については詳細な説明は今回は割愛します。
いったんデータ構造を確認したら今度はcount関数の実装を見てみることにしましょう。

count関数

array.cファイルを覗いてみます。

PHP_FUNCTION(count)
{
    zval *array;
    long mode = COUNT_NORMAL;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) {
        return;
    }

    switch (Z_TYPE_P(array)) {
        case IS_NULL:
            RETURN_LONG(0);
            break;
        case IS_ARRAY:
            RETURN_LONG (php_count_recursive (array, mode TSRMLS_CC));
            break;
        case IS_OBJECT: {
#ifdef HAVE_SPL
            zval *retval;
#endif
            /* first, we check if the handler is defined */
            if (Z_OBJ_HT_P(array)->count_elements) {
                RETVAL_LONG(1);
                if (SUCCESS == Z_OBJ_HT(*array)->count_elements(array, &Z_LVAL_P(return_value) TSRMLS_CC)) {
                    return;
                }
            }
#ifdef HAVE_SPL
            /* if not and the object implements Countable we call its count() method */
            if (Z_OBJ_HT_P(array)->get_class_entry && instanceof_function(Z_OBJCE_P(array), spl_ce_Countable TSRMLS_CC)) {
                zend_call_method_with_0_params(&array, NULL, NULL, "count", &retval);
                if (retval) {
                    convert_to_long_ex(&retval);
                    RETVAL_LONG(Z_LVAL_P(retval));
                    zval_ptr_dtor(&retval);
                }
                return;
            }
#endif
        }
        default:
            RETURN_LONG(1);
            break;
    }
}

ごちゃごちゃと記述してありますが、今回は単純な配列な値の場合を確認します。
すると対象は一行だけでphp_count_recursiveを呼び出していることがわかります。

さらにphp_count_recursiveをおってみましょう。

PHPAPI int php_count_recursive(zval *array, long mode TSRMLS_DC) /* {{{ */
{
    long cnt = 0;
    zval **element;

    if (Z_TYPE_P(array) == IS_ARRAY) {
        if (Z_ARRVAL_P(array)->nApplyCount > 1) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "recursion detected");
            return 0;
        }

        cnt = zend_hash_num_elements(Z_ARRVAL_P(array));
        if (mode == COUNT_RECURSIVE) {
            HashPosition pos;

            for (zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(array), &pos);
                zend_hash_get_current_data_ex(Z_ARRVAL_P(array), (void **) &element, &pos) == SUCCESS;
                zend_hash_move_forward_ex(Z_ARRVAL_P(array), &pos)
            ) {
                Z_ARRVAL_P(array)->nApplyCount++;
                cnt += php_count_recursive(*element, COUNT_RECURSIVE TSRMLS_CC);
                Z_ARRVAL_P(array)->nApplyCount--;
            }
        }
    }

    return cnt;
}

ここでも型チェックや再帰的に関数を呼び出している部分がありますが、本質的な部分は一行だけです。
変数cntを代入しているzend_hash_num_elementsを見てみましょう。
実装はzend_hash.cにあります。

ZEND_API int zend_hash_num_elements(const HashTable *ht)
{
    IS_CONSISTENT(ht);

    return ht->nNumOfElements;
}

IS_CONSISTENTマクロはハッシュテーブルが破壊されていないかどうかを確認するものです。
さてここでようやく実装が見えましたね。
count関数の行っていることはhashtable構造体のnNumOfElementsを返却しているだけなのです。

結論

冒頭で出した2つのコードのパフォーマンスの違いはそこまで大きくないでしょう、ビッグオー的に計算量で表すとどちらもO(1)になるからです。

ただ実装で言うとメソッドの呼び出しが3つほど噛まれていますので、関数スタックにガツガツと何度も積み上げられることになります。
特にパフォーマンスに気を使う部分であれば、count関数の呼び出しは一度だけに限定するほうが良いでしょう。

個人的にはfor文の中に挟み込む方式は、あまり推奨しないかな。
コードは行儀、シンプルに誰でもわかるように書きべきでしょう。


xhprofを利用する

アプリケーションのパフォーマンスを向上させるためにはプロファイラと呼ばれるツールを利用するのが早いです。
phpではxhprofというプロファイラがメジャーであり今回はその導入手順や使用感について紹介します。

インストール

PECLから入れます。そんなに難しくありません。

  • ソースをダウンロード
  • wget http://pecl.php.net/get/xhprof-0.9.2.tgz
    
  • make
  • tar zxvf xhprof-0.9.2.tar
    cd xhprof-0.9.2/extension/
    phpize
    make
    make install
    
  • php.iniを編集
  • [xhprof]
    extension=xhprof.so
    xhprof.output_dir=/var/log/xhprof
    
  • ログディレクトリを作成
  • mkdir /var/log/xhprof
    chmod -R 777 /var/log/xhprof
    
  • apache再起動
  • service httpd restart
    
  • xhprofのテンプレディレクトリをドキュメントルート配下に移動
  • cp -r /tmp/xhprof-0.9.2/xhprof_html {document_root}/xhprof_html
    cp -r /tmp/xhprof-0.9.2/xhprof_lib {document_root}/xhprof_lib
    
  • プロファイリングしたい箇所にコードを埋め込みます
  • {document_root}は適宜自分の環境に置き換えてください。

    xhprof_enable(); // プロファイリング開始
    
    //
    // ** プロファイリングしたい処理をここに記述 **
    //
    
    $xhprof_data = xhprof_disable();    //stop profiler
     
    //  プロファイリングページへのリンクを追加
    include_once "{document_root}/xhprof_lib.php";
    include_once "{document_root}/xhprof_runs.php";
    $xhprof_runs = new XHProfRuns_Default();
    $run_id = $xhprof_runs->save_run($xhprof_data, $XHPROF_SOURCE_NAME);
    echo "<a href=\"http://{document_root}/xhprof_html/index.php?run=$run_id&source=$XHPROF_SOURCE_NAME\">xhprof Result</a>\n";
    

    結果

    実行すると結果ページヘのリンクが表示されます。クリックすることで結果の詳細を確認することができます。

    おぉー参照できましたね。なかなかにいいかんじです。

  • 各項目
  • 項目 意味
    Function Name 関数名
    Calls プロファイリングした期間でコールされた回数
    Calls% コール回数のパーセント表示
    Incl. Wall Time 処理にかかった時間のうち、その関数が消費した時間。なお処理をネストした場合全経過時間を含むことになるので、あまり参考することはないと思われる
    IWall% Incl. Wall Timeのパーセント表示
    Excl. Wall Time 実際にその関数のみが消費した時間。多くの場合はこの項目なんかを参照しながらプロファイリングすることになると思われる
    IWall% Execl. Wall Timeのパーセント表示

    なるほどかなり正確な情報が把握できて、最終的にボトルネックになっている箇所が確認できます。
    実際に動かしてみると、当然ですが大きく時間を消費していたのがネットワーク処理、ついでファイル処理などかなり実用的なレベルで参考することができそうです。

    余談

    ちなみに結構長い間フリーランスしてる同僚と、プロファイリングからの性能検証について雑談してて。
    現実的な現場感としては、こういったプロファイリングを常に実行してボトルネックをチェックすることが多いのかな。ということについて話してると、現実的には実際に何か問題が発覚してから調査する。ってのが多いようです。

    あとは細かな性能検証も大事なんですが、昔と比べてマシンのスペックが格段に向上しているのでアプリのチューニングを突き詰めていくよりかは、マシンを追加してインフラ面からパワーを上げて対応する事が多いです。
    (もちろん普通にボトルネックになっているような場合は除く。アプリのチューニングが不要と言っているわけではないです。)

    実際その通りで、僕もどちらかと言うと小難しいコンパクトなコードを書くよりかは、コストが多少高くつくとしても可読性の高いコードを書くように心がけています。
    (よくある普通のレベルの企業では)あまり出番の多いわけではさそうですが、覚えておくと絶対役に立ちますね。
    個人的には常にこのへんの意識は頭のなかにあるエンジニアでいたいです。