ご無沙汰しております。
今日は掲題のように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文の中に挟み込む方式は、あまり推奨しないかな。
コードは行儀、シンプルに誰でもわかるように書きべきでしょう。
コメントを残す