Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHP8.2から見る、2つの配列 / PHP Conference Japan 2023
Search
meihei
October 08, 2023
Programming
2.6k
0
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PHP8.2から見る、2つの配列 / PHP Conference Japan 2023
see
https://fortee.jp/phpcon-2023/proposal/78a58810-29e4-463f-b1e2-2cb6808e4e81
meihei
October 08, 2023
More Decks by meihei
See All by meihei
隙間ツール開発のすすめ / PHP Conference Fukuoka 2025
meihei3
0
1.2k
QRコードを学んで遊ぼう / php-study-177
meihei3
0
220
改めて学ぶ Trait の使い方 / phpcon odawara 2025
meihei3
1
2k
List とは何か? / PHPerKaigi 2025
meihei3
0
1.7k
WebアプリケーションにおけるPDOの使い方入門 / phpcon odawara 2024
meihei3
3
2.6k
PHPerライフをChrome拡張開発でちょっと便利に / PR TIMES x DMM.com
meihei3
0
480
ファイルを選択してZIPダウンロードする機能ってどうやって作るの? / phpcondo 2023
meihei3
1
920
New Relic CodeStreamを 使って、エラーを 加速的迅速に改修しよう! #NRUG Vol.8
meihei3
0
450
良いコードを書く 〜10年後のPR TIMESを作る〜 / LT会 in #PRTIMES_HACKATHON 2023
meihei3
2
290
Other Decks in Programming
See All in Programming
AutonomyとControlのあいだ:Graflowで記述するAIエージェント協調
myui
0
120
AI時代のUIはどこへ行く?その2!
yusukebe
21
7.1k
Inside Stream API
skrb
1
700
気圧・高度・GPSを記録&可視化するアプリ「Koudo」を作った話
hjmkth
1
210
ローカルLLMでどこまでコードが書けるか -拡張版 / How much code can be written on a local LLM Extended
kishida
10
4k
AI 時代のソフトウェア設計の学び方
masuda220
PRO
29
12k
IBM Bobを活用したレガシーアプリの最新化
oniak3ibm
PRO
1
190
代数的データ型って何が嬉しいの? #frontend_phpcon_do
kajitack
8
3.6k
Oxcを導入して開発体験が向上した話
yug1224
4
310
Developing with AI Agents — Codex, Claude Code & Cowork Practical Guide
x5gtrn
PRO
0
1.3k
Technical Debt: Understanding it Rightly, Engaging it Rightly #LaravelLiveJP
shogogg
0
220
AI時代の仕事技芸論 — ソフトウェア開発で「遊ぶように働く」職人的熟達のすすめ
kuranuki
2
670
Featured
See All Featured
The AI Revolution Will Not Be Monopolized: How open-source beats economies of scale, even for LLMs
inesmontani
PRO
3
3.5k
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
65
55k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
287
14k
Building a Modern Day E-commerce SEO Strategy
aleyda
45
9.1k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
310
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
720
Designing for humans not robots
tammielis
254
26k
Building the Perfect Custom Keyboard
takai
2
790
How STYLIGHT went responsive
nonsquared
100
6.2k
Done Done
chrislema
186
16k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Bioeconomy Workshop: Dr. Julius Ecuru, Opportunities for a Bioeconomy in West Africa
akademiya2063
PRO
1
140
Transcript
PHP8.2から見る、2つの配列 PHP Conference Japan 2023
meihei / 江間 洋平 株式会社PR TIMES Backend Engineer (PHP/Python/Go) X:
@app1e_s GitHub: @meihei3 Bluesky: @meihei.bsky.social #BLEACH #VTuber(どラ、ぶいぱい、個人) #クラロワ #モンハンNOW #秋競馬 etc. 自己紹介 2
最初にお断り • プロポーザルに”話さないこと”として「探索以外の HashTableの基本操作(追加・更新・削除)」と書い ていましたが、ちゃんと理解するために「追加・削 除」の説明も話します。 • そのため、少しペースを早めて話します。 • また、このスライドは公開済みです。文字が読めな
いところは、是非お手元のPCでも開いて貰えればと 思います。 3
本日のゴール • PHPの配列の操作の、内部挙動を理解する • php-srcちょっと読める気分になる 4
アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 5
アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 6
前準備 • PHPの配列のおさらい • C言語の知識 • 道具の準備 7
前準備 • PHPの配列のおさらい • C言語の知識 • 道具の準備 8
PHP の配列は、実際には 順番付けられたマップです。 9
PHP の配列は、実際には 順番付けられたマップです。 10
PHPの配列 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024',
'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; 11
順番付けられた $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024',
'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; • 追加された順番を担保している ① ② ③ ④ ⑤ next next next next 12
マップ $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024',
'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; • キーによって値が取り出せるデータ構造 • 連想配列・辞書などとも呼ばれている キーと値がペアになってる 13
前準備 • PHPの配列のおさらい • C言語の知識 • 道具の準備 14
php-srcは結構難読 15
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
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
? 18
C言語で最低限必要な 知識を身につけよう 19
C言語の配列 配列とは、同一の型のデータを メモリ上に一列に並べたもの。 (例) int arr[] = {1, 2, 3};
であれば、 12 bytes の領域に 4 bytes の int 型 のデータが 3 つ並べられる。 20
ポインタ アドレスを記憶する変数のこと ポインタに*をつける事で、アド レスが示す値にアクセスできる (例) int x = 0x20241008; int
*p = &x; であれば、p は int へのポインタ型とな り、 p には x のアドレスが保存されてい る。 21
配列とポインタ 配列はその先頭要素への ポインタに読み替えられる • &arr[0] と arr は同義 • arr[0]
と *arr は同義 • arr[1] と *(arr+1) は同義 22
ポインタの操作 ポインタ演算は整数演算とは異なり、型のサイズ分アド レスを移動する。 (例) int *x; と char *s; のポインタ操作の比較
sizeof(int) = 4 bytes sizeof(char) = 1 byte 23
マクロ コンパイル時にソースコードの一部として展開される、 事前に定義されたシンボルやコードの断片。 • #define ディレクティブを用いて定義 • 定数マクロ:値をマクロ名として定義するマクロ • 関数マクロ:引数を取ることができるマクロ
• 文マクロ:C言語の文や式から成るマクロ 24
条件付きコンパイル 条件に基づきコードをコンパイルするか、除外するかを 決定することができる。 • #if と #endif ディレクティブを用いる • #elif
や #else で複数の条件にも対応できる 25
構造体(structure) 異なるデータ型の変数を1つのグループとしてまとめる ためのデータ構造。 (例) 2 つの型から成る構造体 struct example { int32_t
i; int *ptr; } 26
共用体(union) 複数の異なるデータ型を1つのメモリ領域に格納するた めのデータ構造。 (例) 3 つの型から成る共用体 union example { int32_t
i; int *ptr; char str[8]; } 27
視聴者の皆さん「(C言語を完全に理解する)」 28
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
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
• PHPは魔法ではない • 一つ一つ見ていけばコンピューターサイエンス の知識で説明可能 (脱線)PHPの配列もメモリ上に並ぶ $phpcon2024 = [ 'January'
=> 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024', 'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; ※この配列はイメージです。実際は4bytesではありません。 31
前準備 • PHPの配列のおさらい • C言語の知識 • 道具の準備 32
コードを読むだけでは理解が難しい 33
コードを読むだけでは理解が難しい デバッグをして挙動を確認する 34
• GDB を使ったデバッグ ◦ 王道だけど、個人的には難しい。 • PHP Extension によるプリントデバッグ ◦
PHP Extension から直接変数へアクセスする php-src のデバッグ 35
• 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
アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 37
配列の全体図 38
配列の全体図 配列本体であり 配列の情報を 管理する構造体 39
配列の全体図 キーとデータの配列の添字 を紐付けるハッシュ表 配列本体であり 配列の情報を 管理する構造体 40
配列の全体図 キーとデータの配列の添字 を紐付けるハッシュ表 データの配列 配列本体であり 配列の情報を 管理する構造体 41
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
HashTable structure • PHPの配列型(IS_ARRAY) を成す構造体 • 「順序付けられたマップ」を 提供する • sizeof:
56 bytes 43
リファレンスカウントとGCを 管理する構造体 HashTable structure 44
配列の状態を管理するフラグ HashTable structure 45
ハッシュ表用のビットマスク HashTable structure 46
実際のデータの配列(の先頭要 素)へのポインタ HashTable structure 47
実際に使用されているエントリ の数 (要素を削除しても減らない) HashTable structure 48
配列に含まれる要素の数 (要素を削除すると減る) HashTable structure 49
データの配列のサイズ(長さ) HashTable structure 50
データの配列の添字 (current(), next() などで使わ れるもの) HashTable structure 51
自動的に割り当てられる次の 数値キーの値 (文字列キーが追加されても変 わらず、数値キーが追加される と+1される) HashTable structure 52
配列の要素を破棄するためのデ ストラクタへのポインタ HashTable structure 53
データの配列へのポインタは • Bucket の配列: arData • zval の配列: arPacked を選択できる共用体となってい
る。 (タイトル回収)2つの配列 Use more compact representation for packed arrays. #7491 54
2つの配列は共用体で切り替える Packed Array ht->arPacked でアクセス 通常のHashTable ht->arData でアクセス 55
通常のHashTable 56
通常のHashTable(PHPコード) $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024', 'February' => 'phpcon-kansai2024',
'March' => 'phperkaigi-2024', 'April' => 'phpconodawara-2024', 'May' => 'phpconkagawa-2024', ]; • キーを指定して、値を取り出す • Packed Arrayではないものは全てこれ 57
通常のHashTableの全体図 58
通常のHashTableの全体図 キーとデータの配列の添字 を紐付けるハッシュ表 59
通常のHashTableの全体図 Bucket(構造体)型の配列 60
通常のHashTableの全体図 キーの文字列(の構造体) 61
Bucket structure typedef struct _Bucket { zval val; zend_ulong h;
zend_string *key; } Bucket; 62
• 配列の要素を表す構造体 • 値とキー(文字列・数値)が ペアになっている Bucket structure 63
• 配列の値の部分 • zval 型 • sizeof: 16 bytes (zval構造体については後述)
Bucket structure 64
• 文字列キーのハッシュ値 • 数値キーの値 • zend_ulong 型 • sizeof: 8
bytes Bucket structure 65
• 文字列キーへのポインタ ◦ 数値キーの場合はNULL • zend_string 型 • sizeof: 8
bytes (zend_string構造体は後述) Bucket structure 66
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
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
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
zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t
len; char val[1]; }; 70
zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t
len; char val[1]; }; 文字列のハッシュ値 71
zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t
len; char val[1]; }; 文字列の長さ(\0を含めない) 72
zend_string structure struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t
len; char val[1]; }; 文字列(終端に\0を含めている) 73
Packed Array 74
Packed Array(PHPコード) $phpcon2024 = [ 'phpcon-hokkaido-2024', 'phpcon-kansai2024', 'phperkaigi-2024', 'phpconodawara-2024', 'phpconkagawa-2024',
]; • キーが0からの昇順の整数となっている配列 • キーがそのまま配列の添字となる 75
Packed Array(PHPコード) $phpcon2024 = [ 0 => 'phpcon-hokkaido-2024', 1 =>
'phpcon-kansai2024', 2 => 'phperkaigi-2024', 3 => 'phpconodawara-2024', 4 => 'phpconkagawa-2024', ]; • キーが0からの昇順の整数となっている配列 • キーがそのまま配列の添字となる 76
Packed Arrayの全体図 77
Packed Arrayの全体図 キーとデータの配列の添字 を紐付けるハッシュ表は 使われていない 78
Packed Arrayの全体図 zval(構造体)型の配列 79
ここまでのまとめ 80
PHPの配列が内部的に持つ2種類の配列 • ht->arData でアクセスする Bucket 構造体の配列 • キーからハッシュ表を用いて、データの 配列の添字としてアクセス 通常のHashTable
Packed Array • ht->arPacked でアクセスする zval 構造体の配列 • キーをそのままデータの配列の添字とし てアクセス 81
アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 82
コードのどこを読めばいいか? 83
配列の基本的な操作 • 要素の探索、追加、更新は基本的に同じ関数を 呼び出している。 ◦ 引数にフラグを使って条件分岐 • キーが文字列か数値かで変わる ◦ _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
実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 85
実際の挙動を確認する(実行する操作) 1. 配列の初期化 2. 要素の追加 3. 要素の削除 4. 要素の追加(Packed Arrayを通常のHashTableへ)
86
実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 87
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
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
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
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
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
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
HashTableの生成 94
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
Packed Array の初期化 data = emalloc(HT_PACKED_SIZE_EX( HT_MIN_SIZE, HT_MIN_MASK)); 96
Packed Array の初期化 HT_SET_DATA_ADDR(ht, data); 97
Packed Array の初期化 ht->u.v.flags = HASH_FLAG_PACKED | HASH_FLAG_STATIC_KEYS; 98
Packed Array の初期化 HT_HASH_RESET_PACKED(ht); 99
要素の追加 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
要素の追加 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
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
要素の追加(2回目) 先程同様に _zend_hash_index_add_or_u pdate_i の add_to_packed ラベルの部分を実行 103
要素の追加(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
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
要素の削除 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
要素の削除 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
要素の削除 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
要素の削除 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
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
キーと値のペアの要素の追加 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
キーと値のペアの要素の追加 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
キーと値のペアの要素の追加 HT_FLAGS(ht) &= ~HASH_FLAG_PACKED; 113
キーと値のペアの要素の追加 arPacked のサイズをもとに、 Bucket 構造体の配列(とハッシュ 表)を作る 114
キーと値のペアの要素の追加 zval をそれぞれ Bucket にコピーし て、 arPacked の zval の配列を開
放する。 115
キーと値のペアの要素の追加 UNDEF があれば、前に詰めるよう に arData 上の Bucket をコピーす る。 116
キーと値のペアの要素の追加 ハッシュ表にデータ配列への添字を 入れる。 nNumUsed を更新する。 117
キーと値のペアの要素の追加 追記したいキーと値のペアを Bucket に追加 118
キーと値のペアの要素の追加 追加した Bucket のキーのハッシュ値 を nTableMask でマスクしたものを追 加。 nNumUsed、nNumOfElements もイン
クリメント。 119
ここまでのまとめ 120
• 探索:ht->arPacked + h • 追加:ht->arPacked + h に与えられた zval
をコピー • 削除:ht->arPacked + h を UNDEF に置き換える • キーと値のペアで追加:現在の Packed Array の要素が Bucket の配列にコピーされ、通常の HashTable へ変換 される。 Packed Array の操作 121
視聴者の皆さん「(理解する)」 122
実際の挙動を確認する(見る対象) 1. Packed Array の実際の挙動を確認する 2. 通常の HashTable の実際の挙動を確認する 123
通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024',
]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 124
通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024',
]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 125
HashTableの生成&初期化 126
最初の要素の追加 追記したいキーと値のペアを Bucket に追加 127
通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024',
]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 128
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
2つ目の要素の追加 とりあえず Bucket を追加 130
2つ目の要素の追加 とりあえず Bucket を追加 → ハッシュ表上のハッシュ値 がコンフリクトした どちらもビットマスク すると-1になる 131
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
2つ目の要素の追加 1番目の Bucket から0番目の Bucket へチェインする 133
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
2つ目の要素の追加 ハッシュ表の-1番目に保存されてい た添字が更新される 135
通常のHashTable(PHPコード) // 1. 配列の初期化 $phpcon2024 = [ 'January' => 'phpcon-hokkaido-2024',
]; // 2. 要素の追加 $phpcon2024['February*'] = 'phpcon-kansai2024'; // 3. 要素の削除 unset($phpcon2024['January']); 136
要素の削除 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
要素の削除 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
要素の削除 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
要素の削除 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
要素の削除 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
要素の削除 1番目の Bucket からのチェイン がなくなる。 142
要素の削除 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
要素の削除 0番目の Bucket がUNDEFになる nNumOfElements を更新 144
ここまでのまとめ 145
• 探索:h (ハッシュ値 or 数値) からビットマスクしてハッ シュ表に保存されている添字を取り出し、Bucket 配列に アクセスする。衝突している場合は Z_NEXT
で次の要素 にチェインする。 • 追加:Bucket 配列に要素を追加し、ハッシュ表上で衝突 する場合は Z_NEXT で1つ前の Bucket を設定する • 削除:指定された Bucket を UNDEF に置き換える。 Z_NEXT で指定されていた場合は、それをなくす。 通常の HashTable の操作 146
アジェンダ 1. 前準備 2. 配列のデータ構造について 3. 実際の挙動を見てみよう 4. まとめ 147
まとめ • PHPの配列の内部データの配列は2種類ある ◦ Packed Array と、それ以外の通常の HashTable ◦ zval
の配列か、Bucket の配列かの違い • 配列の操作は、2つの配列で実装が異なる 148
• 探索:ht->arPacked + h • 追加:ht->arPacked + h に与えられた zval
をコピー • 削除:ht->arPacked + h を UNDEF に置き換える • キーと値のペアで追加:現在の Packed Array の要素が Bucket の配列にコピーされ、通常の HashTable へ変換 される。 (再掲)Packed Array の操作 149
• 探索:h (ハッシュ値 or 数値) からビットマスクしてハッ シュ表に保存されている添字を取り出し、Bucket 配列に アクセスする。衝突している場合は Z_NEXT
で次の要素 にチェインする。 • 追加:Bucket 配列に要素を追加し、ハッシュ表上で衝突 する場合は Z_NEXT で1つ前の Bucket を設定する • 削除:指定された Bucket を UNDEF に置き換える。 Z_NEXT で指定されていた場合は、それをなくす。 (再掲)通常の HashTable の操作 150
エンジニア採用サイト エンジニア採用資料 開発者ブログ PR TIMES エンジニア 採用情報 検索 PR TIMESでは一緒に働く仲間を募集中! We Are
Hiring! 151
Appendix 152
• 配列の構造体やマクロ ◦ zend_type.h • 配列の操作 ◦ zend_hash.h ◦ zend_hash.c
実際、どのファイルを読めばいいの? 153
• 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
• おそらくメモリの節約と CPU キャッシュ戦略 • メモリの節約 ◦ arHash を別の配列として用意すると、それを参照す るポインタを別途用意する必要がある。
なので、その分 64 (32) bits の節約ができる。 • CPU キャッシュ戦略 ◦ arData と arHash がメモリ上の近い場所にあること で、空間的局所性を利用しキャッシュに乗りやすく なる(と思われる) 何故、ハッシュ表はマイナスでアクセスするの? 155
156 備考: なぜPHPの配列の内部構造で使われるハッシュテーブルは、OR演算子でビットマスク を計算しているのか? https://zenn.dev/meihei/articles/e5aa05a0e6c82e ビットマスクはANDではなくORなの?
• 変数をメモリに配置する際、CPUの環境によってバ イト列の並べ方が異なる ◦ これを「バイトオーダー」と呼ぶ ◦ 上位バイトから順に配置されるビッグエンディアン ◦ 上位バイトから逆順に配置されるリトルエンディアン •
uint32_tとuint8_tが4つの構造体を共用体とすると き、uint32_tのバイトオーダーによって構造体から のアクセスが意図せぬものになることがある ◦ それを防ぐためにバイトオーダーによって構造体の定 義の順番を変えている(文マクロ) ZEND_ENDIAN_LOHI_~ は何をしているの? 157
• List とはキーが0からの連番になった配列 • Packed Array は HASH_FLAG_PACKED が有効な配 列
◦ 一度 zend_hash_packed_to_hash が呼ばれると基本 的には HT_IS_PACKED は FALSE になる • 条件によっては List であってもハッシュ表を用いた Bucket の配列であることもある Packed Array ≠ List 158
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
参考 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