admin のすべての投稿

mysqlにおけるvarcharのindex使用時の注意

概要

みなさんmysqlを使用していてvarcharのカラムにindexをつけることってないだろうか?
selectする際にindexが付いているカラムを対象にwhereをつけているのにindexが有効になっていない機会があり得る。
今回はそんな時に認識しておかないと、はまるかもしれない挙動について取り上げる。
(そもそもvarcharにindexつけることについてはまた別の機会にでも)

準備

  • テーブル
  • varcharのカラムにindex付いているテーブルを用意

  • 確認方法
  • 確認方法としてはmysqldの設定としてINDEXの効いていないクエリがslowlogに出力されるように設定し、slowlogの出力を確認しながら進める。

    事前確認

    まずは普通にqueryを発行してみる

  • 数値として検索した場合
  • mysql> select count(*) from t1 where column1 = 7395584;
    +----------+
    | count(*) |
    +----------+
    |       37 |
    +----------+
    1 row in set (0.63 sec)
    
    # Time: 150129  5:47:34
    # User@Host: mysql[mysql] @ localhost []
    # Query_time: 0.633334  Lock_time: 0.000088 Rows_sent: 1  Rows_examined: 1264725
    SET timestamp=1422510454;
    select count(*) from t1 where column1 = 7395584;
    
  • 文字列として検索した場合
  • mysql> select count(*) from t1 where column1 = '7395584';
    +----------+
    | count(*) |
    +----------+
    |       37 |
    +----------+
    1 row in set (0.01 sec)
    

    早い。slowlogの出力はなし。

  • explain
  • explainすると数値として検索した場合はいわゆるフルスキャンとなっていることがわかる

    mysql> explain select count(*) from t1 where column1 = 7395584;
    +----+-------------+--------+-------+---------------+------+---------+------+---------+--------------------------+
    | id | select_type | table  | type  | possible_keys | key  | key_len | ref  | rows    | Extra                    |
    +----+-------------+--------+-------+---------------+------+---------+------+---------+--------------------------+
    |  1 | SIMPLE      | t1     | index | idx1          | idx1 | 396     | NULL | 1265625 | Using where; Using index |
    +----+-------------+--------+-------+---------------+------+---------+------+---------+--------------------------+
    1 row in set (0.00 sec)
    
    mysql> explain select count(*) from t1 where column1 = '7395584';
    +----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
    | id | select_type | table  | type | possible_keys | key  | key_len | ref   | rows | Extra                    |
    +----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
    |  1 | SIMPLE      | t1     | ref  | idx1          | idx1 | 386     | const |   37 | Using where; Using index |
    +----+-------------+--------+------+---------------+------+---------+-------+------+--------------------------+
    1 row in set (0.00 sec)
    

    と、生のmysqlクライアントからの検索は分かった。
    アプリケーションからの実行はどうなるんだろうか。
    使用する機会の多いpdoについて検証してみる。

    アプリケーションレイヤからの検証

  • 生のquery
  • 簡単なphpアプリケーションを作成して、slowlogの出力を見る。

    <?php
    $dns = "mysql:host=localhost;dbname=testdb;charset=utf8";
    $user = "mysql";
    $pass = "pass";
    $pdo = new PDO($dns, $user, $pass);
    
    $stmt = $pdo->query("SELECT count(*) AS cnt FROM t1 WHERE column1 = 7395584");
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    
    var_dump($row);
    

    出力は下記のようになった。きちんと動作はしている。

    [vagrant@local tmp]$ php select.php 
    array(1) {
      ["cnt"]=>
      string(2) "37"
    }
    

    さてslowlog出力は?

    # Time: 150129  5:55:12
    # User@Host: mysql[mysql] @ localhost []
    # Query_time: 0.242804  Lock_time: 0.000055 Rows_sent: 1  Rows_examined: 1264725
    SET timestamp=1422510912;
    SELECT count(*) AS cnt FROM t1 WHERE column1 = 7395584;
    

    でた!
    これに関してはpdoは特に何もしない。mysqlクライアントから直接クエリを入力した時と同じような結果となった。
    結果は割愛するが、シングルクォートで文字列を囲った時にはslowlogの出力はされなくなった。したがってindexによる検索が有効になっているようである。

    ここで勘の良い人は気になっているかもしれないが、では逆に数値のカラムにINDEXがはられている場合、where句に記述したデータをシングルクォートでくくり文字列として検索した場合どうなるのか。
    ということで気になっているであろうが、これに関して検証した結果どちらでも適切にindexが効くことが確認できた。

  • prepared statement
  • さてprepared statementを利用した場合はどのようになるのか、検証したいと思う。
    pdoにはbindを行うために二種類のインタフェースが存在するが、今回はbindValueを取り上げる。
    マニュアルを確認すると第三引数にバインドするパラメータの方が指定できる。なんとなく今検証していることをの結果を匂わせるようなインタフェースをしている。

    まずは第三引数のことはおいておき、純粋にPHPの型推論による挙動の変化を観察しようではないか。

    初めに数値としてbindValueを実行してアプリケーションを叩いてみる。
    アプリケーションは下記のようになる。

    <?php
    $dns = "mysql:host=localhost;dbname=testdb;charset=utf8";
    $user = "mysql";
    $pass = "pass";
    $pdo = new PDO($dns, $user, $pass);
    
    $sql = "SELECT count(*) AS cnt FROM t1 WHERE column1 = :column1";
    $stmt = $pdo->prepare($sql);
    $stmt->execute(array('column1' => 7395584));
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
    
    var_dump($row);
    

    動作を確認し、結果も正常に取得できている。

    [vagrant@local tmp]$ php select2.php 
    array(1) {
      ["cnt"]=>
      string(2) "37"
    }
    

    さて、肝心なslowlogはどうなっているかというと。
    出ていない!
    prepared statementとbindを利用すると、数値文字列などのギャップはpdo側で吸収してくれるようだ。

    また当然bindの引数の型を文字列に加工したとしてもslowlogは出力されずに、indexが有効になっていることが確認できた。

    これについて思うことは、phpの場合に限って言うとなるべくprepared statementを使用したほうがお馬鹿なミスが減るだろうということだ。
    生のsqlは思ったよりもずっとデリケートなのかもしれない。

    他のアプリケーションに関してもアプリケーションが提供してくれる機能を用いるほうが良いだろう。


    macのtopコマンドを追う

    家に帰ると充電器に指しておいた愛器のMACがファンを最高スピードまでクロックアップさせながら唸りをあげていた。

    なぜだ。蓋閉じてるのに。

    今回はこういった漠然とした状態からコンピュータ内部でざっくり何が起こっているか判別するときに使用できるtopコマンドを掘り下げて解説しよう。

    topコマンドはosx(mac)だけでなくunix実装のほとんどのディストリビューションで提供されているツールであろう。
    システム全体をざっくり見るときによく用いられる。
    ただこれディストリビューションによって確認できる情報や、オプションとか結構変わってくるので注意。

    さてmacでterminalを立ち上げtopコマンドを打鍵すると下記のような表示が見て取れると思う。

    Processes: 226 total, 4 running, 9 stuck, 213 sleeping, 1096 threads
    Load Avg: 3.12, 3.16, 2.93  CPU usage: 16.81% user, 23.22% sys, 59.95% idle  SharedLibs: 9200K resident, 14M data, 0B linkedit.
    MemRegions: 45374 total, 1842M resident, 59M private, 1496M shared. PhysMem: 5103M used (1685M wired), 2594M unused.
    VM: 538G vsize, 1066M framework vsize, 3072256(54) swapins, 3399825(0) swapouts.  Networks: packets: 6042458/6896M in, 3543389/504M out.
    Disks: 1397033/93G read, 1239425/75G written.
    

    Load Avg: 3?何も起動してないのに。。。

    表示される項目について解説する。

    Processes – total

    マシン上で動作しているプロセスの数

    Processes – running

    実行中プロセスの数
    実行中となりうるプロセス数は1CPUにつき1プロセスだけである。
    動作しているマシンのCPUがクアッドコアなため最大で同時に4つのプロセスが動作可能

    Processes – stuck

    そもそもstuckとは?
    osxのtopコマンドにおけるstuckとはプロセスの状態がLIBTOP_STATE_STUCKとなっている状態のプロセス数である。
    またカーネルの状態としてはTH_STATE_UNINTERRUPTIBLEであることを指す。
    これはプロセスが割り込み不可能なwait状態であることを指す。
    通常はディスクやネットワークに対するI/O待ちのような状態が該当する。

    Processes – sleeping

    wait状態のプロセスの数を表す。

    Processes – threads

    スレッドの数を表す。

    Load Avg

    ロードアベレージとは実行キューの中に入っている平均ジョブ数を表す。
    (これはosxの定義であってディストリビューションによって算出方法は多少異なることもある)
    3つの数値が並んでいるがこれは左から、1分平均、5分平均、15分平均を表す。

    CPU usage – user

    ユーザ実行時間を表す。ユーザ時間とはアプリケーションレイヤでカーネル処理(システムコール)に費やされている時間以外の時間のことを指す。
    例えばアプリケーション中でシステムコールを利用している場合、その間はシステム時間として認識される。

    CPU usage – system

    システム実行時間を表す。アプリケーションやOSによりシステムコールに費やされている時間を表す。

    CPU usage – idle

    アイドル時間を表す。

    SharedLibs – resident

    メモリに常駐している共有メモリを表す。

    SharedLibs – data

    データ領域を表す。

    SharedLibs – linkedit

    MemRegions – total

    使用メモリサイズ。単位はmach virtual memory。

    MemRegions – resident

    常駐メモリのサイズ

    MemRegions – private

    非共有メモリのサイズ

    MemRegions – shared

    共有メモリのサイズ

    PhysMem – used

    使用中の物理メモリサイズ
    wiredという表記があるが、これはos kernelによって使用されていることを意味する。

    PhysMem – unused

    未使用な物理メモリサイズ

    VM – vsize

    仮想メモリの総サイズ。
    仮想メモリとは実際には存在しないが、実メモリにマップ領域を用意し、実際のメモリ上にページを配置したり(スワップイン)
    抱えきれなくなったページをディスク上に吐き出したり(スワップアウト)することでプロセスからは膨大なメモリが使用可能なように見えるようにする仕組み。
    当然実メモリよりも大幅に大きなサイズとなる。

    VM – framework vsize

    共有メモリにより諸費される仮想メモリサイズ。

    VM – swapins

    スワップインを起こしたページ数

    VM – swapouts

    スワップアウトを起こしたページ数

    Network packets

    ネットワークに対するin/outのパケットサイズを表す。

    Disks

    ディスク装置に対するread/writeのデータサイズを表す。

    本日はこんなところで。にしてもosxはドキュメントが少ないですね。
    https://apple.stackexchange.com/
    上記はstackoverflowのapple版みたいなものなんですが、そちらが一番情報量が豊富なように思います。


    count(*)からinnodbにおけるindex構成を確認する

    * 概要

    今回はinnodbにおけるcountの高速化について検証する。

    きっかけは下記のブログですが。いつもお世話になっております。

    http://nippondanji.blogspot.jp/2010/03/innodbcount.html

    要約すると下記のようなスキーム雨がある時

    CREATE TABLE t1 (  
      a bigint(20) unsigned NOT NULL AUTO_INCREMENT,  
      b int(11) DEFAULT NULL,  
      c tinyint(4) DEFAULT NULL,  
      d date DEFAULT NULL,  
      e varchar(200) DEFAULT NULL,  
      f varchar(200) DEFAULT NULL,  
      g varchar(200) DEFAULT NULL,  
      h varchar(200) DEFAULT NULL,  
      i varchar(200) DEFAULT NULL,  
      PRIMARY KEY (a)  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
    

    下記のようなsqlを想定する

    SELECT count(*) FROM t1;
    

    このとき例えばtinyintなどにindexを貼ることで、count(*)の高速化が見込める。

    innodbのcount(*)において全レコードへのアクセスが必要になることは変わりないが
    これは主キー(bigint)を全走査することよりも、小さいindexを全走査するほうが効率が良いということである。

    まあ頭のなかでは理解できて、予想はできているんだけどちゃんと自分の手でピコピコやりたいなというところで下記を確認する。
    1. 検索速度がa,b,cで変わることを確認(参照テーブルも)
    2. e,f,g,h,iがあるときとないときで検索速度がそこまで変わらないことの確認(クラスタインデックスのノードが影響を与えないこと)

    * 検証

    検証2のリーフノードの大きさの検証のため下記データベースを用意する。

    CREATE TABLE t1 (  
      a bigint(20) unsigned NOT NULL AUTO_INCREMENT,  
      b int(11) DEFAULT NULL,  
      c tinyint(4) DEFAULT NULL,  
      d date DEFAULT NULL,  
      e varchar(200) DEFAULT NULL,  
      f varchar(200) DEFAULT NULL,  
      g varchar(200) DEFAULT NULL,  
      h varchar(200) DEFAULT NULL,  
      i varchar(200) DEFAULT NULL,  
      PRIMARY KEY (a)  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
    
    CREATE TABLE t2 (
      a bigint(20) unsigned NOT NULL AUTO_INCREMENT,  
      b int(11) DEFAULT NULL,  
      c tinyint(4) DEFAULT NULL,  
      PRIMARY KEY (a)  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
    

    下記みたいな感じでデータを作成する

    <?php
    
    $pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8','root','');
    $stmt = $pdo->prepare("INSERT INTO t1 (b,c,d,e,f,g,h,i) VALUES (:b,:c,:d,:e,:f,:g,:h,:i)");
    
    $bind = array();
    $bind['b'] = $i;
    $bind['c'] = 1;
    $bind['d'] = '2014-12-25';
    $bind['e'] = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
    $bind['f'] = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
    $bind['g'] = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
    $bind['h'] = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
    $bind['i'] = '1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890';
    
    foreach($bind as $key => $value){
     $stmt->bindValue($key, $value);
    }
    
    for($i=1; $i<=10000000; $i++) {
     $stmt->execute();
    }
    
    // 値同じだけ今回はcountなので影響なし。
    

    * sqlのクエリキャッシュを無効にする

    mysql> show variables like 'query_%';
    +------------------------------+---------+
    | Variable_name                | Value   |
    +------------------------------+---------+
    | query_alloc_block_size       | 8192    |
    | query_cache_limit            | 1048576 |
    | query_cache_min_res_unit     | 4096    |
    | query_cache_size             | 0       |
    | query_cache_type             | OFF     |
    | query_cache_wlock_invalidate | OFF     |
    | query_prealloc_size          | 8192    |
    +------------------------------+---------+
    7 rows in set (0.00 sec)
    

    * 検索する

    まずは2.について。リーフノードのデカさは検索性能に影響するのか。
    予想ではしないと思う。B木のキーノードだけ走査するわけだから。

    mysql> explain select count(*) from t1;
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    | id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows    | Extra       |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    |  1 | SIMPLE      | t1    | index | NULL          | PRIMARY | 8       | NULL | 9140175 | Using index |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    1 row in set (0.00 sec)
    
    mysql> desc t1;
    +-------+---------------------+------+-----+---------+----------------+
    | Field | Type                | Null | Key | Default | Extra          |
    +-------+---------------------+------+-----+---------+----------------+
    | a     | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
    | b     | int(11)             | YES  |     | NULL    |                |
    | c     | tinyint(4)          | YES  |     | NULL    |                |
    | d     | date                | YES  |     | NULL    |                |
    | e     | varchar(200)        | YES  |     | NULL    |                |
    | f     | varchar(200)        | YES  |     | NULL    |                |
    | g     | varchar(200)        | YES  |     | NULL    |                |
    | h     | varchar(200)        | YES  |     | NULL    |                |
    | i     | varchar(200)        | YES  |     | NULL    |                |
    +-------+---------------------+------+-----+---------+----------------+
    9 rows in set (0.00 sec)
    
    mysql> select count(*) from t1;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (15.87 sec)
    
    mysql> explain select count(*) from t2;
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    | id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows    | Extra       |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    |  1 | SIMPLE      | t2    | index | NULL          | PRIMARY | 8       | NULL | 9223787 | Using index |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    1 row in set (0.00 sec)
    
    mysql> desc t2;
    +-------+---------------------+------+-----+---------+----------------+
    | Field | Type                | Null | Key | Default | Extra          |
    +-------+---------------------+------+-----+---------+----------------+
    | a     | bigint(20) unsigned | NO   | PRI | NULL    | auto_increment |
    | b     | int(11)             | YES  |     | NULL    |                |
    | c     | tinyint(4)          | YES  |     | NULL    |                |
    +-------+---------------------+------+-----+---------+----------------+
    3 rows in set (0.00 sec)
    
    mysql> select count(*) from t2;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (1.98 sec)
    

    !驚愕である。リーフノードの大きさが検索性能に大きく依存しているではないか。
    これについてはもっと深追いして理解する必要がありそうだ。

    * b (int)にindexを追加

    mysql> ALTER TABLE t1 ADD INDEX idx_int(b);
    Query OK, 0 rows affected (33.17 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    
    mysql> explain select count(*) from t1;
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    | id | select_type | table | type  | possible_keys | key     | key_len | ref  | rows    | Extra       |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    |  1 | SIMPLE      | t1    | index | NULL          | idx_int | 5       | NULL | 9140175 | Using index |
    +----+-------------+-------+-------+---------------+---------+---------+------+---------+-------------+
    1 row in set (0.00 sec)
    
    mysql> select count(*) from t1;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (1.92 sec)
    

    疾いっ!

    * つづいてc (tinyint)にindexを追加

    mysql> ALTER TABLE t1 ADD INDEX idx_tinyint(c);
    
    Query OK, 0 rows affected (34.54 sec)
    Records: 0  Duplicates: 0  Warnings: 0
    
    mysql> explain select count(*) from t1;
    +----+-------------+-------+-------+---------------+-------------+---------+------+---------+-------------+
    | id | select_type | table | type  | possible_keys | key         | key_len | ref  | rows    | Extra       |
    +----+-------------+-------+-------+---------------+-------------+---------+------+---------+-------------+
    |  1 | SIMPLE      | t1    | index | NULL          | idx_tinyint | 2       | NULL | 9140175 | Using index |
    +----+-------------+-------+-------+---------------+-------------+---------+------+---------+-------------+
    1 row in set (0.00 sec)
    
    mysql> select count(*) from t1;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (1.78 sec)
    

    あんまりintと変わらない。これも参考元にある通り。
    だが内部的には読み込みページとかの量が半減しているはずである。

    さて予想外の挙動を見せたクラスタインデックスの違いはなんだろうか。

    ここでt1のvarcharで表されるカラムに対してもゴミを投入していたことに着目する具体的には下記のような感じで全レコードに対して同じようなデータを投入している
    検証のために下記のようなテーブルt6を作成した

    CREATE TABLE <code>t6</code> (
      <code>a</code> bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      <code>b</code> int(11) DEFAULT NULL,
      <code>c</code> tinyint(4) DEFAULT NULL,
      <code>d</code> date DEFAULT NULL,
      <code>e</code> varchar(200) DEFAULT NULL,
      <code>f</code> varchar(200) DEFAULT NULL,
      <code>g</code> varchar(200) DEFAULT NULL,
      PRIMARY KEY (<code>a</code>)
    ) ENGINE=InnoDB AUTO_INCREMENT=20000001 DEFAULT CHARSET=utf8;
    

    これに対してデータを1000万件投入する。その時のデータと速度は下記のようになっている

    mysql> select * from t6 limit 1;
    +----------+-----------+------+------------+------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+------+
    | a        | b         | c    | d          | e                                                                                                    | f                                                                                                    | g    |
    +----------+-----------+------+------------+------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+------+
    | 10000001 | 100000000 |    1 | 2014-12-25 | 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 | 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 |      |
    +----------+-----------+------+------------+------------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+------+
    1 row in set (0.00 sec)
    
    mysql> select count(*) from t6;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (10.42 sec)
    

    リーフノードのデータ量が検索速度に影響を及ぼす可能性を検証するためにカラムgを空文字にupdateして検証する。

    mysql> update t6 set g = '';
    Query OK, 10000000 rows affected (1 min 55.63 sec)
    Rows matched: 10000000  Changed: 10000000  Warnings: 0
    
    mysql> alter table t6 engine innodb;
    Query OK, 10000000 rows affected (59.86 sec)
    Records: 10000000  Duplicates: 0  Warnings: 0
    
    mysql> select count(*) from t6;
    +----------+
    | count(*) |
    +----------+
    | 10000000 |
    +----------+
    1 row in set (6.58 sec)
    

    なんと!高速化されたではないか。
    正直今までcount(*)するときにindexのキーだけなめて検索しているのかと思っていた。(これはcount(id)でも計測時間が変わらなかったことからの推測でもある)
    しかしこの結果から導かれるのは、count(*)したときにリーフノードのデータを読み取っているということにほかならないのではないかと。

    B木インデックスってのはB木のキーと値を全部読み取るような振る舞いをしている。と仮定できる。


    linux上での時間について

    まあなんでもないことでもあるんだけれども、個人的にもメモ。

    本日はlinuxにおける、時間、についてです。

    正確に言うとlinuxシステムとして提供している時間です。これには三種類ありそれぞれ

    • 実時間
    • ユーザ時間
    • システム時間

    になります。それぞれ説明していくと。

    実時間とは実際にプログラムの実行中などに経過した時間を表します。これは現実世界の時間の経過と完全に一致します。

    またユーザ時間とはプログラムの実行中にユーザのアプリケーションが消費したCPU時間のことを示します。

    同様にシステム時間とはプログラムの実行中にシステムが消費したCPU時間のことを示します。

    通常アプリケーションの消費した時間を計測したい際にはユーザ時間+システム時間ということになると思います。

    実時間に関してはOS内部では他にもアプリケーションがいくつも走っていますからそれらを除外した時間を計測できるという意味で、実時間を対象とするケースは少ないでしょう。

     

     


    エンジニアはフリーランスになったほうが良い

    掲題の話。ちょっと誇大広告入っていますが、ちょっと思うところもあってまとめてみようと思います。

    より正確に言うと別にフリーでなくても独立しなくても、会社員という立場でも良い。
    なんだけど大事なのは

    常に自分にとって良い仕事を追求し続けることを忘れないこと。
    そして今の環境が不適切だと感じたら次のステップへ移動する時期であり、迷わず移動すること。

    を心がけていたほうがよい。
    逆に会社員でも社内にそういった仕組みを取り組めている会社もあり、そういった会社は非常に良い環境づくりに成功しているといえる。

    というか裏っ返すと技術一本で食っていくのであれば、食べるのに困らないように自分でカバーする次世代のドメインを意識し、腕を磨き続ける必要があるというネガティブな側面にも置き換えられるのですが。

    個人的には上記のテーマをコンスタントに実現していくためにはフリーランスという独立した立場が一番やりやすいと思う。
    その点も踏まえて幾つか実体験と自分の考えをもとにフリーランスになったほうが良い理由を展開していこうではないか。
    今まさに会社の中でくすぶっている人、独立しようか考えている人などの参考になればなと思う。

    0. 自分の経歴

    と、実体験の伴ってない主張はあまり信頼できるものでもない気がするので自分の経歴でも紹介しようと思います。
    働き出してからはエンジニア一色。ただ転職回数は同年代にしてはかなり多いと思います。
    5年で5社くらいに所属しており。(これは社員として所属。です)
    ペース的には1年で一社ペース。これは驚異的な飽きっぽさ。だと思います。
    国内だけでなくフィリピンでエンジニアとして働いた実績もあり、日本人とフィリピン人の能力差、価格差なども見てきました。

    今現在はフリーランスとして活動しており、メインで1箇所の現場に常駐して日中働く形で、1年程度同じ現場に常駐しています。
    また合わせて幾つか他の現場でも仕事を走らせているスタイルをとっています。

    自分自身もう社員として雇われようという気は毛頭なく、今後はしばらくフリーランスまたは起業という形で仕事を進めていきたいと思っています。
    そのほうが合理的ですしね。

    さて、このように一応ひととおり経験した中でメリットと思っていることを列挙していきましょう。

    1. 給与基準が高い。そして明確

    まずはこれ、ここで言うのは一般的な市場の話なんだけれども、フリーランスのほうが頭一個抜いて給与が高い。
    そもそもの話、フリーエンジニアを受け入れることができる企業というのは、すでに体力がある企業だけだから、という話でもある。

    対照的にもう悲惨としか言いようがないのが、SESをメインとして行っている会社である。
    この場合従業員は作業者として、他の会社に対して常駐し、業務を遂行することで所属会社に対して利益を発生させる。
    当たり前なんだけど会社としては発生している利益以上に、社員に給料を発生させる訳にはいかない。
    ということで社員からしてみると実際に自分が稼いでいる単価の大部分を会社に納めて、残りカスをもらっているような形になる。
    ・・・いやもうこれわけわかんないよね。
    さっさと直接契約したほうがよいのである。だってほぼ全部自分でもらえるようになるんだもん。

    おんなじ仕事をしているにもかかわらず給料が違ったら、選択は明確ですよね。

    またその給与基準としては、仕事ができればよいのである。
    面倒くさい人間関係とかなく、入社後に上司に気をもむ必要もなく、とにかく作業者としての実績を上げていけばそれなりに給与が上がる。

    社員として入社すると、まずは訳の分からない今期目標とかたてさせられ、終わったら振り返りだのなんだの。
    結果的に達成度がこのくらいだから、今期はこのくらいの査定だね。なんてやりとりもない。
    そんなものは自分で管理すれば良い。なぜいちいち上長とすり合わせる必要があるのか。
    こんな上長の能力に左右される運ゲーするくらいなら自分の能力だけで勝負していったほうがよっぽど合理的であるし、結果的に評価される。

    また会社というのはどうしても大人数を評価する際に、賃金テーブルのような評価制度を導入する。
    そのため社員として評価を上げたとしても、そのテーブルに沿ってしか変化しないことが多い。
    対してフリーであれば成果を、社外にも展開することで、再利用可能な評価へと昇華することができる。
    社会全体を横断的に移動しやすいのである。(これは意識的に自分でメディアを活用していくとやりやすい。)

    2. 自分で現場を選ぶことができる利益

    まずはいまの日本はかなりの売り手市場だ。優秀なエンジニアはどの企業でもほしがっており、まず選ぶ権利がある。
    自分の経歴でも紹介したが、短期間で転職したとしても、能力さえあれば職にはつける
    (ただし、転職を繰り返すと別の面が心配され雇用されなくなるだろう。汗)
    これが他の業種だったら今頃私はコンビニのバイトにでもなっていると思う。

    また自分が足りないと思っている技術を補ったり、伸ばしたい技術があったり、今後伸びると踏んでいる技術があった場合現場を変えれば良い。
    またこれは給与でも同様であり、とりあえず必要なスキルセットで金がほしい場合、これも環境を変えれば良い。

    また家庭があり仕事よりも、嫁や子どもと一緒にいたいと思うのであれば、中にはリモートOKな現場もある。
    田舎でのんびりしながら働きたいのであればそれもよいでしょう。

    幸せを追い求めて働き方を選べるというのは大きいと思う。

    3. 複数の仕事を走らせられる

    常に自分で仕事をある状態にしておくことで結果的にリスクを避けられる。
    複数の掛け持ちをしていれば、仮に一つの仕事で失敗してもリスクは抑えられるでしょう。
    会社員では副業禁止。などとくだらない制約を設けている企業もあり。フリーならば縛られることもない。

    またいろいろな現場で働くことによって自分の価値を認識できるのも良い所です。
    今の自分のどのくらいのパワーで、どのくらいお金を稼げるか。
    これは逆に繰り返すと仕事を依頼する側になった時のバロメータにもなる。

    仕事を納品ベースの仕事に切り替えれば、爆発的に売上を上げることも可能でしょう。
    その場合各仕事で繁忙期をうまくずらすようにしてスケジューリングすればよい。
    こういう働き方のチョイスは常駐型の社員としてでは無理な選択です。

    4. いつでもやめられる

    社員でもやめられますが、はじめからフリーとしてスタイルを構築しておいてリスクヘッジをとっておけば。
    次の仕事を見つけるのはたやすいです(というかそういうふうに作っておく)

    嫌ならやめたらいいんよ。人生休みたい時もあるし。
    定期的なinputの時間を取りたいときもあり。集中したい活動があればそれを優先するべきである。
    これって人生の本質だよね。仕事をするのは稼がなければならないわけですが、仕事をするために生きているわけではない。

    5. 楽しい

    とにかく楽しい。なんでも自分の好きなようにできることが楽しい。

    どうでしょうか。当たり前ですが別にフリーにならなくても良いと思います。人それぞれですから。
    また冒頭でも述べましたが、中には会社内部でうまく社員を満足させるように仕組みを作っているところもいっぱいあります。
    参考になればと思います。

    また質問・相談などあれば寄せていただければ。
    一人でも多くの人の生き方の参考になればいいなと思います。


    Cのソケット実装を追う

    最近TokyoTyrant、TokyoCabinetに触る機会があって、その実装レベルまで込み入った話に接する機会があった。
    本日は最近気になっているTokyoTyrant, TokyoCabinetに関するモジュールのソースを追って、C言語で通信をどのように実装しているのかを確認してみたいと思います。

    さてちょっといきなり飛ぶが、tokyotyrantのインタフェースから各種のメソッドを呼び出すとき
    当然初めにリモートホストとの接続リソースを生成する必要がある。そこから始めよう。

    tokyotyrantでは接続リソースを生成する際に下記のtcrdbopenを利用する。

    /* Open a remote database. */
    bool tcrdbopen(TCRDB *rdb, const char *host, int port){
      assert(rdb && host);
      if(!tcrdblockmethod(rdb)) return false;
      bool rv;
      pthread_cleanup_push((void (*)(void *))tcrdbunlockmethod, rdb);
      rv = tcrdbopenimpl(rdb, host, port);
      pthread_cleanup_pop(1);
      return rv;
    }
    

    リモートリソースをオープンするときtcrdblockmethodを使用。
    ロックの取得を行っている。

    /* Lock a method of the remote database object.
       <code>rdb' specifies the remote database object.
       If successful, the return value is true, else, it is false. */
    static bool tcrdblockmethod(TCRDB *rdb){
      assert(rdb);
      if(pthread_mutex_lock(&amp;rdb-&gt;mmtx) != 0){
        tcrdbsetecode(rdb, TCEMISC);
        return false;
      }
      return true;
    }
    

    実態は何をやっているかというと下記メソッドを実行している
    pthread_mutex_lock(&rdb->mmtx)

    &rdb構造体を確認すると

    typedef struct {                         /* type of structure for a remote database */
      pthread_mutex_t mmtx;                  /* mutex for method */
      pthread_key_t eckey;                   /* key for thread specific error code */
      char *host;                            /* host name */
      int port;                              /* port number */
      char *expr;                            /* simple server expression */
      int fd;                                /* file descriptor */
      TTSOCK *sock;                          /* socket object */
      double timeout;                        /* timeout */
      int opts;                              /* options */
    } TCRDB;
    

    各種メソッドのためのmutexロックとある
    ところでさらにpthread_mutex_tという型が何なのか。

    pthread_mutex_tとはtokyotyrantにおける拡張ではなくc言語で用意されている機構である。
    manpageは下記になる。

    http://linuxjm.sourceforge.jp/html/glibc-linuxthreads/man3/pthread_mutex_lock.3.html
    

    pthread_mutex_lockはmutexロックを取得しようとする。mutexロックはスレッドの間で共通のロックを一つだけ持ち共有する。
    ということはプロセス間ではそのロックは有効ではないので、その場合はまた別のロック機構が必要となる。

    またpthread_mutex_lockはロックを取得できれば、直ちにロックを返却し、ロックが取得できなかった場合はロックが取得できるまでスレッドの実行を停止させるなどの挙動を示す。
    ここですでにロックを取得していたスレッドがいた場合の挙動は、mutexを初期化した際の状態に依存して異なる。
    tcrdbの初期化処理を見てみよう。

    /* Create a remote database object. */
    TCRDB *tcrdbnew(void){
      TCRDB *rdb = tcmalloc(sizeof(*rdb));
      if(pthread_mutex_init(&rdb->mmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
      if(pthread_key_create(&rdb->eckey, NULL) != 0) tcmyfatal("pthread_key_create failed");
      rdb->host = NULL;
      rdb->port = -1;
      rdb->expr = NULL;
      rdb->fd = -1;
      rdb->sock = NULL;
      rdb->timeout = UINT_MAX;
      rdb->opts = 0;
      tcrdbsetecode(rdb, TTESUCCESS);
      return rdb;
    }
    

    するとpthread_mutex_init関数を用いて、第二引数がNULLで初期化されていることがわかる。
    pthread_mutex_initのmanpageは下記にある

    http://linuxjm.sourceforge.jp/html/glibc-linuxthreads/man3/pthread_mutexattr_init.3.html
    

    マニュアルを拝見すると第二引数はpthread_mutexattr_tという属性を指定することになる。
    これがNULLである場合代わりにデフォルトの属性が使用される。
    デフォルトのmutex種別は「速い(fast)」である。

    さて、先のコードに戻って、このfast種別の場合、ロックを取得しようと場合どうなるかというところに話を戻そう。
    mutexがfastの場合、すでにロックがあるmutexを取得しようとした場合呼び出しスレッドを永遠に停止させる。
    ちなみにpthread_mutexattr_initの返り値の定義に関しては常に0を返却する。とある。
    これはつまりtcrdblockmethodメソッドは、純粋にロックを直列に管理するような機構になっている事がわかる。
    0でない場合雑多なエラーとして返却しているが、これはプログラムとして異常な状態になっていることを管理する機構であると推測される。

    さて無事にほかスレッドとの競合に打ち勝ってロックが取得できたところで、再びtcrdbopenに戻ってみよう。

    /* Open a remote database. */
    bool tcrdbopen(TCRDB *rdb, const char *host, int port){
      assert(rdb && host);
      if(!tcrdblockmethod(rdb)) return false;
      bool rv;
      pthread_cleanup_push((void (*)(void *))tcrdbunlockmethod, rdb);
      rv = tcrdbopenimpl(rdb, host, port);
      pthread_cleanup_pop(1);
      return rv;
    }
    

    次にpthread_cleanup_pushという関数を呼び出している。これを見てみよう。
    マニュアルによると、スレッドがキャンセルされた場合の、ハンドラメソッドを追加するとある。
    C言語では作成したスレッドは明示的に終了することができ、その際にpthread_cleanup_pushで登録したハンドラが実行される。
    この場合tcrdbunlockmethodが実行され、その時の引数としてrdbを渡すということを宣言している。

    ここでtcrdbunlockmethodを見てみよう

    /* Unlock a method of the remote database object.
       </code>rdb' specifies the remote database object. */
    static void tcrdbunlockmethod(TCRDB *rdb){
      assert(rdb);
      if(pthread_mutex_unlock(&amp;rdb-&gt;mmtx) != 0) tcrdbsetecode(rdb, TCEMISC);
    }
    

    非常にシンプルでmutex_lockを開放していることがわかる。

    さて再びtcrdbopen関数に戻って、tcrdbopenimplを参照してみよう。

    /* Open a remote database.
       <code>rdb' specifies the remote database object.
       </code>host' specifies the name or the address of the server.
       `port' specifies the port number.
       If successful, the return value is true, else, it is false. */
    static bool tcrdbopenimpl(TCRDB *rdb, const char *host, int port){
      assert(rdb &amp;&amp; host);
      if(rdb-&gt;fd &gt;= 0){
        tcrdbsetecode(rdb, TTEINVALID);
        return false;
      }
      int fd;
      if(port &lt; 1){
        fd = ttopensockunix(host);
      } else {
        char addr[TTADDRBUFSIZ];
        if(!ttgethostaddr(host, addr)){
          tcrdbsetecode(rdb, TTENOHOST);
          return false;
        }
        fd = ttopensock(addr, port);
      }
      if(fd == -1){
        tcrdbsetecode(rdb, TTEREFUSED);
        return false;
      }
      if(rdb-&gt;host) tcfree(rdb-&gt;host);
      rdb-&gt;host = tcstrdup(host);
      rdb-&gt;port = port;
      rdb-&gt;expr = tcsprintf(&quot;%s:%d&quot;, host, port);
      rdb-&gt;fd = fd;
      rdb-&gt;sock = ttsocknew(fd);
      return true;
    }
    

    細かなエラーチェックは割愛し、本体を見てみると
    port番号が1未満、これを想定しているのはおそらく未指定であった場合であるが、そのときはttopensockunix関数を呼び出している。
    またportが指定されていた場合はttopensockが呼び出されている。これらについて追っていこう。

    初めにttopensockunixから参照してみる。

    /* Open a client socket of UNIX domain stream to a server. */
    int ttopensockunix(const char *path){
      assert(path);
      struct sockaddr_un saun;
      memset(&saun, 0, sizeof(saun));
      saun.sun_family = AF_UNIX;
      snprintf(saun.sun_path, SOCKPATHBUFSIZ, "%s", path);
      int fd = socket(PF_UNIX, SOCK_STREAM, 0);
      if(fd == -1) return -1;
      int optint = 1;
      setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char *)&optint, sizeof(optint));
      struct timeval opttv;
      opttv.tv_sec = (int)SOCKRCVTIMEO;
      opttv.tv_usec = (SOCKRCVTIMEO - (int)SOCKRCVTIMEO) * 1000000;
      setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&opttv, sizeof(opttv));
      opttv.tv_sec = (int)SOCKSNDTIMEO;
      opttv.tv_usec = (SOCKSNDTIMEO - (int)SOCKSNDTIMEO) * 1000000;
      setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&opttv, sizeof(opttv));
      double dl = tctime() + SOCKCNCTTIMEO;
      do {
        int ocs = PTHREAD_CANCEL_DISABLE;
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &ocs);
        int rv = connect(fd, (struct sockaddr *)&saun, sizeof(saun));
        int en = errno;
        pthread_setcancelstate(ocs, NULL);
        if(rv == 0) return fd;
        if(en != EINTR && en != EAGAIN && en != EINPROGRESS && en != EALREADY && en != ETIMEDOUT)
          break;
      } while(tctime() <= dl);
      close(fd);
      return -1;
    }
    

    sockaddr_unという構造体が出現しているが、これはUNIX LOCALなソケットのアドレスの情報を表す構造体である。
    通信のためのfd(ファイルディスクリプタ)を取得するソケット生成の実態はscoket関数で行われている
    引数がPF_UNIX, SOCK_STREAM, 0となっているが順に説明する。

    PF_UNIXはソケット通信にファイルを用いることを示す。
    詳しくは割愛するが、UNIXのローカルのソケット通信にはファイルを用いたものとTCP通信を用いたものがある。
    またSOCK_STREAMに関してはソケットのタイプを表しており、これも詳しくは割愛するが
    UNIXのソケットタイプにはSOCK_STREAMとSOCK_DGRAMが存在する。

    ちなみにドメインソケットのアドレスは下記の構造体でシンプルに示される

    #define UNIX_PATH_MAX    108
    
    struct sockaddr_un {
        sa_family_t sun_family;               /* AF_UNIX */
        char        sun_path[UNIX_PATH_MAX];  /* pathname */
    };
    

    また作成したソケットに対してオプション設定している箇所がある。
    SOL_SOCKETはオプションの層を表す識別子で、ソケットAPIそうでそうでオプションを有効にすることを表す。
    SO_RCVTIMEO, SO_SNDTIMEOはそれぞれ受信、送信のタイムアウト設定をしている。これを超えた場合エラーが送信される。

    最終的にconnectシステムコールを要求してファイルディスクリプタが参照しているソケットを引数で渡しているアドレス空間が示す場所とに連結してます。
    この一連の処理によってサーバと接続されたファイルディスクリプタが取得でき、それによって通信することができるようになるわけですね。

    これ以降の深いレイヤについてはUnixソースを参照していくと良いと思います。

    さて対してリモートサーバと通信する場合はどうしているのかを追っていきましょう。

    /* Open a client socket of TCP/IP stream to a server. */
    int ttopensock(const char *addr, int port){
      assert(addr && port >= 0);
      struct sockaddr_in sain;
      memset(&sain, 0, sizeof(sain));
      sain.sin_family = AF_INET;
      if(inet_aton(addr, &sain.sin_addr) == 0) return -1;
      uint16_t snum = port;
      sain.sin_port = htons(snum);
      int fd = socket(PF_INET, SOCK_STREAM, 0);
      if(fd == -1) return -1;
      int optint = 1;
      setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, (char *)&optint, sizeof(optint));
      struct timeval opttv;
      opttv.tv_sec = (int)SOCKRCVTIMEO;
      opttv.tv_usec = (SOCKRCVTIMEO - (int)SOCKRCVTIMEO) * 1000000;
      setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&opttv, sizeof(opttv));
      opttv.tv_sec = (int)SOCKSNDTIMEO;
      opttv.tv_usec = (SOCKSNDTIMEO - (int)SOCKSNDTIMEO) * 1000000;
      setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (char *)&opttv, sizeof(opttv));
      optint = 1;
      setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char *)&optint, sizeof(optint));
      double dl = tctime() + SOCKCNCTTIMEO;
      do {
        int ocs = PTHREAD_CANCEL_DISABLE;
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &ocs);
        int rv = connect(fd, (struct sockaddr *)&sain, sizeof(sain));
        int en = errno;
        pthread_setcancelstate(ocs, NULL);
        if(rv == 0) return fd;
        if(en != EINTR && en != EAGAIN && en != EINPROGRESS && en != EALREADY && en != ETIMEDOUT)
          break;
      } while(tctime() <= dl);
      close(fd);
      return -1;
    }
    

    ところどころがTCP通信に対応するような設定になっているだけで、基本的な内容はほぼ同じになっています。
    sockaddr_inについても下記のような定義になっており、ポート番号が増えているなどTCPレイヤに対応していることが予想できます。

    struct sockaddr_in {
        uint8_t sin_len;
        sa_family_t     sin_family;
        in_port_t       sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
    };
    

    こんなかんじにCではsocketを生成しているようですね。
    ちなみに生成したfdを元にデータをやりとりする場合sendというシステムコールを利用することになりますが、これはまた後々取り上げたいと思います。


    PHPのメモリ節約と参照渡しについて

    概要

    今回は以前から調べようと思って調べきれてなかった、参照渡しとを行うことでメモリの節約をできるのかということについてまとめたいと思います。

    トピックとしては下記のような。

    • コピーオンライト
    • 参照カウンタ、参照フラグ

    トピックおさらい

    PHPでは代入を行う際に基本的に何もしなければ値をコピーして渡します。
    参照を渡したい場合は&をつけることによって実現します。

    $a = 'str';
    $b = $a;
    $c = &$a;
    

    例えば上記のようなプログラムを作成した場合。
    $bには$aのコピーが作成され、$cには$aの参照が渡されます。

    ここで、「$bを使用することで、$aのコピーが作成されてしまうということはメモリが無駄になる。$cのように参照で保持しておいたほうがメモリの節約につながるのではないか」と思う方もいらっしゃるかもしれません。

    今日はこの辺について考察していきます。

    さて、私の考察の結論から先に書いてしまいますと「適切に参照渡しを使用することで、メモリの節約につながる」と思います。
    また合わせて「特に必要がなければ参照渡しを行う必要はない。これによってメモリが無駄になることもない」とも思っています。

    具体的に参照渡しするべき場所として考慮すべき時は

    • 巨大な構造を保持しているためメモリの消費を抑えたい
    • 参照元の変数(シンボル)と、参照先の変数が変更を共有できる(当たり前ですが)

    逆に参照渡ししなくてもメモリの消費につながらない時とは

    • 参照先の変数で値の変更を行わない※超重要

    であり、まさにここが今日のメイントピックでもあります。

    調査

    具体的に内部実装としてどのように先ほどの参照関係を保持しているかというと
    PHP内部で参照カウントとよばれる、値(変数コンテナ)を参照しているシンボルやリンクの数(refcount)および、参照フラグという変数コンテナを参照型で保持しているシンボルが存在するかどうかという情報(is_ref)を保持しています。

    例えば下記のコードに関して参照カウントと、参照フラグを見てみましょう。

    $a = 'str';
    $b = $a;
    $c = $b;
    

    このとき各シンボルの情報は下記のようになります。

    $a => (refcount => 3, is_ref => 0)
    $b => (refcount => 3, is_ref => 0)
    $c => (refcount => 3, is_ref => 0)
    

    すべての変数が同じ変数コンテナ(’str’)を参照している形になるので、納得ですね。

    次に冒頭に設置したコードだとどうなるかを確認してみましょう。

    $a = 'str';
    $b = $a;
    $c = &$b;
    

    非常におどろくべきところなのですが、参照カウントが2となっている変数コンテナと、参照カウントが1となっている変数コンテナにわかれました。

    $a => (refcount => 2, is_ref => 1)
    $b => (refcount => 1, is_ref => 0)
    $c => (refcount => 2, is_ref => 1)
    

    これはどういうことなのかというと、参照渡しによりシンボルに代入を行うと、そのタイミングで別個の変数シンボルが即座に作成されます。
    つまり変数コンテナのユニーク性にis_refフラグが関与しているということになります。

    上記の情報を元にもう一度、参照渡ししなくてもメモリの消費につながらない時について考えてみましょう。
    つまるところ、変数コンテナが作成される条件に注意していけば大丈夫です。

    悪いコードとして下記の例を上げましょう(ちょっと無理矢理かな・・・)

    // アプリケーションの設定項目なんかを参照で取得
    $config = &$app->_config;
    ...
    // 一旦退避とかしてみる
    $tmp = $config;
    

    上記の例では$tmpに$configを代入した段階で参照フラグの異なる変数コンテナを生成しています。
    このタイミングで一気にドカンと$app_configと参照フラグのみ異なり、内容の全く同じ変数コンテナが生成されメモリを逼迫します。

    結論

    じゃあどうすればいいのか・・・ということなんですが。

    結論、普通に使用していれば大丈夫です

    さきほどの例で言うと、下記のように通常参照にすれば良いと思います。

    // アプリケーションの設定項目なんかを参照で取得
    $config = $app->_config;
    ...
    // 一旦退避とかしてみる
    $tmp = $config;
    $tmp['url'] = $new_url;
    

    たとえば$app->_configが配列であった場合、配列のデータの持ち方として変数コンテナをネストするような形でちょうど、親子のような関係がうまれます。
    このときに上記のように一個の変数をいじるとしても、その末端の変数コンテナのみがコピー・変更されるだけで全体の他の項目は全く影響がないので、そこまでメモリなどについて心配する必要はありません。

    問題が起こるのは参照渡しを保持していた時のみということです。

    また冒頭のキーワードで示したようにコピーオンライトという方針に則ってメモリの最適化を図っています。
    これは変数を単純にコピーした時点においては、元々あった変数の変数コンテナをずっと参照し続けます。
    いざ、変更が入った段階で新しい変数コンテナを作成し、参照先をそちらに切り替えるのです。賢いですね。

    ちなみにこのコピーオンライトとはプログラミングの実装以外でも、OSのメモリ管理やファイル管理にも用いられているような技術ですね。

    本日はそろそろこのへんで。
    参照になれば幸いです。

    参考にしたページ
    PHP本家


    単調配列array_diffの最適化について

    簡単なarray_diffの検証用プログラム。

    皆さんご存知の通りphpにはarray_diffという引数指定した配列の差分だけを抽出する関数が備え付けであります。
    このarray_diffは配列が保持する値を比較します。

    機能的にはこれで十分要件を満たしてくれている場合が多いと思います。

    なんですが単調配列(連想配列ではない、オートインクリメントなインデックスによって保存される配列という意味で)においては
    これをarray_diff_keyとarray_flipを用いた処理に置き換えることができ、その処理速度について検討します。

    早速ですが簡単なテスト用のプログラムを用意

    ここでは100個の要素を持つ配列と、5この要素を持つ配列の差分を抽出します。

    if (!isset($argv[1])) {
      echo 'usage -- php profile.php {try_count}' . PHP_EOL;
    }
    $count = intval($argv[1]);
    
    $array1 = array(100,200,300,400,500,600,700,800,900,1000,1100,1200,1300,1400,1500,1600,1700,1800,1900,2000,2100,2200,2300,2400,2500,2600,2700,2800,2900,3000,3100,3200,3300,3400,3500,3600,3700,3800,3900,4000,4100,4200,4300,4400,4500,4600,4700,4800,4900,5000,5100,5200,5300,5400,5500,5600,5700,5800,5900,6000,6100,6200,6300,6400,6500,6600,6700,6800,6900,7000,7100,7200,7300,7400,7500,7600,7700,7800,7900,8000,8100,8200,8300,8400,8500,8600,8700,8800,8900,9000,9100,9200,9300,9400,9500,9600,9700,9800,9900,10000);
    $array2 = array(100,200,300,400,500);
    
    $start = microtime(true);
    for($i=0; $i<$count; $i++) {
      $array = array_diff($array1, $array2);
    }
    $end = microtime(true);
    var_dump($end - $start);
    
    $start = microtime(true);
    for($i=0; $i<$count; $i++) {
      $array = array_diff_key(array_flip($array1), array_flip($array2));
    }
    $end = microtime(true);
    var_dump($end - $start);
    

    で結果。10000回試行してみます。

    php profile.php 10000
    float(1.888808965683)
    float(0.19536399841309)
    

    すごい差ですね。
    array_flipのコストなんか関係なしにarray_diff_keyのほうが高速に参照できていることがわかります。

    ここまで来ると実装内部まで気になりますね。また暇を見つけて掲載できればなと思います。

    予想としては、ソートに最適化されたキー構造があるためやはりキーを下に検索すると早い。
    値の比較では、全キーに関して値を走査する必要が有るため、単純に総当り、ということになるんでしょう。
    件数が多ければ多いほど爆発的な違いなってくることが予想できます。

    単調な配列のdiffに関してはarray_diff_keyを利用すると良さそうですね。