今回は最適化の効果等について検証します。
世の中にはコードが短ければ速くなると思っている人がいますがJavaScriptはJITコンパイラが使われていることがほとんどなので計算段階での計算速度への影響はありません。
TL;DR
- 速度重視ならアロー関数は使うな
- for文で配列の長さを使うなら変数に保存しろ
- さほど大きくない数字の配列を扱うならUint8ArrayやInt16Array等を使え、ただし範囲はよく調べろ
- thisやクラスはスコープ内変数より遅い
計測方法
const checkcount = 10000;
const check = (n,fn) => {
const c = () => {
const start = performance.now();
fn();
const end = performance.now();
return end - start;
}
const res = [];
for(let i = 0;i < checkcount;i++)
res.push(c())
console.log(n+":"+res.reduce((p,c)=>p+=c)/checkcount)
}
performance.now()
で時間を取得して1万回行った平均を求めます。
環境
Intel(R) Core(TM) -5-7200U CPU @ 2.50GHz
Intel(R) HD Graphics 620
Microsoft Windows Pro 10 バージョン22H2 OSビルド19045.5247
nodejs v20.12.2
bun 1.1.38
Visual studio code
powershell
YouTubeとか見ながらしてたので多少左右します
1.匿名関数(noop)
内容のない関数二つの速度を比較します。
check("arrow noop",()=>{})
check("funct noop",function(){})
予想
thisの余計な計算がないからアロー関数の方が速そう
結果
arrow noop:0.0005066499999999692
funct noop:0.0001306200000000061
通常の関数の方が速かった...
考察
アロー関数はスコープをたどらないといけないため遅くなった?
BigIntでループ
bigintとnumberで1万回ループします
check("number loop",()=>{
for(let i = 0; i < 10000;i++);
})
check("bigint loop",()=>{
for(let i = 0n;i < 10000n;i++);
})
予想
numberと違ってBigIntは小数点がないから速そう
結果
number loop:0.006236490000000312
bigint loop:0.4420627400000003
BigIntのボロ負け
考察
BigIntはJIT内でループの最適化がされなかった?
それかBigIntが普通に遅い
配列の長さのキャッシュ
長さを保存してからループします
check("len cached",()=>{
const arr = new Array(10000);
const len = arr.length
for(let i = 0;i < len;i++);
})
check("len nocache",()=>{
const arr = new Array(10000);
for(let i = 0;i < arr.length;i++);
})
予想
どっかのサイトにこれで速くなるとあったし理屈が通ってるので速くなるね、言うまでもない。
結果
len cached:0.00830133999999707
len nocache:0.022141320000002726
速い!
考察
普通にlengthのゲッター処理があるから時間がかかる?
evalを使う
evalと通常表記で1万回のループをします
check("eval",()=>{
eval("for(let Z=0;Z<10000;Z++);")
})
check("native",()=>{
for(let i=0;i<10000;i++);
})
予想
evalごとにJITコンパイラかインタプリタが動くのでevalは遅いね、言うまでもない。
結果
eval:0.007514940000000057
native:0.004491899999999981
evalの方がちょっと遅い
考察
処理の使いまわしをしている?
var vs. let
var宣言とlet宣言で10万回ループします
check("var",()=>{
for(var i=0;i<100000;i++);
})
check("let",()=>{
for(let i=0;i<100000;i++);
})
予想
印象的にvarが遅そう
結果
var:0.0510033400000004
let:0.03656134999999887
何回か計測したけどletの方が遅いこともあった
考察
宣言方法に大差無し!
Uint8ArrayとArrayで10万回の代入
Uint8ArrayとArrayに1を代入します
check("uint8loop",()=>{
const arr = new Uint8Array(100000);
for(let i = 0;i < arr.length;i++) arr[i] = 1;
})
check("arrayloop",()=>{
const arr = new Array(100000);
for(let i = 0;i < arr.length;i++) arr[i] = 1;
})
予想
Uint8Arrayの方が機械に近くて速そう
結果
uint8loop:0.2944365700000023
arrayloop:0.8646147300000013
目に見えてUint8Arrayが速い!
考察
Uint8Arrayは型が確定しているか、機械に近いか。
三項演算子
三項演算子とIF文どちらが速いか
check("?:",()=>{
const arr = [];
for(let i = 0;i < 10000;i++) i % 2 ? arr.push(5) : arr.push(2);
})
check("if",()=>{
const arr = [];
for(let i = 0;i < 10000;i++)
if(i % 2) arr.push(5)
else arr.push(2)
})
予想
三項演算子の方が速そう
結果
?::0.056880590000000064
if:0.05327013000000045
微々たる差だがif文の方が速い!
考察
if文の方が機械に近い?
スコープとthis
スコープ内の変数によるループとクラス内のループで比較します。
ついでにプライベート変数も比較します。
check("scope",()=>{
function loopFactory(){
let i = 0;
return function(){
for(i = 0;i < 10000;i++);
}
}
const loop = loopFactory();
loop();
})
check("class",()=>{
class Loop{
i;
constructor(){
this.i = 0;
}
loop(){
for(this.i = 0;this.i < 10000;this.i++);
}
}
const loop = new Loop();
loop.loop();
})
check("classP",()=>{
class Loop{
#i;
constructor(){
this.#i = 0;
}
loop(){
for(this.#i = 0;this.#i < 10000;this.#i++);
}
}
const loop = new Loop();
loop.loop();
})
考察
thisを介さないといけないからクラスは遅いけどプライベート変数は外部に表示しないから比較的速い?
結果
scope:0.036733379999999934
class:0.2731350399999993
classP:0.45610956999999763
圧倒的スコープ
考察
thisを介すのはやっぱり遅いよね...
プライベート変数は特別な処理をするのかな?
node vs. bun
nodeとbun、どちらが速いかこれでもう一度確かめます。以下はここのすべてのコード比較コードです
const checkcount = 10000;
const check = (n,fn) => {
const c = () => {
const start = performance.now();
fn();
const end = performance.now();
return end - start;
}
const res = [];
for(let i = 0;i < checkcount;i++)
res.push(c())
console.log(n+":"+res.reduce((p,c)=>p+=c)/checkcount)
}
check("arrow noop",()=>{})
check("funct noop",function(){})
console.log("")
check("number loop",()=>{
for(let i = 0; i < 10000;i++);
})
check("bigint loop",()=>{
for(let i = 0n;i < 10000n;i++);
})
console.log("")
check("len cached",()=>{
const arr = new Array(10000);
const len = arr.length
for(let i = 0;i < len;i++);
})
check("len nocache",()=>{
const arr = new Array(10000);
for(let i = 0;i < arr.length;i++);
})
console.log("")
check("eval",()=>{
eval("for(let Z=0;Z<10000;Z++);")
})
check("native",()=>{
for(let i=0;i<10000;i++);
})
console.log("")
check("var",()=>{
for(var i=0;i<100000;i++);
})
check("let",()=>{
for(let i=0;i<100000;i++);
})
console.log("")
check("uint8loop",()=>{
const arr = new Uint8Array(100000);
for(let i = 0;i < arr.length;i++) arr[i] = 1;
})
check("arrayloop",()=>{
const arr = new Array(100000);
for(let i = 0;i < arr.length;i++) arr[i] = 1;
})
console.log("")
check("?:",()=>{
const arr = [];
for(let i = 0;i < 10000;i++) i % 2 ? arr.push(5) : arr.push(2);
})
check("if",()=>{
const arr = [];
for(let i = 0;i < 10000;i++)
if(i % 2) arr.push(5)
else arr.push(2)
})
結果
頑張ってテーブル化しました。きつい。
タイトル | nodejs | bun |
---|---|---|
arrow noop | 0.0002971100000000263 | 0.00023385000000004084 |
funct noop | 0.00017492000000002435 | 0.00007084999999999298 |
number loop | 0.005526530000000038 | 0.011351059999999987 |
bigint loop | 0.4051602199999979 | 0.7218548800000042 |
len cached | 0.007827849999996852 | 0.06264597000000012 |
len nocache | 0.013203849999999875 | 0.057115710000000125 |
eval | 0.004763229999995383 | 0.022002820000001703 |
native | 0.004406499999999687 | 0.013971630000003097 |
var | 0.03667173999999968 | 0.11217761000001501 |
let | 0.037452690000006714 | 0.11275581000001311 |
uint8loop | 0.2656331500000025 | 0.17104559999998656 |
arrayloop | 0.8199938500000077 | 0.45781148000002103 |
?: | 0.05362607000000017 | 0.10521647000000026 |
if | 0.052280720000000044 | 0.07787996000000007 |
scope | 0.036733379999999934 | 0.02870950999999985 |
class | 0.2731350399999993 | 0.019864589999999804 |
classP | 0.45610956999999763 | 1.3481609800000118 |
やっぱりbunは速い!と思ったけど所々遅い部分も。
集計ミスもあるかもだけどV8製nodejsとwebkit製bunだと最適化方法が違うのかな?