Slide 1

Slide 1 text

PHP8.2から見る、2つの配列 PHP Conference Japan 2023

Slide 2

Slide 2 text

meihei / 江間 洋平 株式会社PR TIMES Backend Engineer (PHP/Python/Go) X: @app1e_s GitHub: @meihei3 Bluesky: @meihei.bsky.social #BLEACH #VTuber(どラ、ぶいぱい、個人) #クラロワ #モンハンNOW #秋競馬 etc. 自己紹介 2

Slide 3

Slide 3 text

最初にお断り ● プロポーザルに”話さないこと”として「探索以外の HashTableの基本操作(追加・更新・削除)」と書い ていましたが、ちゃんと理解するために「追加・削 除」の説明も話します。 ● そのため、少しペースを早めて話します。 ● また、このスライドは公開済みです。文字が読めな いところは、是非お手元のPCでも開いて貰えればと 思います。 3

Slide 4

Slide 4 text

本日のゴール ● PHPの配列の操作の、内部挙動を理解する ● php-srcちょっと読める気分になる 4

Slide 5

Slide 5 text

アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 5

Slide 6

Slide 6 text

アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 6

Slide 7

Slide 7 text

前準備 ● PHPの配列のおさらい ● C言語の知識 ● 道具の準備 7

Slide 8

Slide 8 text

前準備 ● PHPの配列のおさらい ● C言語の知識 ● 道具の準備 8

Slide 9

Slide 9 text

PHP の配列は、実際には 順番付けられたマップです。 9

Slide 10

Slide 10 text

PHP の配列は、実際には 順番付けられたマップです。 10

Slide 11

Slide 11 text

PHPの配列 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; 11

Slide 12

Slide 12 text

順番付けられた $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; ● 追加された順番を担保している ① ② ③ ④ ⑤ next next next next 12

Slide 13

Slide 13 text

マップ $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; ● キーによって値が取り出せるデータ構造 ● 連想配列・辞書などとも呼ばれている キーと値がペアになってる 13

Slide 14

Slide 14 text

前準備 ● PHPの配列のおさらい ● C言語の知識 ● 道具の準備 14

Slide 15

Slide 15 text

php-srcは結構難読 15

Slide 16

Slide 16 text

php-srcのコードの一部 union { struct { ZEND_ENDIAN_LOHI_4( uint8_t flags, uint8_t _unused, uint8_t nIteratorsCount, uint8_t _unused2 ) } v; uint32_t flags; } u; 16

Slide 17

Slide 17 text

php-srcのコードの一部 #if SIZEOF_SIZE_T == 4 # define HT_HASH_TO_BUCKET_EX(data, idx) \ ((Bucket*)((char*)(data) + (idx))) #elif SIZEOF_SIZE_T == 8 # define HT_HASH_TO_BUCKET_EX(data, idx) \ ((data) + (idx)) #else # error "Unknown SIZEOF_SIZE_T" #endif 17

Slide 18

Slide 18 text

? 18

Slide 19

Slide 19 text

C言語で最低限必要な 知識を身につけよう 19

Slide 20

Slide 20 text

C言語の配列 配列とは、同一の型のデータを メモリ上に一列に並べたもの。 (例) int arr[] = {1, 2, 3}; であれば、 12 bytes の領域に 4 bytes の int 型 のデータが 3 つ並べられる。 20

Slide 21

Slide 21 text

ポインタ アドレスを記憶する変数のこと ポインタに*をつける事で、アド レスが示す値にアクセスできる (例) int x = 0x20241008; int *p = &x; であれば、p は int へのポインタ型とな り、 p には x のアドレスが保存されてい る。 21

Slide 22

Slide 22 text

配列とポインタ 配列はその先頭要素への ポインタに読み替えられる ● &arr[0] と arr は同義 ● arr[0] と *arr は同義 ● arr[1] と *(arr+1) は同義 22

Slide 23

Slide 23 text

ポインタの操作 ポインタ演算は整数演算とは異なり、型のサイズ分アド レスを移動する。 (例) int *x; と char *s; のポインタ操作の比較 sizeof(int) = 4 bytes sizeof(char) = 1 byte 23

Slide 24

Slide 24 text

マクロ コンパイル時にソースコードの一部として展開される、 事前に定義されたシンボルやコードの断片。 ● #define ディレクティブを用いて定義 ● 定数マクロ:値をマクロ名として定義するマクロ ● 関数マクロ:引数を取ることができるマクロ ● 文マクロ:C言語の文や式から成るマクロ 24

Slide 25

Slide 25 text

条件付きコンパイル 条件に基づきコードをコンパイルするか、除外するかを 決定することができる。 ● #if と #endif ディレクティブを用いる ● #elif や #else で複数の条件にも対応できる 25

Slide 26

Slide 26 text

構造体(structure) 異なるデータ型の変数を1つのグループとしてまとめる ためのデータ構造。 (例) 2 つの型から成る構造体 struct example { int32_t i; int *ptr; } 26

Slide 27

Slide 27 text

共用体(union) 複数の異なるデータ型を1つのメモリ領域に格納するた めのデータ構造。 (例) 3 つの型から成る共用体 union example { int32_t i; int *ptr; char str[8]; } 27

Slide 28

Slide 28 text

視聴者の皆さん「(C言語を完全に理解する)」 28

Slide 29

Slide 29 text

php-srcのコードの一部 union { struct { ZEND_ENDIAN_LOHI_4( uint8_t flags, uint8_t _unused, uint8_t nIteratorsCount, uint8_t _unused2 ) } v; uint32_t flags; } u; 共用体 文マクロの使用 構造体 29

Slide 30

Slide 30 text

php-srcのコードの一部 #if SIZEOF_SIZE_T == 4 # define HT_HASH_TO_BUCKET_EX(data, idx) \ ((Bucket*)((char*)(data) + (idx))) #elif SIZEOF_SIZE_T == 8 # define HT_HASH_TO_BUCKET_EX(data, idx) \ ((data) + (idx)) #else # error "Unknown SIZEOF_SIZE_T" #endif 条件付きコンパイル 関数マクロの定義 30

Slide 31

Slide 31 text

● PHPは魔法ではない ● 一つ一つ見ていけばコンピューターサイエンス の知識で説明可能 (脱線)PHPの配列もメモリ上に並ぶ $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; ※この配列はイメージです。実際は4bytesではありません。 31

Slide 32

Slide 32 text

前準備 ● PHPの配列のおさらい ● C言語の知識 ● 道具の準備 32

Slide 33

Slide 33 text

コードを読むだけでは理解が難しい 33

Slide 34

Slide 34 text

コードを読むだけでは理解が難しい デバッグをして挙動を確認する 34

Slide 35

Slide 35 text

● GDB を使ったデバッグ ○ 王道だけど、個人的には難しい。 ● PHP Extension によるプリントデバッグ ○ PHP Extension から直接変数へアクセスする php-src のデバッグ 35

Slide 36

Slide 36 text

● PHP Extension の開発環境の整備 ○ 雛形を利用する(zeriyoshi/pskel) ● 欲しい情報を print する ○ GitHub Copilot と ChatGPT を駆使する PHP Extension の開発 PHP Extension の開発については 13:45〜 トラック6 で発表があるので、 是非聞いてみてください! https://fortee.jp/phpcon-2023/proposal/747ee16f-31 69-4094-be4f-b5b558273ea9 36

Slide 37

Slide 37 text

アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 37

Slide 38

Slide 38 text

配列の全体図 38

Slide 39

Slide 39 text

配列の全体図 配列本体であり 配列の情報を 管理する構造体 39

Slide 40

Slide 40 text

配列の全体図 キーとデータの配列の添字 を紐付けるハッシュ表 配列本体であり 配列の情報を 管理する構造体 40

Slide 41

Slide 41 text

配列の全体図 キーとデータの配列の添字 を紐付けるハッシュ表 データの配列 配列本体であり 配列の情報を 管理する構造体 41

Slide 42

Slide 42 text

HashTable structure struct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( uint8_t flags, uint8_t _unused, uint8_t nIteratorsCount, uint8_t _unused2) } v; uint32_t flags; } u; uint32_t nTableMask; union { uint32_t *arHash; Bucket *arData; zval *arPacked; }; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor; }; 42

Slide 43

Slide 43 text

HashTable structure ● PHPの配列型(IS_ARRAY) を成す構造体 ● 「順序付けられたマップ」を 提供する ● sizeof: 56 bytes 43

Slide 44

Slide 44 text

リファレンスカウントとGCを 管理する構造体 HashTable structure 44

Slide 45

Slide 45 text

配列の状態を管理するフラグ HashTable structure 45

Slide 46

Slide 46 text

ハッシュ表用のビットマスク HashTable structure 46

Slide 47

Slide 47 text

実際のデータの配列(の先頭要 素)へのポインタ HashTable structure 47

Slide 48

Slide 48 text

実際に使用されているエントリ の数 (要素を削除しても減らない) HashTable structure 48

Slide 49

Slide 49 text

配列に含まれる要素の数 (要素を削除すると減る) HashTable structure 49

Slide 50

Slide 50 text

データの配列のサイズ(長さ) HashTable structure 50

Slide 51

Slide 51 text

データの配列の添字 (current(), next() などで使わ れるもの) HashTable structure 51

Slide 52

Slide 52 text

自動的に割り当てられる次の 数値キーの値 (文字列キーが追加されても変 わらず、数値キーが追加される と+1される) HashTable structure 52

Slide 53

Slide 53 text

配列の要素を破棄するためのデ ストラクタへのポインタ HashTable structure 53

Slide 54

Slide 54 text

データの配列へのポインタは ● Bucket の配列: arData ● zval の配列: arPacked を選択できる共用体となってい る。 (タイトル回収)2つの配列 Use more compact representation for packed arrays. #7491 54

Slide 55

Slide 55 text

2つの配列は共用体で切り替える Packed Array ht->arPacked でアクセス 通常のHashTable ht->arData でアクセス 55

Slide 56

Slide 56 text

通常のHashTable 56

Slide 57

Slide 57 text

通常のHashTable(PHPコード) $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; ● キーを指定して、値を取り出す ● Packed Arrayではないものは全てこれ 57

Slide 58

Slide 58 text

通常のHashTableの全体図 58

Slide 59

Slide 59 text

通常のHashTableの全体図 キーとデータの配列の添字 を紐付けるハッシュ表 59

Slide 60

Slide 60 text

通常のHashTableの全体図 Bucket(構造体)型の配列 60

Slide 61

Slide 61 text

通常のHashTableの全体図 キーの文字列(の構造体) 61

Slide 62

Slide 62 text

Bucket structure typedef struct _Bucket { zval val; zend_ulong h; zend_string *key; } Bucket; 62

Slide 63

Slide 63 text

● 配列の要素を表す構造体 ● 値とキー(文字列・数値)が ペアになっている Bucket structure 63

Slide 64

Slide 64 text

● 配列の値の部分 ● zval 型 ● sizeof: 16 bytes (zval構造体については後述) Bucket structure 64

Slide 65

Slide 65 text

● 文字列キーのハッシュ値 ● 数値キーの値 ● zend_ulong 型 ● sizeof: 8 bytes Bucket structure 65

Slide 66

Slide 66 text

● 文字列キーへのポインタ ○ 数値キーの場合はNULL ● zend_string 型 ● sizeof: 8 bytes (zend_string構造体は後述) Bucket structure 66

Slide 67

Slide 67 text

zval structure struct _zval_struct { zend_value value; union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( uint8_t type, uint8_t type_flags, union { uint16_t extra; } u) } v; } u1; union { uint32_t next; uint32_t cache_slot; uint32_t opline_num; uint32_t lineno; uint32_t num_args; uint32_t fe_pos; uint32_t fe_iter_idx; uint32_t guard; uint32_t constant_flags; uint32_t extra; } u2; }; 67

Slide 68

Slide 68 text

zval structure struct _zval_struct { zend_value value; union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( uint8_t type, uint8_t type_flags, union { uint16_t extra; } u) } v; } u1; union { uint32_t next; uint32_t cache_slot; uint32_t opline_num; uint32_t lineno; uint32_t num_args; uint32_t fe_pos; uint32_t fe_iter_idx; uint32_t guard; uint32_t constant_flags; uint32_t extra; } u2; }; 実際の値を保存する共用体 68

Slide 69

Slide 69 text

zval structure struct _zval_struct { zend_value value; union { uint32_t type_info; struct { ZEND_ENDIAN_LOHI_3( uint8_t type, uint8_t type_flags, union { uint16_t extra; } u) } v; } u1; union { uint32_t next; uint32_t cache_slot; uint32_t opline_num; uint32_t lineno; uint32_t num_args; uint32_t fe_pos; uint32_t fe_iter_idx; uint32_t guard; uint32_t constant_flags; uint32_t extra; } u2; }; 情報やフラグを管理する領域 69

Slide 70

Slide 70 text

zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; }; 70

Slide 71

Slide 71 text

zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; }; 文字列のハッシュ値 71

Slide 72

Slide 72 text

zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; }; 文字列の長さ(\0を含めない) 72

Slide 73

Slide 73 text

zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; }; 文字列(終端に\0を含めている) 73

Slide 74

Slide 74 text

Packed Array 74

Slide 75

Slide 75 text

Packed Array(PHPコード) $phpcon2024 = [ 'phpcon-hokkaido-2024', 'phpcon-kansai2024', 'phperkaigi-2024', 'phpconodawara-2024', 'phpconkagawa-2024', ]; ● キーが0からの昇順の整数となっている配列 ● キーがそのまま配列の添字となる 75

Slide 76

Slide 76 text

Packed Array(PHPコード) $phpcon2024 = [ 0 => 'phpcon-hokkaido-2024', 1 => 'phpcon-kansai2024', 2 => 'phperkaigi-2024', 3 => 'phpconodawara-2024', 4 => 'phpconkagawa-2024', ]; ● キーが0からの昇順の整数となっている配列 ● キーがそのまま配列の添字となる 76

Slide 77

Slide 77 text

Packed Arrayの全体図 77

Slide 78

Slide 78 text

Packed Arrayの全体図 キーとデータの配列の添字 を紐付けるハッシュ表は 使われていない 78

Slide 79

Slide 79 text

Packed Arrayの全体図 zval(構造体)型の配列 79

Slide 80

Slide 80 text

ここまでのまとめ 80

Slide 81

Slide 81 text

PHPの配列が内部的に持つ2種類の配列 ● ht->arData でアクセスする Bucket 構造体の配列 ● キーからハッシュ表を用いて、データの 配列の添字としてアクセス 通常のHashTable Packed Array ● ht->arPacked でアクセスする zval 構造体の配列 ● キーをそのままデータの配列の添字とし てアクセス 81

Slide 82

Slide 82 text

アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 82

Slide 83

Slide 83 text

コードのどこを読めばいいか? 83

Slide 84

Slide 84 text

配列の基本的な操作 ● 要素の探索、追加、更新は基本的に同じ関数を 呼び出している。 ○ 引数にフラグを使って条件分岐 ● キーが文字列か数値かで変わる ○ _zend_hash_str_add_or_update_i(...) ○ _zend_hash_index_add_or_update_i(...) ● 削除は _zend_hash_packed_del_val(...) _zend_hash_del_el_ex(...) 84

Slide 85

Slide 85 text

実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 85

Slide 86

Slide 86 text

実際の挙動を確認する(実行する操作) 1. 配列の初期化 2. 要素の追加 3. 要素の削除 4. 要素の追加(Packed Arrayを通常のHashTableへ) 86

Slide 87

Slide 87 text

実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 87

Slide 88

Slide 88 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; 88

Slide 89

Slide 89 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; 89

Slide 90

Slide 90 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; HashTableの生成、Packed Arrayの初期化、要素の追加、 の3つを行っている 90

Slide 91

Slide 91 text

HashTableの生成 ZEND_API HashTable* ZEND_FASTCALL _zend_new_array( uint32_t nSize) { HashTable *ht = emalloc(sizeof(HashTable)); _zend_hash_init_int(ht, nSize, ZVAL_PTR_DTOR, 0); return ht; } 91

Slide 92

Slide 92 text

HashTableの生成 ZEND_API HashTable* ZEND_FASTCALL _zend_new_array( uint32_t nSize) { HashTable *ht = emalloc(sizeof(HashTable)); _zend_hash_init_int(ht, nSize, ZVAL_PTR_DTOR, 0); return ht; } 92

Slide 93

Slide 93 text

HashTableの生成 static zend_always_inline void _zend_hash_init_int(HashTable *ht, uint32_t nSize, dtor_func_t pDestructor, bool persistent) { GC_SET_REFCOUNT(ht, 1); GC_TYPE_INFO(ht) = GC_ARRAY | (persistent ? ((GC_PERSISTENT|GC_NOT_COLLECTABLE) << GC_FLAGS_SHIFT) : 0); HT_FLAGS(ht) = HASH_FLAG_UNINITIALIZED; ht->nTableMask = HT_MIN_MASK; HT_SET_DATA_ADDR(ht, &uninitialized_bucket); ht->nNumUsed = 0; ht->nNumOfElements = 0; ht->nInternalPointer = 0; ht->nNextFreeElement = ZEND_LONG_MIN; ht->pDestructor = pDestructor; ht->nTableSize = zend_hash_check_size(nSize); } 93

Slide 94

Slide 94 text

HashTableの生成 94

Slide 95

Slide 95 text

Packed Array の初期化 static zend_always_inline void zend_hash_real_init_packed_ex(HashTable *ht) { void *data; if (UNEXPECTED(GC_FLAGS(ht) & IS_ARRAY_PERSISTENT)) { data = pemalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK), 1); } else if (EXPECTED(ht->nTableSize == HT_MIN_SIZE)) { data = emalloc(HT_PACKED_SIZE_EX(HT_MIN_SIZE, HT_MIN_MASK)); } else { data = emalloc(HT_PACKED_SIZE_EX(ht->nTableSize, HT_MIN_MASK)); } HT_SET_DATA_ADDR(ht, data); ht->u.v.flags = HASH_FLAG_PACKED | HASH_FLAG_STATIC_KEYS; HT_HASH_RESET_PACKED(ht); } 95

Slide 96

Slide 96 text

Packed Array の初期化 data = emalloc(HT_PACKED_SIZE_EX( HT_MIN_SIZE, HT_MIN_MASK)); 96

Slide 97

Slide 97 text

Packed Array の初期化 HT_SET_DATA_ADDR(ht, data); 97

Slide 98

Slide 98 text

Packed Array の初期化 ht->u.v.flags = HASH_FLAG_PACKED | HASH_FLAG_STATIC_KEYS; 98

Slide 99

Slide 99 text

Packed Array の初期化 HT_HASH_RESET_PACKED(ht); 99

Slide 100

Slide 100 text

要素の追加 static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht, zend_ulong h, zval *pData, uint32_t flag) { ... if (HT_IS_PACKED(ht)) { ... } else if (EXPECTED(h < ht->nTableSize)) { add_to_packed: zv = ht->arPacked + h; if ((flag & (HASH_ADD_NEW|HASH_ADD_NEXT)) != (HASH_ADD_NEW|HASH_ADD_NEXT)) { if (h > ht->nNumUsed) { zval *q = ht->arPacked + ht->nNumUsed; while (q != zv) { ZVAL_UNDEF(q); q++; } } } ht->nNextFreeElement = ht->nNumUsed = h + 1; ht->nNumOfElements++; if (flag & HASH_LOOKUP) { ZVAL_NULL(zv); } else { ZVAL_COPY_VALUE(zv, pData); } return zv; } else if ... } 100

Slide 101

Slide 101 text

要素の追加 ht->nNextFreeElement = ht-> nNumUsed = h + 1; ht->nNumOfElements++; if (flag & HASH_LOOKUP) { ZVAL_NULL(zv); } else { ZVAL_COPY_VALUE(zv, pData); } zv = ht->arPacked + h; 101

Slide 102

Slide 102 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; 102

Slide 103

Slide 103 text

要素の追加(2回目) 先程同様に _zend_hash_index_add_or_u pdate_i の add_to_packed ラベルの部分を実行 103

Slide 104

Slide 104 text

要素の追加(2回目) ht->nNextFreeElement = ht-> nNumUsed = h + 1; ht->nNumOfElements++; if (flag & HASH_LOOKUP) { ZVAL_NULL(zv); } else { ZVAL_COPY_VALUE(zv, pData); } zv = ht->arPacked + h; 104

Slide 105

Slide 105 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; 105

Slide 106

Slide 106 text

要素の削除 ZEND_API zend_result ZEND_FASTCALL zend_hash_index_del(HashTable *ht, zend_ulong h) { ... if (HT_IS_PACKED(ht)) { if (h < ht->nNumUsed) { zval *zv = ht->arPacked + h; if (Z_TYPE_P(zv) != IS_UNDEF) { _zend_hash_packed_del_val(ht, HT_IDX_TO_HASH(h), zv); return SUCCESS; } } return FAILURE; } ... } 106

Slide 107

Slide 107 text

要素の削除 ZEND_API zend_result ZEND_FASTCALL zend_hash_index_del(HashTable *ht, zend_ulong h) { ... if (HT_IS_PACKED(ht)) { if (h < ht->nNumUsed) { zval *zv = ht->arPacked + h; if (Z_TYPE_P(zv) != IS_UNDEF) { _zend_hash_packed_del_val(ht, HT_IDX_TO_HASH(h), zv); return SUCCESS; } } return FAILURE; } ... } 107

Slide 108

Slide 108 text

要素の削除 static zend_always_inline void _zend_hash_packed_del_val(HashTable *ht, uint32_t idx, zval *zv) { idx = HT_HASH_TO_IDX(idx); ht->nNumOfElements--; ... if (ht->pDestructor) { zval tmp; ZVAL_COPY_VALUE(&tmp, zv); ZVAL_UNDEF(zv); ht->pDestructor(&tmp); } else { ZVAL_UNDEF(zv); } } 108

Slide 109

Slide 109 text

要素の削除 ht->nNumOfElements--; ... if (ht->pDestructor) { zval tmp; ZVAL_COPY_VALUE(&tmp, zv); ZVAL_UNDEF(zv); ht->pDestructor(&tmp); } else { ZVAL_UNDEF(zv); } zv = ht->arPacked + h; は UNDEF に置き換えられる 109

Slide 110

Slide 110 text

Packed Array(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024[] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024[0]); // 4. 要素の追加(Packed Arrayを通常のHashTableへ) $phpcon2024['March'] = 'phperkaigi-2024'; 110

Slide 111

Slide 111 text

キーと値のペアの要素の追加 static zend_always_inline zval *_zend_hash_str_add_or_update_i(HashTable *ht, const char *str, size_t len, zend_ulong h, zval *pData, uint32_t flag) { ... if (UNEXPECTED(HT_FLAGS(ht) & (HASH_FLAG_UNINITIALIZED|HASH_FLAG_PACKED))) { if (EXPECTED(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) { zend_hash_real_init_mixed(ht); goto add_to_hash; } else { zend_hash_packed_to_hash(ht); } } else if ... } 111

Slide 112

Slide 112 text

キーと値のペアの要素の追加 static zend_always_inline zval *_zend_hash_str_add_or_update_i(HashTable *ht, const char *str, size_t len, zend_ulong h, zval *pData, uint32_t flag) { ... if (UNEXPECTED(HT_FLAGS(ht) & (HASH_FLAG_UNINITIALIZED|HASH_FLAG_PACKED))) { if (EXPECTED(HT_FLAGS(ht) & HASH_FLAG_UNINITIALIZED)) { zend_hash_real_init_mixed(ht); goto add_to_hash; } else { zend_hash_packed_to_hash(ht); } } else if ... } これより先は図で説明 112

Slide 113

Slide 113 text

キーと値のペアの要素の追加 HT_FLAGS(ht) &= ~HASH_FLAG_PACKED; 113

Slide 114

Slide 114 text

キーと値のペアの要素の追加 arPacked のサイズをもとに、 Bucket 構造体の配列(とハッシュ 表)を作る 114

Slide 115

Slide 115 text

キーと値のペアの要素の追加 zval をそれぞれ Bucket にコピーし て、 arPacked の zval の配列を開 放する。 115

Slide 116

Slide 116 text

キーと値のペアの要素の追加 UNDEF があれば、前に詰めるよう に arData 上の Bucket をコピーす る。 116

Slide 117

Slide 117 text

キーと値のペアの要素の追加 ハッシュ表にデータ配列への添字を 入れる。 nNumUsed を更新する。 117

Slide 118

Slide 118 text

キーと値のペアの要素の追加 追記したいキーと値のペアを Bucket に追加 118

Slide 119

Slide 119 text

キーと値のペアの要素の追加 追加した Bucket のキーのハッシュ値 を nTableMask でマスクしたものを追 加。 nNumUsed、nNumOfElements もイン クリメント。 119

Slide 120

Slide 120 text

ここまでのまとめ 120

Slide 121

Slide 121 text

● 探索:ht->arPacked + h ● 追加:ht->arPacked + h に与えられた zval をコピー ● 削除:ht->arPacked + h を UNDEF に置き換える ● キーと値のペアで追加:現在の Packed Array の要素が Bucket の配列にコピーされ、通常の HashTable へ変換 される。 Packed Array の操作 121

Slide 122

Slide 122 text

視聴者の皆さん「(理解する)」 122

Slide 123

Slide 123 text

実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 123

Slide 124

Slide 124 text

通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 124

Slide 125

Slide 125 text

通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 125

Slide 126

Slide 126 text

HashTableの生成&初期化 126

Slide 127

Slide 127 text

最初の要素の追加 追記したいキーと値のペアを Bucket に追加 127

Slide 128

Slide 128 text

通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 128

Slide 129

Slide 129 text

2つ目の要素の追加 add_to_hash: idx = ht->nNumUsed++; ht->nNumOfElements++; p = ht->arData + idx; p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); p->h = ZSTR_H(key) = h; HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS; if (flag & HASH_LOOKUP) { ZVAL_NULL(&p->val); } else { ZVAL_COPY_VALUE(&p->val, pData); } nIndex = h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx); 129

Slide 130

Slide 130 text

2つ目の要素の追加 とりあえず Bucket を追加 130

Slide 131

Slide 131 text

2つ目の要素の追加 とりあえず Bucket を追加 → ハッシュ表上のハッシュ値   がコンフリクトした どちらもビットマスク すると-1になる 131

Slide 132

Slide 132 text

2つ目の要素の追加 add_to_hash: idx = ht->nNumUsed++; ht->nNumOfElements++; p = ht->arData + idx; p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); p->h = ZSTR_H(key) = h; HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS; if (flag & HASH_LOOKUP) { ZVAL_NULL(&p->val); } else { ZVAL_COPY_VALUE(&p->val, pData); } nIndex = h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx); zval.u2.next にハッシュ表上 の添字を保存する 132

Slide 133

Slide 133 text

2つ目の要素の追加 1番目の Bucket から0番目の Bucket へチェインする 133

Slide 134

Slide 134 text

2つ目の要素の追加 add_to_hash: idx = ht->nNumUsed++; ht->nNumOfElements++; p = ht->arData + idx; p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT); p->h = ZSTR_H(key) = h; HT_FLAGS(ht) &= ~HASH_FLAG_STATIC_KEYS; if (flag & HASH_LOOKUP) { ZVAL_NULL(&p->val); } else { ZVAL_COPY_VALUE(&p->val, pData); } nIndex = h | ht->nTableMask; Z_NEXT(p->val) = HT_HASH(ht, nIndex); HT_HASH(ht, nIndex) = HT_IDX_TO_HASH(idx); ハッシュ表上の添字を更新する 134

Slide 135

Slide 135 text

2つ目の要素の追加 ハッシュ表の-1番目に保存されてい た添字が更新される 135

Slide 136

Slide 136 text

通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', ]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 136

Slide 137

Slide 137 text

要素の削除 ZEND_API zend_result ZEND_FASTCALL zend_hash_del(HashTable *ht, zend_string *key) { ... h = zend_string_hash_val(key); nIndex = h | ht->nTableMask; idx = HT_HASH(ht, nIndex); while (idx != HT_INVALID_IDX) { p = HT_HASH_TO_BUCKET(ht, idx); if ((p->key == key) || (p->h == h && p->key && zend_string_equal_content(p->key, key))) { zend_string_release(p->key); p->key = NULL; _zend_hash_del_el_ex(ht, idx, p, prev); return SUCCESS; } prev = p; idx = Z_NEXT(p->val); ... 137

Slide 138

Slide 138 text

要素の削除 ZEND_API zend_result ZEND_FASTCALL zend_hash_del(HashTable *ht, zend_string *key) { ... h = zend_string_hash_val(key); nIndex = h | ht->nTableMask; idx = HT_HASH(ht, nIndex); while (idx != HT_INVALID_IDX) { p = HT_HASH_TO_BUCKET(ht, idx); if ((p->key == key) || (p->h == h && p->key && zend_string_equal_content(p->key, key))) { zend_string_release(p->key); p->key = NULL; _zend_hash_del_el_ex(ht, idx, p, prev); return SUCCESS; } prev = p; idx = Z_NEXT(p->val); ... コンフリクトした ハッシュ値から、 目的の要素を探索 138

Slide 139

Slide 139 text

要素の削除 ZEND_API zend_result ZEND_FASTCALL zend_hash_del(HashTable *ht, zend_string *key) { ... h = zend_string_hash_val(key); nIndex = h | ht->nTableMask; idx = HT_HASH(ht, nIndex); while (idx != HT_INVALID_IDX) { p = HT_HASH_TO_BUCKET(ht, idx); if ((p->key == key) || (p->h == h && p->key && zend_string_equal_content(p->key, key))) { zend_string_release(p->key); p->key = NULL; _zend_hash_del_el_ex(ht, idx, p, prev); return SUCCESS; } prev = p; idx = Z_NEXT(p->val); ... 139

Slide 140

Slide 140 text

要素の削除 static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx, Bucket *p, Bucket *prev) { if (prev) { Z_NEXT(prev->val) = Z_NEXT(p->val); } else { HT_HASH(ht, p->h | ht->nTableMask) = Z_NEXT(p->val); } idx = HT_HASH_TO_IDX(idx); ht->nNumOfElements--; ... if (ht->pDestructor) { ... } else { ZVAL_UNDEF(&p->val); } } 140

Slide 141

Slide 141 text

要素の削除 static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx, Bucket *p, Bucket *prev) { if (prev) { Z_NEXT(prev->val) = Z_NEXT(p->val); } else { HT_HASH(ht, p->h | ht->nTableMask) = Z_NEXT(p->val); } idx = HT_HASH_TO_IDX(idx); ht->nNumOfElements--; ... if (ht->pDestructor) { ... } else { ZVAL_UNDEF(&p->val); } } 141

Slide 142

Slide 142 text

要素の削除 1番目の Bucket からのチェイン がなくなる。 142

Slide 143

Slide 143 text

要素の削除 static zend_always_inline void _zend_hash_del_el_ex(HashTable *ht, uint32_t idx, Bucket *p, Bucket *prev) { if (prev) { Z_NEXT(prev->val) = Z_NEXT(p->val); } else { HT_HASH(ht, p->h | ht->nTableMask) = Z_NEXT(p->val); } idx = HT_HASH_TO_IDX(idx); ht->nNumOfElements--; ... if (ht->pDestructor) { ... } else { ZVAL_UNDEF(&p->val); } } 143

Slide 144

Slide 144 text

要素の削除 0番目の Bucket がUNDEFになる nNumOfElements を更新 144

Slide 145

Slide 145 text

ここまでのまとめ 145

Slide 146

Slide 146 text

● 探索:h (ハッシュ値 or 数値) からビットマスクしてハッ シュ表に保存されている添字を取り出し、Bucket 配列に アクセスする。衝突している場合は Z_NEXT で次の要素 にチェインする。 ● 追加:Bucket 配列に要素を追加し、ハッシュ表上で衝突 する場合は Z_NEXT で1つ前の Bucket を設定する ● 削除:指定された Bucket を UNDEF に置き換える。 Z_NEXT で指定されていた場合は、それをなくす。 通常の HashTable の操作 146

Slide 147

Slide 147 text

アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 147

Slide 148

Slide 148 text

まとめ ● PHPの配列の内部データの配列は2種類ある ○ Packed Array と、それ以外の通常の HashTable ○ zval の配列か、Bucket の配列かの違い ● 配列の操作は、2つの配列で実装が異なる 148

Slide 149

Slide 149 text

● 探索:ht->arPacked + h ● 追加:ht->arPacked + h に与えられた zval をコピー ● 削除:ht->arPacked + h を UNDEF に置き換える ● キーと値のペアで追加:現在の Packed Array の要素が Bucket の配列にコピーされ、通常の HashTable へ変換 される。 (再掲)Packed Array の操作 149

Slide 150

Slide 150 text

● 探索:h (ハッシュ値 or 数値) からビットマスクしてハッ シュ表に保存されている添字を取り出し、Bucket 配列に アクセスする。衝突している場合は Z_NEXT で次の要素 にチェインする。 ● 追加:Bucket 配列に要素を追加し、ハッシュ表上で衝突 する場合は Z_NEXT で1つ前の Bucket を設定する ● 削除:指定された Bucket を UNDEF に置き換える。 Z_NEXT で指定されていた場合は、それをなくす。 (再掲)通常の HashTable の操作 150

Slide 151

Slide 151 text

エンジニア採用サイト エンジニア採用資料 開発者ブログ  PR TIMES エンジニア 採用情報 検索 PR TIMESでは一緒に働く仲間を募集中! We Are Hiring! 151

Slide 152

Slide 152 text

Appendix 152

Slide 153

Slide 153 text

● 配列の構造体やマクロ ○ zend_type.h ● 配列の操作 ○ zend_hash.h ○ zend_hash.c 実際、どのファイルを読めばいいの? 153

Slide 154

Slide 154 text

● PHP スクリプト実行の流れを知る ○ PHP による hello world 入門 by @sji_ch ■ https://tech.respect-pal.jp/php-helloworld/ ○ php-srcを読んでみよう by @tzm_freedom ■ https://fortee.jp/phpcon-2022/proposal/1addf51 d-6f72-4c96-9337-034ec6cc0643 ● php-srcをデバッグビルド、GDBでスタックトレース しながらデバッグ。 PHP スクリプトから配列が呼ばれるまでの流れを知りたい 154

Slide 155

Slide 155 text

● おそらくメモリの節約と CPU キャッシュ戦略 ● メモリの節約 ○ arHash を別の配列として用意すると、それを参照す るポインタを別途用意する必要がある。 なので、その分 64 (32) bits の節約ができる。 ● CPU キャッシュ戦略 ○ arData と arHash がメモリ上の近い場所にあること で、空間的局所性を利用しキャッシュに乗りやすく なる(と思われる) 何故、ハッシュ表はマイナスでアクセスするの? 155

Slide 156

Slide 156 text

156 備考: なぜPHPの配列の内部構造で使われるハッシュテーブルは、OR演算子でビットマスク を計算しているのか? https://zenn.dev/meihei/articles/e5aa05a0e6c82e ビットマスクはANDではなくORなの?

Slide 157

Slide 157 text

● 変数をメモリに配置する際、CPUの環境によってバ イト列の並べ方が異なる ○ これを「バイトオーダー」と呼ぶ ○ 上位バイトから順に配置されるビッグエンディアン ○ 上位バイトから逆順に配置されるリトルエンディアン ● uint32_tとuint8_tが4つの構造体を共用体とすると き、uint32_tのバイトオーダーによって構造体から のアクセスが意図せぬものになることがある ○ それを防ぐためにバイトオーダーによって構造体の定 義の順番を変えている(文マクロ) ZEND_ENDIAN_LOHI_~ は何をしているの? 157

Slide 158

Slide 158 text

● List とはキーが0からの連番になった配列 ● Packed Array は HASH_FLAG_PACKED が有効な配 列 ○ 一度 zend_hash_packed_to_hash が呼ばれると基本 的には HT_IS_PACKED は FALSE になる ● 条件によっては List であってもハッシュ表を用いた Bucket の配列であることもある Packed Array ≠ List 158

Slide 159

Slide 159 text

Packed Array ≠ List $phpcon2024 = [ 'phpcon-hokkaido-2024', 'phpcon-kansai2024', 4 => 'phpconkagawa-2024', ]; // bool(false) var_dump(array_is_list( $phpcon2024)); $phpcon2024 = [ 'phpcon-hokkaido-2024', 'phpcon-kansai2024', ]; unset($phpcon2024[1]); $phpcon2024[1] = 'phpcon-kansai2024'; // bool(true) var_dump(array_is_list( $phpcon2024)); Packed Array だが List ではない List だが Packed Array ではない 159

Slide 160

Slide 160 text

参考 1. 本物のC http://real-c.info/index.html 2. Go and CPU Caches https://teivah.medium.com/go-and-cpu-caches-af5d32cc5592 3. PHP による hello world 入門 https://tech.respect-pal.jp/php-helloworld/ 4. PHP7で変わること ——言語仕様とエンジンの改善ポイント https://www.slideshare.net/hnw/phpcon-kansai20150530 5. PHPとPythonとRubyの連想配列のデータ構造が同時期に同じ方針で性能改善されてた話 https://hnw.hatenablog.com/entry/2021/01/10/162018 6. [php-src読書録]その6: array https://blog.freedom-man.com/php_src_6 7. php内核源码分析之HashTable https://fantiq.github.io/2019/05/26/php%E5%86%85%E6%A0%B8%E6%BA%90%E7 %A0%81%E5%88%86%E6%9E%90%E4%B9%8BHashTable/ 8. PHPはどのように動くのか ~PHPコアから読み解く仕組みと定石, 技術評論 社,https://gihyo.jp/book/2015/978-4-7741-7642-0 160