カテゴリー別アーカイブ: チューニング

GASでスプレッドシートのセルの書式を文字列に設定する

GAS 便利ですよね。
スクリプトでサクッとかける上にもともと拡張性の高いスプレッドシートなのでちょっとした実装でも良いものが作れます。
スプレッドシートなんかのインタフェースはエンジニア以外の人にも馴染み深いのでコストをかけて画面系を実装するよりも適している場合がよくあります。

なのですがGASを利用する際に正しくAPIやその動作を理解していないと、非常に重い動作になってしまうことがあります。
テストしてみたらループ処理なんかでずーっと時間がかかってとまらないスクリプトなんてこともあります。

今回GASでスプレッドシート上のセルの書式を文字列にできないかというところを試行錯誤にして改善してみたので、一つのパフォーマンス改善のアプローチとして参考にしてもらえればと思います。

やりたいこと

具体的にやりたいことはシンプルで、セルの値の先頭に + を入力したいということです。しかしそのままやろうとするとこれがうまくいかない。
デフォルトの状態ではスプレッドシートは先頭に+を認識すると値を数式であると認識して、展開しようとします。
実際には文字列ですので式の値が不正でありエラーとなります。

エクセルですと書式設定で テキスト なんていう書式があるようですが、現在のスプレッドシートの書式にはこのケースを許容してくれる書式は無いようでした。

ではどうするかというと先頭にシングルクォート()を打ち込みます。
こうすることでセルに入力された値をテキストだと認識してくれます。

問題はこれを大量のセルに対して実施したいということ。
ちまちま一つ一つのセルを手作業で編集していたのでは気が遠くなります。

GASを使って解決してみた・・・

始めに単純な思いつきで実装してみます。
レンジに関してはあまり深く考えないで良いです。

var valueRange = sheet.getRange('A1:A'); // A列に存在するデータに対して実施
for (var i=0; i<valueRange.getNumRows(); i++) {
  var cell = valueRange.getCell(i, 0);
  var value = cell.getValue();
  if (value == "") {
    continue;
  }
  cell.setValue("'" + values);
}

余裕のある人は一度実装してみてください。
動かしてみるとわかるのですがこの実装、めちゃめちゃ遅いです。

何故かと言うと getValue, setValue というAPIをforループ内で毎回呼び出しているからなんですね。

スプレッドシートは基本的にサーバ側で反映されたデータがブラウザに反映されます。
ですので上記のスクリプトでデータを書き換えている箇所は、ブラウザ上に表示されているデータを書き換えているのではなく実際にはGoogleのサーバ上にあるスプレッドシートのデータを編集しているような設計になっています。
で、ブラウザは逐次その反映されたデータを更新して表示しているということになります。
なのでループ毎に getValue, setValue を呼び出すことがものすごい遅延につながります。

ここまでの話で想像できるかと思いますが、GASの動作を高速化する際にまず考えることは GASのAPI呼び出しをなるべく少なくする ということです。
スプレッドシートを取り上げますがその他のGServiceでも同じことだと思います。
まーRDBでもなんでもバッチ処理にする。ということは基本ですね。。

この点について留意して最適化したコードを実装してみましょう。

処理を最適化する

下記が最適化されたコードになります。

var valueRange = sheet.getRange('A1:A');
var values = valueRange.getValues();
for (var i=0; i<valueRange.getNumRows(); i++) {
  if (values[i][0] == "") {
    continue;
  }
  values[i][0] = "'" + values[i][0];
}
valueRange.setValues(values);

こちらも実行していただけるとわかるのですが、一瞬で実行が完了します。
見ての通り API呼び出しを getValues, setValues にすることでたったの二回に減らすことができています。

APIマニュアルを良く読んで自分がやりたいことが他の方法で実現できないか?とよく考えると意外と様々なAPIが存在しているのでぜひ確認してみてください。
今回はこの方法で目的を達成する事ができました。
それでは。


str_replace, preg_replaceのパフォーマンス検証と呼び出しの最適化

概要

PHPでは文字列を置換するのにstr_replaceとpreg_replaceという関数を用いることができます、今回はそれぞれのパフォーマンスについて考察していきたいと思います。

予想としては当然preg_replaceのほうがコストが高く付くと思います、実際本当にそうなのか。またどの程度のパフォーマンスの開きが出るのかをいくつかのサンプルを用意して比較します。
簡単な表現から、多少複雑な表現まで検証してみます。
またそれぞれの関数の使用時の最適化についても調べたいと思います。

ちなみに当然ですがPHPの 公式マニュアル では特別な必要性がない限りstr_replaceを使用することを推奨しています。

preg_replaceとstr_replaceを理解する

preg_replaceとstr_replaceについてはご存じの方も多いかと思いますが、その名のごとくある文字列を対象として、特定の単語を別の単語で置き換えることのできる関数になります。
その違いも簡単で、str_replaceは特定の単語を検索置換するのに対して、preg_replaceは正規表現を用いて検索置換が行えます。

str_replaceについて簡単な例を示します。

$before = "this is test.";
$after = str_replace("this", "that", $before);
var_dump($after); // "that is test."

シンプルですね。文字列の置換ができます。

つづいてpreg_replaceについても簡単な例を示します。

$before = "this costs 1000yen. it costs 2000yen.";
$after = preg_replace("/(\d+)yen/", "¥$1", $before);
var_dump($after); // "this costs ¥1000. it costs ¥2000."

こちらも単純に正規表現を用いての置換ができることがわかりました。シンプルです。

性能を検証する

さて気になるパフォーマンスを見て行きましょう。
今回はテストケースを3つ用意しました。
1. 単純なパターンの単数の置換
2. 単純なパターンの複数の置換
3. 複雑なパターンの単数の置換

検証したPHPのバージョンは下記になります

$ php -v
PHP 5.4.30 (cli) (built: Jul 29 2014 23:43:29)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2014 Zend Technologies

単純なパターンの単数の置換を実行した際のパフォーマンス

ここでは単純なパターンでの一つの文字列の置換を実行します。
用意する計測するコードは下記のようになります。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('this', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace('/this/', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_2.php 100000
run test 100000 times.
check performance of str_replace.
double(0.15461802482605)
check performance of preg_replace
double(0.35560202598572)

このような単純なパターンでも約二倍程度の性能差があることが見て取れました。
予想通りの結果となり、検索置換をアプリケーションで行うようであれば特別な理由なくpreg_replaceを使用するべきでないですね。

単純なパターンの複数の置換を実行した際のパフォーマンス

続いて複数文字列を置換する際のパフォーマンスについて検証します。
下記のようにサンプルプログラムを用意します。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
$source = array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'nine', 'ten');
$dest = array(1,2,3,4,5,6,7,8,9,10);
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);


$source = array('/one/', '/two/', '/three/', '/four/', '/five/', '/six/', '/seven/', '/nine/', '/ten/');
$dest = array(1,2,3,4,5,6,7,8,9,10);
echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_1.php 100000
run test 100000 times.
check performance of str_replace.
double(0.53684210777283)
check performance of preg_replace
double(1.4594769477844)

ここでも2倍から3倍程度の性能差が確認できました。
置換対象が複数になることでその性能差が開いていることが確認できます。

ここでちょっと気になるのは、str_replace, preg_replaceのそれぞれの性能を先の検証パターンと比較した時に性能が線形に悪化しているわけではなさそうだということです。
これについては別件として最後に検証を行うこととします。

複雑なパターンの単数の置換を実行した際のパフォーマンス

最後に複雑な正規表現を用いた際のパフォーマンスの違いについて見てみます。
下記のプログラムのように検索する表現を無駄に複雑にしてみます。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of str_replace." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('this', 'that', $subject);
}
$end = microtime(true);
var_dump($end - $start);

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of preg_replace" . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = preg_replace('/([a-z]+) (is) (test)/', '$1 $2 $3', $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測します。

$ php profile_test_3.php 100000
run test 100000 times.
check performance of str_replace.
double(0.14943814277649)
check performance of preg_replace
double(2.3758080005646)

すさまじい差ですね。(こんなケースはないかと思いますが)
正規表現による検索が、検索パターンによってコストが増大していることがわかります。

(番外編)置換対象が複数ある場合の関数呼び出し時の最適化について

検証パターン2の考察時にも触れましたが、複数の検索置換を行うときにstr_replace, preg_replaceは同様のインタフェースを用意しており、検索パターンと置換パターンを配列で渡すことができます。
複数の置換を実行するときに、置換を一回一回実行する方法と、置換パターンを配列で渡し一度に実行する方法の実装の仕方によってどれだけ差が出るかということを検証します。

プログラムを用意します。
一回一回実行する方法では、置換を行い、置換後の文字列に対して再度置換を行い、、、を繰り返します。

<?php
if (!isset($argv[1])) {
  echo 'usage -- php profile.php {try_count}' . PHP_EOL;
}
$count = intval($argv[1]);
echo "run test {$count} times." . PHP_EOL;

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of multiple call." . PHP_EOL;
$start = microtime(true);
for($i=0; $i<$count; $i++) {
  $subject = str_replace('one', 1, $subject);
  $subject = str_replace('two', 2, $subject);
  $subject = str_replace('three', 3, $subject);
  $subject = str_replace('four', 4, $subject);
  $subject = str_replace('five', 5, $subject);
  $subject = str_replace('six', 6, $subject);
  $subject = str_replace('seven', 7, $subject);
  $subject = str_replace('eight', 8, $subject);
  $subject = str_replace('nine', 9, $subject);
  $subject = str_replace('ten', 10, $subject);
}
$end = microtime(true);
var_dump($end - $start);

$subject = "this is test sentence one. this is test sentence two. this is test sentence three. this is test sentence four. this is test sentence five. this is test sentence six. this is test sentence seven. this is test sentence eight. this is test sentence nine. this is test sentence ten.";
echo "check performance of single call." . PHP_EOL;
$start = microtime(true);
$source = array('one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten');
$dest = array(1,2,3,4,5,6,7,8,9,10);
for($i=0; $i<$count; $i++) {
  $subject = str_replace($source, $dest, $subject);
}
$end = microtime(true);
var_dump($end - $start);

計測してみます。

 $ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
double(1.2376821041107)
check performance of single call.
double(0.51983189582825)

!!!
をを。思った以上に差が出てきた
約倍程度のパフォーマンスの差がでています。

と余談なんですが、他のPCでも同じプログラムの実行を行ったところ、上記の結果ほどの差は出なかった。
その際のバージョンは5.4.30であった。

$ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
float(0.44886112213135)
check performance of single call.
float(0.36096215248108)

なんだかしこり残るのでphpのバージョンを5.6にあげて検証してみます。

$ php -v
PHP 5.6.6 (cli) (built: Feb 20 2015 22:35:31)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2015 Zend Technologies
    with Zend OPcache v7.0.4-dev, Copyright (c) 1999-2015, by Zend Technologies
    with Xdebug v2.2.5, Copyright (c) 2002-2014, by Derick Rethans
...
$ php profile_test_4.php 100000
run test 100000 times.
check performance of multiple call.
double(1.22190117836)
check performance of single call.
double(0.46425199508667)

悪化した!?!?!?

ここから考察できるのはどうやらphp5.4.30以降str_replaceの性能は悪くなっているようである。

どちらにしても最適化された呼び出しとしては配列で一度に渡したほうが高速である。

以上、もろもろパフォーマンスについて検証しました、参考にしていただければと思います。


xhprofを利用する

アプリケーションのパフォーマンスを向上させるためにはプロファイラと呼ばれるツールを利用するのが早いです。
phpではxhprofというプロファイラがメジャーであり今回はその導入手順や使用感について紹介します。

インストール

PECLから入れます。そんなに難しくありません。

  • ソースをダウンロード
  • wget http://pecl.php.net/get/xhprof-0.9.2.tgz
    
  • make
  • tar zxvf xhprof-0.9.2.tar
    cd xhprof-0.9.2/extension/
    phpize
    make
    make install
    
  • php.iniを編集
  • [xhprof]
    extension=xhprof.so
    xhprof.output_dir=/var/log/xhprof
    
  • ログディレクトリを作成
  • mkdir /var/log/xhprof
    chmod -R 777 /var/log/xhprof
    
  • apache再起動
  • service httpd restart
    
  • xhprofのテンプレディレクトリをドキュメントルート配下に移動
  • cp -r /tmp/xhprof-0.9.2/xhprof_html {document_root}/xhprof_html
    cp -r /tmp/xhprof-0.9.2/xhprof_lib {document_root}/xhprof_lib
    
  • プロファイリングしたい箇所にコードを埋め込みます
  • {document_root}は適宜自分の環境に置き換えてください。

    xhprof_enable(); // プロファイリング開始
    
    //
    // ** プロファイリングしたい処理をここに記述 **
    //
    
    $xhprof_data = xhprof_disable();    //stop profiler
     
    //  プロファイリングページへのリンクを追加
    include_once "{document_root}/xhprof_lib.php";
    include_once "{document_root}/xhprof_runs.php";
    $xhprof_runs = new XHProfRuns_Default();
    $run_id = $xhprof_runs->save_run($xhprof_data, $XHPROF_SOURCE_NAME);
    echo "<a href=\"http://{document_root}/xhprof_html/index.php?run=$run_id&source=$XHPROF_SOURCE_NAME\">xhprof Result</a>\n";
    

    結果

    実行すると結果ページヘのリンクが表示されます。クリックすることで結果の詳細を確認することができます。

    おぉー参照できましたね。なかなかにいいかんじです。

  • 各項目
  • 項目 意味
    Function Name 関数名
    Calls プロファイリングした期間でコールされた回数
    Calls% コール回数のパーセント表示
    Incl. Wall Time 処理にかかった時間のうち、その関数が消費した時間。なお処理をネストした場合全経過時間を含むことになるので、あまり参考することはないと思われる
    IWall% Incl. Wall Timeのパーセント表示
    Excl. Wall Time 実際にその関数のみが消費した時間。多くの場合はこの項目なんかを参照しながらプロファイリングすることになると思われる
    IWall% Execl. Wall Timeのパーセント表示

    なるほどかなり正確な情報が把握できて、最終的にボトルネックになっている箇所が確認できます。
    実際に動かしてみると、当然ですが大きく時間を消費していたのがネットワーク処理、ついでファイル処理などかなり実用的なレベルで参考することができそうです。

    余談

    ちなみに結構長い間フリーランスしてる同僚と、プロファイリングからの性能検証について雑談してて。
    現実的な現場感としては、こういったプロファイリングを常に実行してボトルネックをチェックすることが多いのかな。ということについて話してると、現実的には実際に何か問題が発覚してから調査する。ってのが多いようです。

    あとは細かな性能検証も大事なんですが、昔と比べてマシンのスペックが格段に向上しているのでアプリのチューニングを突き詰めていくよりかは、マシンを追加してインフラ面からパワーを上げて対応する事が多いです。
    (もちろん普通にボトルネックになっているような場合は除く。アプリのチューニングが不要と言っているわけではないです。)

    実際その通りで、僕もどちらかと言うと小難しいコンパクトなコードを書くよりかは、コストが多少高くつくとしても可読性の高いコードを書くように心がけています。
    (よくある普通のレベルの企業では)あまり出番の多いわけではさそうですが、覚えておくと絶対役に立ちますね。
    個人的には常にこのへんの意識は頭のなかにあるエンジニアでいたいです。


    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木のキーと値を全部読み取るような振る舞いをしている。と仮定できる。


    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を利用すると良さそうですね。