fc2ブログ

奇特なブログ

「殊勝に値する行いや心掛け」を意味する、奇特な人になる為のブログです

値渡しで値を変えると処理速度が激遅になる時がある

久々の技術ネタです。
いや、ネタは沢山あるんですけど、書くのが難しいのが多くて(苦笑)
あと、この記事は特になんですけど、
ハッキリしない点が多いので、識者からのツッコミ大歓迎です。
よろしくお願いします。

では本題に入りますが、先に結論です。
以下の条件を「全て満たした」時に、処理速度が「ビックリするぐらい」遅くなります。
ただ、まだ結論が完全には出ていない(というか出せない)ので、
以下の条件以外でも起きるかもしれませんけど。
あと、以下の文中内の「関数」は「メソッド」に置き換えても通じます。

1.繰り返し文(for、foreach、whileのいずれか)の中で関数呼び出しをしている
2.関数への引数の渡し方が値渡しである
3.関数の中で値渡しされた引数の値が変更されている
4.3の引数または、3の引数と参照関係にある(イコール代入をしている)変数を戻り値で返していて且つ、関数の呼び元で戻り値を任意の変数に代入している
5.3の引数のデータ型が文字列か配列である

これに対して、処理速度低下を防ぐ対策としては、以下のいずれかです。
他にもあると思いますけど。

1.値が変わる変数をクラスのメンバとして宣言して処理を書く(要するにOOP。でも、変な書き方だと起きる)
2.関数内で引数の値を変えて戻り値を返している場合には、参照渡しにする。当然戻り値は書かない

対策1の詳細は、だいぶ下にある5に書きました。

ではここで、どういうコードだと上記の問題が発生するかを知るために、
一例として、以下の様なプログラムもどきを掲載します。
一応、実行環境を書いておくと、
OSがSlackware13.1で、PHPのバージョンは5.3.8です。

-------------------------------------------------------------------------------------------
<?php

function 関数A($配列B, $数値B) {
$配列B[$数値B] = $数値B;
return $配列B;
}

$配列A = 数値添字の開始値が0で且つ、要素数が百万個有って且つ、要素値が全て0の配列を作成する

$配列の要素数 = 配列の要素数を取得する

for ($数値A = 0; $数値A < $配列の要素数; $数値A++) {
$配列A = 関数A($配列A, $数値A);
}
-------------------------------------------------------------------------------------------

中~上級者はともかく、そんなに狂った様なコードには見えないと思いますがいかがでしょうか。
「期待した通りに動きます」し。
でも、このコードだと上記の条件全てに合致するので、処理速度が激遅になります。
ちなみに激遅がどのくらいかというと、
私の環境では配列だと約373倍、文字列だと約22倍遅くなりました。
逆に言えば、上記の条件を一つでも満たしていなければOKなので、
ここからは、各条件について詳しく見ていきます。
あと、読む前に以下のリンク先を読んでおいた方が良いかもしれません。

[PHP] 配列やオブジェクトの値渡しと参照渡し
http://screw-axis.com/2009/06/05/php-%E9%85%8D%E5%88%97%E3%82%84%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E5%80%A4%E6%B8%A1%E3%81%97%E3%81%A8%E5%8F%82%E7%85%A7%E6%B8%A1%E3%81%97/

[PHP] 高速化Tipsのオカルト(1) 関数への参照渡し
http://screw-axis.com/2009/07/04/php-performance-superstitions1-reference/

【PHP】参照渡しと値渡しの速度比較
http://se-suganuma.blogspot.com/2008/08/php.html

1.繰り返し文(for、foreach、whileのいずれか)の中で関数呼び出しをしている

どういうことかというと。
上記のプログラムだと、for文の「中で」関数呼び出しをしていますが、
それを「関数の中にfor文を書いて繰り返し処理をする」様に変更すると、
あ~らビックリ、処理速度が普通になります。
違いとしては、
(1)関数を呼ぶ回数が違う
(2)return文を実行する回数が違う
があります。
で、「ここからがイマイチ自信がない」のですが、
多分値渡しの時には、「関数を呼び出した時点」では、変更元の値はコピーされなくて、
「実際に値を変更しようとした時点」で、変更元の値がコピーされるのだと思われます。
で、このコピーに時間がかかる。
だから、関数呼び出しの回数が多いと、呼ぶ度に値のコピー処理が行われるから時間がかかり、
呼び出しの回数が例えば1回だと、コピー処理も1回しか行われないので時間がかからないのではと。
また、return文を実行する回数については。
実行する回数自体は関係なさそうですが、
その戻り値を任意の変数(参照関係にない他の変数でも)に代入した時には、時間がかかりました。
だから、「代入」がポイントだと思います。
まあ、こっちの場合でもコピーしているんでしょうかね。
とはいっても、returnするけど絶対代入しちゃダメっていうのも変な話ですけど。
なので、可能であれば(全て出来るとは限らないので)ですが、
「関数呼び出し回数を減らす様なコーディングを心がける」に加え、
以下の2以下の対策も合わせて行うで良いかなと。
そうすれば、当然returnする回数も減りますので。
当たり前じゃんって言われたら、そりゃそうですけど。

2.関数への引数の渡し方が値渡しである

値渡しが絶対にダメではないですけど、
関数内で引数の値を変えている場合には、
参照渡しにして戻り値を返さない(というかその必要がない)様にすれば良いのではと。
だからまあ、関数内の処理をよく見てみましょうとなりますかね。
ここは、この辺でいいかと思います。

3.関数の中で値渡しされた引数の値が変更されている

上記2で書いた通りです。
だから、値を変えないなら値渡しでも問題ないということで。

4.3の引数または、前述の引数と参照関係にある(イコール代入をしている)変数を戻り値で返していて且つ、関数の呼び元で戻り値を任意の変数に代入している

上記1で書いたreturn文の事です。
それに加え、以下の様な書き方でもNGでした。

$新たに宣言した変数 = $値渡しで引数に渡されて値を変えられた変数;
return $新たに宣言した変数;

ただこれは、ディープコピーをしても、
returnして代入している時点でどうでしょうね。
まあここは、そもそも参照渡しにして戻り値を返さなければ済む話ではあるのですが。

5.3の引数のデータ型が文字列か配列である

とりあえずハッキリ言えそうなのは、数値型(int)では起きませんでした。
あとそういえば、floatとbooleanを調べてませんでしたね。
でも、とりあえず今回は止めときます(苦笑)
で、原因ですが。
正直サッパリ分かりませんけど、
メモリの確保量の問題ではないかと。
数値型で「0」と「2147483647(整数型の最大値)」でもメモリの確保量は変わらない(そうか?)けど、
文字列で「'a'」と「'aaaaaa'」では確保量が変わる(配列の要素数でも同じ)からとか。
また、かなり上で書いた、クラスのメンバとして変数を書くについて。
例えば、クラスのメンバに文字列や配列があっても、セッター経由で値を変えるとかの、
文字列や配列を引数に「渡さない」且つ、
戻り値を「返さない」形で値を変更すれば、処理速度は普通でした。
変更された値の取得は、ゲッター経由で行えば良いわけですしね。
しかし、こんな所でもOOPが役立ちましたね。

長々と書いてきましたが、要するにOOPするか参照渡しで値を変更せよってことで。
あと最後に、ある意味最も重要な話。
上記の書き方について、自分が書いているソースで気をつけるのは勿論として、
自分が使っているライブラリやフレームワーク(OSSか否かは関係なく)で、
これらの書き方がされていないかどうかも注意すると良いかと思いますね。
で、もしライブラリやフレームワークでされていたら・・・改修しましょう(苦笑)
わりと本気で、ありそうで恐いですけど(苦笑)

テーマ:日記 - ジャンル:日記

  1. 2012/01/22(日) 22:17:55|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0

PHPのセッションの保存先と関数群について

php.iniの「session.save_handler」のデフォルト値って「files」だと思うのですが。
以下の様な事をしたい場合に、filesである事が問題になる様です。

Webサーバのクラスタリング

じゃあどうするの?って話になると思うのですが、

PHPのセッションを考える

にある通り、セッションの保存先は「DB」が良い様です。
セッション情報のやり取りをする度にDBアクセスが発生するので、
DBにどのくらいの負荷がかかっているかを意識する必要はありますが。

また、PHPが提供している「session」から始まる関数群は、
上記リンク先によると全然使えないと。
確かに筆者も、セッションがタイムアウトしたりしなかったりという、
不安定な挙動を経験した事があります。
単に、筆者のプログラムがバグっていただけかもしれませんが(苦笑)

なので、

自作のセッションライブラリが必要ではないかと思います(苦笑)


やれやれ。

テーマ:日記 - ジャンル:日記

  1. 2011/02/12(土) 02:05:04|
  2. PHP
  3. | トラックバック:0
  4. | コメント:0

パフォーマンスが上がるfor文の書き方

以下の様な2つの書き方、皆さんしていませんか?
1.for(i = 0; i < strlen(文字列変数); i++)
2.for(i = 0; i < count(配列変数); i++)
strlen(マニュアル)は文字列のバイト数(文字数ではない)をint型で返す関数で、
count(マニュアル)は配列の要素数などをint型で返す関数です。
詳細はマニュアルを参照して下さい。

で、実は上記の書き方。
ループを終了するかどうか判断する際に、「必ず」関数が実行されます。
なので、その分多くの処理が実行され、 結果としてコストがかかるわけです。
関数内でバイト数や要素数を求めている以上。

よって、上記の文字列変数や配列変数の中身を、
ループ内で編集したり「しない」ならば、
必ず実行するのは無意味というか無駄なわけです。
バイト数や要素数がループ内で増減するのであれば、
上記の書き方で良いのですが。

そこで、strlenなどを「for文の1行上に書く」という方法を提案します。
以下は、コストをマイクロミリ秒単位で計測したサンプルプログラムです。
環境はOSはFedora12で、PHPのバージョンは5.3.0です。

1.for文の中にstrlen関数を書いた場合

--------------------------------------------------------------------------------

<?php

function microtime_float() {
  list($usec, $sec) = explode(' ', microtime());
  return ((float)$usec + (float)$sec);
}

$time_start = microtime_float();

for($i = 0; $i < strlen('11'); $i++) {
}

$time_end = microtime_float();
$time = $time_end - $time_start;

echo $time . ' seconds' . "\n";

?>

結果例:
[root@www ~]# for i in 1 2 3 4 5 6 7 8 9 10; do php for_test_in.php; done
6.6995620727539E-5 seconds
6.6041946411133E-5 seconds
6.6041946411133E-5 seconds
6.4849853515625E-5 seconds
7.3909759521484E-5 seconds
6.4849853515625E-5 seconds
6.4849853515625E-5 seconds
7.5101852416992E-5 seconds
6.6041946411133E-5 seconds
7.1048736572266E-5 seconds
[root@www ~]#

--------------------------------------------------------------------------------

2.for文の外にstrlen関数を書いた場合

--------------------------------------------------------------------------------

<?php

function microtime_float() {
  list($usec, $sec) = explode(' ', microtime());
  return ((float)$usec + (float)$sec);
}

$time_start = microtime_float();

$length = strlen('11');

for($i = 0; $i < $length; $i++) {
}

$time_end = microtime_float();
$time = $time_end - $time_start;

echo $time . ' seconds' . "\n";

?>

結果例:
[root@www ~]# for i in 1 2 3 4 5 6 7 8 9 10; do php for_test_out.php; done
6.6041946411133E-5 seconds
6.6041946411133E-5 seconds
0.00010585784912109 seconds
0.0001070499420166 seconds
6.5088272094727E-5 seconds
6.5088272094727E-5 seconds
7.3909759521484E-5 seconds
7.6055526733398E-5 seconds
6.6041946411133E-5 seconds
8.2969665527344E-5 seconds
[root@www ~]#

--------------------------------------------------------------------------------

3.for文の中にcount関数を書いた場合

--------------------------------------------------------------------------------

<?php

function microtime_float() {
  list($usec, $sec) = explode(' ', microtime());
  return ((float)$usec + (float)$sec);
}

$array = array();

for($j = 0; $j < 2; $j++) {
  $k = strval($j);
  $array[$k] = $k;
}

$time_start = microtime_float();

for($i = 0; $i < count($array); $i++) {
}

$time_end = microtime_float();
$time = $time_end - $time_start;

echo $time . ' seconds' . "\n";

?>

結果例:
[root@www ~]# for i in 1 2 3 4 5 6 7 8 9 10; do php for_test_array_in.php; done
4.4107437133789E-5 seconds
4.4822692871094E-5 seconds
5.5074691772461E-5 seconds
7.1048736572266E-5 seconds
4.3869018554688E-5 seconds
5.9127807617188E-5 seconds
4.3869018554688E-5 seconds
4.4107437133789E-5 seconds
5.1975250244141E-5 seconds
4.3869018554688E-5 seconds
[root@www ~]#

--------------------------------------------------------------------------------

4.for文の外にcount関数を書いた場合

--------------------------------------------------------------------------------

<?php

function microtime_float() {
  list($usec, $sec) = explode(' ', microtime());
  return ((float)$usec + (float)$sec);
}

$array = array();

for($j = 0; $j < 2; $j++) {
  $k = strval($j);
  $array[$k] = $k;
}

$time_start = microtime_float();

$length = count($array);

for($i = 0; $i < $length; $i++) {
}

$time_end = microtime_float();
$time = $time_end - $time_start;

echo $time . ' seconds' . "\n";

?>

結果例:
[root@www ~]# for i in 1 2 3 4 5 6 7 8 9 10; do php for_test_array_out.php; done
4.1961669921875E-5 seconds
4.3869018554688E-5 seconds
4.5061111450195E-5 seconds
5.2213668823242E-5 seconds
6.8187713623047E-5 seconds
7.2002410888672E-5 seconds
4.2200088500977E-5 seconds
4.1961669921875E-5 seconds
4.1961669921875E-5 seconds
4.1961669921875E-5 seconds
[root@www ~]#

--------------------------------------------------------------------------------

で、上記4つのプログラムをそれぞれ100回実行した時の、
平均速度が以下です。

プログラムのパターン 100回実行時の平均速度
1.for文の中にstrlen関数を書いた場合 0.0000736秒
2.for文の外にstrlen関数を書いた場合 0.0000746369秒
3.for文の中にcount関数を書いた場合 0.0000503135秒
4.for文の外にcount関数を書いた場合 0.0000476432秒

微妙な結果です。
では、上記では「2」だったバイト数と要素数を、
それぞれ「100」に変更して実行した結果が以下です。

プログラムのパターン 100回実行時の平均速度
1.for文の中にstrlen関数を書いた場合 0.000111005秒
2.for文の外にstrlen関数を書いた場合 0.0000818014秒
3.for文の中にcount関数を書いた場合 0.00007671秒
4.for文の外にcount関数を書いた場合 0.000055秒

予想通り、for文の外に書いた方がコストがかからないという結果が出ました。

結果はほんの僅かの差ですが、
システムやアプリの性能改善で、
「色々チューニングしてみたけど、要望まであとちょっと足りない!」などの時に、
やってみるのは如何でしょうか。
特にcountの方は要素数100以上などはザラにあるでしょうし、
for文自体、頻繁に使うものですし。
塵も積もれば山となるとも言いますしね。
とはいえ、デグレが恐いのも確かです。
なので、プログラムを書く際に「習慣的」にやっておけば、
「少なくても、for文の部分に関してはチューニングの余地無し」となり、
精神的な心配からは開放されるのではないかと思います(笑)

他言語は現時点では不明です。
JavaScriptだと、文字列or配列のlengthプロパティなど。
Javaだと、Stringクラスのlengthメソッド、配列のlength属性、
Collectionインターフェースのsizeメソッドなどが対象でしょうか。
取り上げるかどうかは・・・不明ですが。

テーマ:日記 - ジャンル:日記

  1. 2010/07/25(日) 18:47:20|
  2. PHP
  3. | トラックバック:0
  4. | コメント:3
前のページ