動機
リファクタリングの本を読んでいるとき、5行ルールというものが紹介されていました。
詳細についてはこちらの本を読んでいただきたいですが、ざっくりいうと、1つの関数にあれやこれやと詰め込むのを避けましょうということだった気がします。
そのとき、このような疑問が浮かびました。
「サブルーチンが増えるとパフォーマンスが悪化しないか・・・?」
サブルーチンの呼び出しには、当然オーバーヘッドがかかるはずです。これは無視できるほど小さいのでしょうか?
実際に計測して確かめてみようと思います。
計測
巨大な関数(サブルーチンなし)
function main() {
let result = 0
for(let i = 0; i <= 1000; i++) {
result += i;
}
console.log(result);
}
console.time('test')
main();
console.timeEnd('test')
分割された関数
function sub() {
let result = 0
for(let i = 0; i <= 1000; i++) {
result += i;
}
return result;
}
function main() {
const result = sub();
console.log(result);
}
console.time('test');
main();
console.timeEnd('test');
実行結果
Chrome開発者ツールのコンソールに、上記のコードを貼り付けて実行してみました。
| 計測結果 | 巨大な関数 | 分割された関数 |
|---|---|---|
| 1回目 | 0.2431640625 ms | 0.281982421875 ms |
| 2回目 | 0.135009765625 ms | 0.283935546875 ms |
| 3回目 | 0.175048828125 ms | 0.326904296875 ms |
| 4回目 | 0.212890625 ms | 0.15283203125 ms |
| 5回目 | 0.1669921875 ms | 0.33984375 ms |
| 平均 | 0.18662109375 ms | 0.277099609375 ms |
5回実行した平均値を取ってみると、分割された関数の方が、0.1msほど遅いことが確認できました。
とはいえ、2つの差は誤差程度でしかありませんね。
サブルーチンを増やしてみる
上記の差分がサブルーチンによって起きているのなら、もっとサブルーチンを呼び出せば、差は広がっていくはずです。
これを検証してみたいと思います。
巨大な関数(サブルーチンなし)
function main() {
for(let i = 0; i <= 100; i++) {
let result = 0
for(let j = 0; j <= 1000; j++) {
result += j;
}
}
}
console.time('test')
main();
console.timeEnd('test')
分割された関数
function sub() {
let result = 0
for(let j = 0; j <= 1000; j++) {
result += j;
}
}
function main() {
for(let i = 0; i <= 100; i++) {
sub();
}
}
console.time('test')
main();
console.timeEnd('test')
実行結果
| 計測結果 | 巨大な関数 | 分割された関数 |
|---|---|---|
| 1回目 | 1.380126953125 ms | 0.98486328125 ms |
| 2回目 | 1.779052734375 ms | 0.7509765625 ms |
| 3回目 | 1.277099609375 ms | 0.815185546875 ms |
| 4回目 | 1.10693359375 ms | 0.94091796875 ms |
| 5回目 | 1.1201171875 ms | 1.724853515625 ms |
| 平均 | 1.332666015625 ms | 1.043359375 ms |
・・・サブルーチンがあるほうが早い?
理由の考察
JavaScriptのJITが最適化を行った結果、分割された関数の方が速くなったと考えられます。
今回はChrome開発者ツールのコンソールを使って計測を行ったので、JS実行環境としてV8が使われています。詳細は省きますが、V8では、同様のコードが繰り返し実行されている場合、最適化が行われることがあります。最適化されたコードは最適化されていないコードと比べると実行時間が短いです。
サブルーチンとして定義していたsub()は、短時間のうちに、全く同じ役割で何度も呼び出されています。この過程で最適化が行われ、短い時間で実行することができるようになったのだと考えられます。
まとめ
1つの関数を短くすることは、可読性の側面が大きいのかなと思っていましたが、試してみたことで、処理速度の面でも効果があるかもしれないという示唆を得ることができました。
手を動かしてみることは大事ですね!