同じように見えて異なる PHP の文字列についての話

はじめに

本記事は PHP Advent Calendar 2017 18日目 です。

先に断っておきます。
この記事の内容は、 php スクリプトを書く上で全く必要のない知識です。

知ってすぐ何かに役立つような情報を求めていたらごめんなさい。
https://qiita.com/advent-calendar/2017/php にはもっとたくさんの素晴く役立つ記事がありますのでどーぞ。

なお、検証に利用した php は 7.2.0 です。

PHP の文字列

PHP の文字列は、どれも同じ "文字列" ではあるのですが、中身がちょっと違うとことがあるのをご存知でしょうか。


例えば、

<?php
$str1 = 'A';
$str2 = sprintf('%s', 'A');

`$str1` と `$str2` はどちらも同じ `'A'` という文字列になりますが、違いがあります。
(変数名が違うとか、異なる zval である とかいう意味ではありません。)

<?php
$str3 = 'class';
$str4 = 'hoge';

`$str3` と `$str4` は、異なる文字列ですが、同じ性質を持っています。
そして同じ性質を持っているにも関わらず、少しだけ違うところがあります。

php スクリプト側には表れない、メモリの確保の仕方が異なるのです。

インターン化文字列

php スクリプト上に現れるすべての文字列リテラルインターン化されます。

インターン化、つまり同じ箇所に割り当てられたメモリを共有しているわけです。

<?php
function hoge() {
	$hoge = 'hoge';
	$fuga = 'hoge';
}

`$hoge`, `$fuga` は別の変数ですが、同じメモリに配置された `hoge` を参照していますし、
さらには 関数名、 変数名 の `hoge` も同じメモリ上に配置されます。(関数名や変数名に直接アクセスすることはあまりないかと思いますが、 `call_user_func` や `$$` で使われます)

この仕組みは 5.4 から導入された *1 もので、詳しくは @hnw さんの記事 (http://d.hatena.ne.jp/hnw/20151205) がわかりやすいので参照してください。

特殊なインターン化文字列

さてこの文字列のインターン化についてですが、実は php スクリプトに現れなくてもインターン化されている文字列が存在します。

1. 空文字 *2
2. 1byte 文字列(0-255) *3
3. その他、ZendEngineにおける頻出文字列 (KNOWN_STRING) *4

空文字がインターン化されていることで、 `if ($str === '')` みたいな条件式があっても、比較するたびに `''` がインスタンス化されるわけではなく、すでにインスタンス化された文字列が利用されることで高速化につながるというわけです。


1byte 文字列については 7.0 から新たに導入されたもの *5 です。
`'A'` のような直接的な表現だけでなく、 `substr` や `explode` を呼び出した結果が 1byte になる場合も、この文字列が利用されます。


KNOWN_STRING は 7.1 から導入されたもの *6 で、これについては、 php スクリプトの実行よりも、 ZendEngine 内部の最適化の意味合いが強そうです。
C言語でも、至る所に文字列リテラルが記述されていると、その分フットプリントが大きくなってしまいます。これを共通化することで、メモリ削減と参照の局所化につなげているのでしょう。


これらは、他のインターン化文字列とは異なり、 php 起動時に ZendEngine が常に生成しています。
そして、最も大きな違いは `emalloc` 使わずに直接 `malloc` によってメモリが割り当てられているということです。

つまり、 `memory_get_usage` の戻り値に全く含まれない部分なんですね。

`memory_get_usage` は php が利用しているメモリ量を返しますが、この値は `emalloc` という ZendEngine のメモリマネージャを介して割り当てられたメモリのみが集計対象となっています。
`malloc` によって割り当てられたメモリについては `memory_get_usage` の引数を true にしても取得できません。

冒頭コードの説明

というわけで、最初にお見せしたコードは

`$str1`
インターン化された 1byte 文字列
`$str2`
通常の文字列 (非インターン化文字列)
`$str3`
インターン化された KNOWN_STRING
`$str4`
インターン化された文字列


というように、それぞれ メモリ確保のされ方が異なる文字列なのでした。 (`$str1` と `$str3` は同じ様式ですが)

php を書く上でこの違いを意識することはないですし、意識しようとしても php スクリプトの世界でこれらが違うということを知る手段はありません。

では、どうやってこの違いがわかったのかというと、php 本体のソースコードを読んだうえで、 innerval という 雑な拡張を書いて試したからです。

<?php
$str1 = 'A';
$str2 = sprintf('%s', 'A');
$str3 = 'class';
$str4 = 'hoge';

check('$str1', $str1);
check('$str2', $str2);
check('$str3', $str3);
check('$str4', $str4);

function check($name, $str) {

	$state = [];
	$state[] = is_interned_string($str) ? 'INTERNED' : null;
	$state[] = is_persistent_string($str) ? 'PERSISTENT': null;
	$state[] = is_permanent_string($str) ? 'PERMANENET': null;

	echo "{$name} \"{$str}\" is ". (implode(",", array_filter($state)) ?: 'normal string'),"\n";
}

https://github.com/do-aki/innerval/コンパイルして有効にしてから 上記のような簡単なスクリプトを実行してみると、

$str1 "A" is INTERNED,PERSISTENT,PERMANENET
$str2 "A" is normal string
$str3 "class" is INTERNED,PERSISTENT,PERMANENET
$str4 "hoge" is INTERNED

という結果が得られます。*7

INTERNED はそのまま インターン化された という意味で、 PERSISTENT が malloc によって確保されたメモリであることを表しています。

PERMANENET は、リクエストをまたいで有効なメモリ (リクエスト終了時に破棄されない) を意味し、例えば opcache によって割り当てられた共有メモリを参照している場合に、 非 PERSISTENT な PERMANENET 文字列になります。 (opcache が絡むと話がややこしくなるので詳しくは割愛)

7.2 での変化

さて、このように表舞台にはでてこないインターン化文字列ですが、 7.2 になってとても大きな変更がありました。

Thread Safe版が再サポートされたのです。
https://github.com/php/php-src/commit/c6982995504b6e21e8a5ade29cfb16a55196dc43#diff-b27dcc67dedd7af10b0fb1ec4fd540dc

実は、5.4 で導入された時点では Thread Safe版 PHP でもインターン化されていました。
ところが、7.0 の開発過程で Thread Safe版 PHP におけるインターン化は無効化されていました https://github.com/php/php-src/commit/f4cfaf36e23ca47da3e352e1c60909104c059647#diff-b27dcc67dedd7af10b0fb1ec4fd540dc (すごいコミットコメントだ)

7.2 になって、それがまた復活したというわけです。

Thread Safe版 PHP を使っている人(いるの?) には朗報ですね。

まとめ

こんなもん知って何に役立つの? って思われるかと思いますが、何の役にも立ちません。
冒頭で免責した通りです。

ただ、オモテには現れないたくさんの改善の積み重ねが 今の PHP を作っているんだなぁと。

(そして、 Thread Safe版 PHP の不憫さよ。)