奇特なブログ

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

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

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

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

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か否かは関係なく)で、
これらの書き方がされていないかどうかも注意すると良いかと思いますね。
で、もしライブラリやフレームワークでされていたら・・・改修しましょう(苦笑)
わりと本気で、ありそうで恐いですけど(苦笑)