/dev/null

脳みそのL1キャッシュ

PHPのOPcacheについて調べた

はじめに

ふと、PHPのOPcacheってどういう実装になってるんだろと思ったので調べてみることにしました。具体的には、いつ、何を、どこに、どんな形で、キャッシュし、いつキャッシュを使うのかについて調べました。

なお、今回の調査に用いたPHPのバージョンは7.4.10です。

github.com

0. そもそもどういう実行パスでコンパイルされるのか

キャッシュ以前にPHPがどうやってコンパイルされるの流れを調べる必要がありました。これがわからないことには、どこから手を付ければいいのかわからないですよね。

ソースコードを見てみたところ、PHPのファイルはどうやら以下の実行パスでコンパイルされることがわかりました。なお、SAPIはfpmである前提です。

OPcacheが関係してくるのは、persistent_compile_file()っぽいので、今回はここを重点的に調べます。

1. いつキャッシュするのか

persistent_compile_file()内を見てみると以下の部分に答えがあります。

/* If script was not found or invalidated by validate_timestamps */ [*1]
if (!persistent_script) {
    uint32_t old_const_num = zend_hash_next_free_element(EG(zend_constants));
    zend_op_array *op_array;

    /* Cache miss.. */
    ZCSG(misses)++;

    /* No memory left. Behave like without the Accelerator */
    if (ZSMMG(memory_exhausted) || ZCSG(restart_pending)) {
        SHM_PROTECT();
        HANDLE_UNBLOCK_INTERRUPTIONS();
        if (ZCG(accel_directives).file_cache) {
            return file_cache_compile_file(file_handle, type);
        }
        return accelerator_orig_compile_file(file_handle, type);
    }

    SHM_PROTECT();
    HANDLE_UNBLOCK_INTERRUPTIONS();
    persistent_script = opcache_compile_file(file_handle, type, key, &op_array);  [*2]
    HANDLE_BLOCK_INTERRUPTIONS();
    SHM_UNPROTECT();

    /* Try and cache the script and assume that it is returned from_shared_memory.
       * If it isn't compile_and_cache_file() changes the flag to 0
       */
        from_shared_memory = 0;
    if (persistent_script) {
        persistent_script = cache_script_in_shared_memory(persistent_script, key, key ? key_length : 0, &from_shared_memory);  [*3]
    }

2. 何をキャッシュするのか

zend_persistent_script構造体をキャッシュします。コンパイル済みのバイトコードだけではなく、キャッシュの一貫性検証に必要な情報など、メタデータ類も含まれています。

typedef struct _zend_persistent_script {
    zend_script    script;
    zend_long      compiler_halt_offset;   /* position of __HALT_COMPILER or -1 */
    int            ping_auto_globals_mask; /* which autoglobals are used by the script */
    accel_time_t   timestamp;              /* the script modification time */
    zend_bool      corrupted;
    zend_bool      is_phar;
    zend_bool      empty;
    uint32_t       num_warnings;
    zend_recorded_warning **warnings;

    void          *mem;                    /* shared memory area used by script structures */
    size_t         size;                   /* size of used shared memory */
    void          *arena_mem;              /* part that should be copied into process */
    size_t         arena_size;

    /* All entries that shouldn't be counted in the ADLER32
    * checksum must be declared in this struct
    */
    struct zend_persistent_script_dynamic_members {
        time_t       last_used;
#ifdef ZEND_WIN32
        LONGLONG   hits;
#else
        zend_ulong        hits;
#endif
        unsigned int memory_consumption;
        unsigned int checksum;
        time_t       revalidate;
    } dynamic_members;
} zend_persistent_script;

コンパイル済みのバイトコードzend_script型のscriptメンバの中にあって、これは以下のように定義されている。

typedef struct _zend_script {
    zend_string   *filename;
    zend_op_array  main_op_array;  // バイトコード列
    HashTable      function_table;
    HashTable      class_table;
    uint32_t       first_early_binding_opline; /* the linked list of delayed declarations */
} zend_script;

3. どこにキャッシュするのか

4. どんな形で保存しているのか

  • ハッシュテーブルのエントリ
  • おそらく、Hashtable APIを使っている

5. いつキャッシュを使うのか

persistent_compile_file()内の以下の部分に答えがあります。

if (file_handle->opened_path) {
    bucket = zend_accel_hash_find_entry(&ZCSG(hash), file_handle->opened_path);

    if (bucket) {
        persistent_script = (zend_persistent_script *)bucket->data;  [*1]

        if (key && !persistent_script->corrupted) {
            HANDLE_BLOCK_INTERRUPTIONS();
            SHM_UNPROTECT();
            zend_shared_alloc_lock();
            zend_accel_add_key(key, key_length, bucket);
            zend_shared_alloc_unlock();
            SHM_PROTECT();
            HANDLE_UNBLOCK_INTERRUPTIONS();
        }
    }
}
  • [*1]: ハッシュのエントリからzend_persistent_script構造体を取り出している箇所がある。ここがハッシュテーブルのエントリからキャッシュを取り出してるところっぽい
  • これ以降、timestampやchecksumによるキャッシュの一貫性の検証がありますが、これらの検証を突破するとめでたくキャッシュが使われることになります
    • 突破できなかった場合はpersistent_script = NULL;が実行され、キャッシュがなかったことにされます

おわりに

OPcacheは共有メモリにバイトコードおよびその他のメタデータをキャッシュします。また、キャッシュはハッシュテーブルのエントリという形で保持されていることがわかりました。

ソースコードを読んでみたところ、共有メモリやハッシュテーブル周りがよくわからなかったので、別の機会にこれらのコードも調査してみたいと思います。