📄

2020/11/1 PHP・GCの話-8話)GC 関連機能紹介2(Weak Reference, Weak Map)(END)

2024/05/24に公開

この記事は2020/11/1に書きました。

前書き

  • すべての記事は、自分の勉強目的と主観の整理を含めています。あくまで参考レベルで活用してください。もし誤った情報などがあればご意見をいただけるととっても嬉しいです。
  • 内容では、省略するか曖昧な説明で、わかりづらいところもあると思います。そこは、連絡いただければ補足などを追加するので、ぜひ負担なくご連絡ください。
  • 本文での「GC」は、「Garbage Collection, Garbage Collector」の意味しており、略語として使われています。
  • この記事は、連載を前提に構成されています。

※ 連載目録

※ 連載で使うサンプルコード

Sample Code Link on Github

● ExampleGc.php : 2話から7話までの内容で使うサンプルコードです。
● ExampleWeakReference : 8話のWeakReferenceの内容で使うサンプルコードです。

本連載記事は、基本的にこのサンプルコードをベースに説明をしています。
サンプルコードは、必ず見る必要も実行してみる必要もありません。
各話ごとに、コードを分解して動作原理と結果を解説しますので、基本記事の内容で足りるように心がけます。
あくまで、全体コードをみたい、手元で回してみたい、修正して回してみたいという方向けです。

今回の話

今回は、以下のものを話そうと思うます。

    1. Weak Reference Classと使い方
    1. Weak Reference とは?
    1. Strong Reference vs Weak Reference
    1. Weak Reference どこで使うと良いか?
    1. Weak ReferenceのCache Pattern実装サンプルコード
    1. サンプルコードの実行・説明
    1. Weak Map Class (new feature in PHP8)
    1. Summary

1. Weak Reference Classと使い方

クラスとインタフェースの定義は、php.netで参考できます。
https://www.php.net/manual/en/class.weakreference.php

WeakReferencクラスのインタフェースはすごくシンプルです。

// 指定オブジェクトに対する弱参照を生成する静的メソッドです
public static create ( object $referent ) : WeakReference

// createで生成されたWeakReferenceインスタンスが参照しているオブジェクトのオリジナルを返却します。
public get ( void ) : ?object

以下のExampleのように、createで作って、getで値を取得し使うのが基本の使い方となります。

$obj = new stdClass;
$weakref = WeakReference::create($obj);
var_dump($weakref->get());
unset($obj);
var_dump($weakref->get());

//-- output
// object(stdClass)#1 (0) {
// }
// NULL

しかし、Weak Referenceは、その概念を理解することと、適切な箇所で使うのが大事になってきます。
引き続き、PHPでのWeak Referenceの概念と例を説明していきます。

2. Weak Reference とは?

PHP 7.4 から提供されるラッパークラスの一種になります。
WeakReference クラスは、object データタイプの変数をラッピングできます。

WeakReference の役目は、オリジナル変数データの消滅が保護されない、弱い参照を意味します。
PHP 基準にわかりやすくいうと、objectのzvalのrefCountを+1せず、objectの参照を持つ変数と言えます。

ここまでだと、ピントこないかもですね。引き続き、Weak Referenceを詳しくみていきましょう。

3. Strong Reference vs Weak Reference

PHP では、参照変数に対して、「強い参照(Strong)」と「弱い参照(Weak)」2つのタイプに分類されます。
厳密にいうと、私たちが一般的に使う変数の指定は全て強い参照であり、Weak Reference なと、言語がサポートする機能を明示的に使うことで、私たちが弱い参照を使うことができます。

● 強い参照(Strong Reference)
該当参照の変数が参照しているかぎりは、GCできない。

● 弱い参照(Weak Reference)
該当参照が参照していることとは関係なく、参照元の変数が消滅条件を満たしたら消滅する。(強い参照が0)

以下のサンプルコードを通して、PHP での強い参照と弱い参照をわかることができます。

① Strong Reference と Weak Reference の動作比較

//this is strong reference
$objA = new \stdClass;
$strongRefL1 = $objA;
$strongRefL2 = $strongRefL1;

//this is weak reference
$objB = new \stdClass;
$weakRefL1 = \WeakReference::create($objB);
$weakRefL2 = $weakRefL1;

xdebug_debug_zval('objA'); //to be refCount=3 because 3 strong reference
xdebug_debug_zval('objB'); //to be refCount=1 because 1 strong reference & 2 weak reference. weak reference don't increse refCount of zval.

//unset each Object's Origin Reference
unset($objA);
unset($objB);

//NOT be cleaned up
var_dump($strongRefL1);
var_dump($strongRefL2);

//BE cleaned up
var_dump($weakRefL1->get());
var_dump($weakRefL2->get());

② 実行結果

objA: (refcount=3, is_ref=0)=class stdClass {  }
objB: (refcount=1, is_ref=0)=class stdClass {  }
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:111:
string(15) "*** strongRefL1"
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:111:
class stdClass#695 (0) {
}
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:112:
string(15) "*** strongRefL2"
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:112:
class stdClass#695 (0) {
}
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:115:
string(20) "*** weakRefL1->get()"
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:115:
NULL
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:116:
string(20) "*** weakRefL2->get()"
/var/www/html/home_sub/cgi/app/Console/Commands/ExampleWeakReference.php:116:
NULL

実行結果の objA は、強い参照を生成しているため、refcount は3になっています。
しかし objB は、弱い参照を使って参照を生成しているため、refcount は1から変わっていません。

そこで、格 object を元参照変数を unset することで、refcount は-1 ずつされます。

強い参照の方は当然 refcount がまだ2なため消滅しません。
しかし、弱い参照はまだ有効ですが、その有無を問わず、refcount は0になった時点で、変数データは消滅し、結果が NULL になっています。

これで、Weak Reference に対する説明の内容も確認できました。

4. Weak Reference どこで使うと良いか?

正直なところ筆者自身も、実例でWeakReferenceを想定した設定と実装をして、大きな効果を得た経験はないですね(笑)

自分はウェブエンジニアなのですが、デスクトップやスマートフォンのアプリ開発する方の話を聞くと、多数の画像表示処理などではメモリー最適化のためWeakReferenceを使った実装をやると言う話は聞いたことがあります。

それでも、いつ使うか?を考えた時、Weak Referenceが活用できる一番わかりやすい事例はCache Patternの実装じゃないかなと思います。

Cache Patternは様々なケースで活用できる物ですね。

PHPに例えるとORM Library内の大容量データハンドリングや、画像みたいなバイナリーデータの要求に対するハンドリングなどが必要な時、有用である場合が多いと思います。

5. Weak ReferenceのCache Pattern実装サンプルコード

ここでは、基本インタフェースのみを具現します。

クラスのコンセプトとしては、以下になります。

  • 外部のパーシステンス領域からblobデータを取得するクラスとなります。
  • 基本インタフェースのみの具現になります。実際にパーシステンス領域からのblobデータの取得は具現していません。
  • blobデータを取得するときに、cacheに保存します。
  • 同じデータを取得する時は、保存されたcacheのデータを即リターンします。
    • 外部パーシステンス領域とのデータ取得時の性能コストが減ります。
  • cacheの消滅タイミングは、当クラスのインスタンスの外部スコープからの参照が0になるタイミングです。
    • 外部スコープから参照が0になった時、当クラスのインスタンスのメンバーとして参照を1持ってる状態になりますが、その参照は弱参照なため実際の参照カウントは0になり、すぐGCされメモリー資源をシステムに返却するようになります。

今回、考慮した方が実は良いですが、省略した観点は以下になります。

  • ※メモリーリミットの考慮は省略しています。本当に大きいデータを扱う時は、cacheで保存するときにカバーできるメモリーサイズを考えてcache制御を考える必要があります。
  • ※ cacheMap内の空のWeak Reference Objectの整理は省略しています。理想的なのは、GCされてオリジナルデータがシステムに返却される時、cacheMapの空のWeak Reference Objectも整理されるのが良いですね。PHP8からサポート予定のWeakMapでは、そう言うややこしいことを考えなくても、よくなるんじゃないかという気がします。

ではサンプルコードと実行事例をみていきましょう。

Example : BloblCacheStorage Class

BloblCacheStorage Sample Code Link on Github

/**
 * Weak Reference Example of Cache Pattern.
 *
 * ※ lifeCycle of Caches is relied on reference on outside scope.
 * ※ Class Design can be changed by cases that how to handle to LifeCycle of Caches.
 */
class BlobCacheStorage
{
    private static $instance = null;

    private $cacheMap = null;

    private function __construct()
    {
        $this->cacheMap = array();
    }

    /**
     * get Instance of BlobCacheStorage
     *
     * @return object BlobCacheStorage
     */
    public static function getInstance()
    {
        if (!self::$instance) {
            self::$instance = new BlobCacheStorage();
        }

        return self::$instance;
    }

    /**
     * get Keys of caches available to use.
     */
    public function getCachedKeysByArray()
    {
        $keys = array();

        foreach ($this->cacheMap as $key => $weakObj) {
            if (!empty($weakObj->get())) {
                $keys[] = $key;
            }
        }

        return $keys;
    }

    /**
     * get Data.
     *
     * if data is cached, return cache data.
     * if not, newly load data.
     *
     * @param string $key
     * @return object
     */
    public function get($key)
    {
        if ($this->isCached($key)) {
            Log::debug(null, ['event' => __CLASS__, 'msg' => 'cached data => '.$key]);
            return $this->cacheMap[$key];
        }

        Log::debug(null, ['event' => __CLASS__, 'msg' => 'NOT cached data => '.$key]);
        $blobObj = $this->getBlobDataObjct($key);
        $this->cacheMap[$key] = \WeakReference::create($blobObj);

        return $blobObj;
    }

    /**
     * determine requested data exists on cache
     *
     * @param string $key
     * @return bool
     */
    private function isCached($key)
    {
        return isset($this->cacheMap[$key]) && !empty($this->cacheMap[$key]) && !empty($this->cacheMap[$key]->get());
    }

    /**
     * get Blob Data from Persistant Storage.
     *
     * ※ this is example mocking code. Features you need can be implemented.
     */
    private function getBlobDataObjct($key)
    {
        $blobObject = new \stdClass;
        $blobObject->key = $key;

        return $blobObject;
    }
}

6. サンプルコードの実行・説明

① インスタンスを生成。persistent領域でのデータ取得

$storage = BlobCacheStorage::getInstance();

$data1 = $storage->get('1');
$data2 = $storage->get('2');
[2020-11-01 12:41:36] local.DEBUG:  {"event":"App\\Console\\Commands\\BlobCacheStorage","msg":"NOT cached data => 1"}
[2020-11-01 12:41:36] local.DEBUG:  {"event":"App\\Console\\Commands\\BlobCacheStorage","msg":"NOT cached data => 2"}

インスタンスを生成して、1と2に当たるデータを取得します。
このタイミングでは、cacheが存在しないため、persistent領域からデータを取得します。
それと同時に、インスタンス内のメンバーとして、原本データをcacheとして保持しますが、「弱参照」として保持するということが大事なポイントとなります。
getメソッドは、そのデータに対する強い参照をリターンします。

これにより、インスタンスないでは「弱参照」だけを保持する形になるので、リターンされた上位スコープの参照カウントが0になった時点でGC対象になります。

Log::debug('datas of 1,2 are cached in BlobCacheStorage => ', $storage->getCachedKeysByArray());
[2020-11-01 12:41:36] local.DEBUG: datas of 1,2 are cached in BlobCacheStorage =>  [1,2]

Storageインスタンスが保持しているcacheデータの情報をログとして出力すると、1と2のデータが存在していることを確認できます。

② cacheされたデータの取得

$data2dummy = $storage->get('2');
Log::debug('data of 2 already cached. get from Cache => ', $storage->getCachedKeysByArray());
[2020-11-01 12:41:36] local.DEBUG:  {"event":"App\\Console\\Commands\\BlobCacheStorage","msg":"cached data => 2"}
[2020-11-01 12:41:36] local.DEBUG: data of 2 already cached. get from Cache =>  [1,2]

2はすでにcacheされているデータなため、getする時、cached dataから取得することをログで確認できます。
persistent領域ではなく、マシーンのメモリ領域に保存しておいたデータへアクセスするため、性能向上を図ることができます。

③ cacheデータの消滅

unset($data1);
Log::debug(
  'unset $data1. cache in BlobCacheStorage will be removed because datas of 1 is not needed on anywhere. => ',
  $storage->getCachedKeysByArray()
);
[2020-11-01 12:41:36] local.DEBUG: unset $data1. cache in BlobCacheStorage will be removed because datas of 1 is not needed on anywhere. =>  [2]

BlobCacheStorageのインスタンス内の弱参照をのぞいて、唯一の参照である$data1をunsetしました。
その後、保持中のcacheを確認すると、1はcacheから無くなっています。その理由は前もって紹介した「弱参照は参照カウントをあげない」ことで、緩い参照関係を具現しているからです。

これによって、取得したデータが、全てのスコップで使い終わったら自動消滅されるというCache Patternのクラスを具現できます。

その後、改めて1のデータを取得すると、cacheが消滅されているため、またpersistent領域から取得するようになるこを確認できます。

$data1 = $storage->get('1');
Log::debug('data of 1 is cached in BloblStorage => ', $storage->getCachedKeysByArray());
[2020-11-01 12:41:36] local.DEBUG:  {"event":"App\\Console\\Commands\\BlobCacheStorage","msg":"NOT cached data => 1"}
[2020-11-01 12:41:36] local.DEBUG: data of 1 is cached in BloblStorage =>  [1,2]

7. Weak Map Class (new feature in PHP8)

※ この内容は、作成当時ではリリースされていない機能なため、直接試しておりません。なので筆者の予想で作成しており、筆者の間違った理解などで間違った内容がある可能性もあります。あくまで参考までにご覧ください。

● WeakMap RFC
https://wiki.php.net/rfc/weak_maps

WeakMapは、PHP8から提供される予定の新機能です。

今まで、Weak Reference概念を適用したCache Patternを具現してみたのですが、実はWeakMapクラスが提供する機能と類似しています。

インタフェース構造は、こういうコンセプトになります。

final class WeakMap implements ArrayAccess, Countable, Traversable {
    public function offsetGet($object);
    public function offsetSet($object, $value): void;
    public function offsetExists($object): bool;
    public function offsetUnset($object): void;
    public function count(): int;
}

実際の使い方としては、既存のarray演算子をそのまま使えると思います。
上記のインタフェースはあくまで機能インタフェースを意味することで、実際の使い方は、格機能に当たる演算子になる感じだと思います。
(C++のOperator Overloadingと似たような概念と言えますかね。)

// ※ RFCのexampleをORMの場合を想像して、喩えを少し変えています。

$map = new WeakMap;
$obj = new ORM/Entity/UserData;

//弱参照として、WeakMapにデータを保持
$map[$obj] = $obj->getAll();
var_dump($map);

//WeakMapは弱参照として、データを参照しているので、強い参照がすべて消滅すると事前に消滅する。
unset($obj);

WeakMapは、基本とあるオブジェクトのインスタンスをキーとして持ち、特定のデータに対する弱参照をバリューとして持つことができます。

上記の例示のように、特定のORMのクラスのインスタンスをキーにしておいて、ORMクラスが具現している特定のデータ取得の結果をcacheとして弱参照で保持することで、簡単にCache Patternを実装できると思います。

そして、UserDataインスタンスが、全てのスコープから消滅する時、WeakMap内のCacheも一緒に消滅することになるでしょう。

PHP8をベースに改善されるFrameworkなどでは、WeakMapを取り入れたメモリ管理最適化などが、すごく楽しみです。

8. Summary

今回、覚えて良い内容は以下になると思います。

  • Weak Referenceは、特定変数データに対する「弱参照」を生成する
  • Weak Referenceは、変数データとメモリ管理において、便利な機能を提供する
  • 弱参照とは、変数データのGC Cycle情において、何の影響も与えない緩い参照
  • 弱参照をいくら生成しても、変数データの参照カウントは上がらない
  • 弱参照がいくらあっても、強い参照が0の場合は、GCの対象となりメモリ上のデータは消滅
  • Weak Referenceの一番わかりやすい例はCache Pattern (ウェブ、アプリ問わず)
  • Weak Mapは、Cache Patternの実装をより簡単に具現できるタメのData TypeとしてPHP8から使える予定

後書き

これで、GCに対して想定していた内容を全て扱いました。

今回は、1-6話までの内容をある程度事前知識としてもっている前提で書きましたので、「弱参照の生成と消滅時の参照カウントとデータの変化、参照関係図」などは省略しています。なので、いきなりみた方では、少し理解しづらいところもあるかもですね。

以前話で書いたように、弱参照の生成と消滅の段階ごとの参照関係図などで捕捉をすることも可能なので、もう少し詳しく理解したい方は、コメントいただけると幸いです。

最近の開発トレンドを考えると、開発の生産性などに役立つ知識とは言いづらいですが、筆者個人にとっては、設計者の思想や動作メカニズムなどを想像できる、とても面白いテーマでした。

次のテーマはまだ決まってないですが、まずは、連載ではなく、いろんな観点での記事を簡単な範囲で、当分はあげようと思ってます。

では、次も頑張りたいと思っております。

Discussion