🐥

JavaScriptで巨大な静的配列を実装するときはswitchよりもarrayを使ったほうが普通にはやい

に公開
1

動機

JSで事前に値が決まっている巨大な静的配列を実装するときに、arrayじゃなくてswitchの方が早いと思ったのでベンチをとった。DenoでやったのでV8のベンチになる。

実験

ベンチマークを作るスクリプト。

await Deno.remove("array_switch.ts");
const Ns = [100, 1000, 10000];
for (const N of Ns) {
  const array_banch: string = `const arr${N} = [${Array.from(
    { length: N },
    (_, i) => i
  ).join(",")}];
const get_val_array${N} = (i: number) => {
    return arr${N}[i];
};
Deno.bench(
    "array_${N}",
    () => {
      for (let i = 0; i < 10000; i++) {
        get_val_array${N}(i);
      }
}
   )`;
  const switch_bench: string = `
const get_val_switch${N} = (i: number) => {
    switch (i) {
      ${Array.from({ length: N }, (_, i) => `case ${i}: return ${i};`).join(
        "\n"
      )}
    }}
Deno.bench(
      "switch_${N}",
      () => {
        for (let i = 0; i < 10000; i++) {
          get_val_switch${N}(i);
        }
    })
`;
  Deno.writeTextFile(`array_switch.ts`, array_banch, {
    append: true,
  });
  Deno.writeTextFile(`array_switch.ts`, switch_bench, {
    append: true,
  });
}

ベンチの実行結果

benchmark      time/iter (avg)        iter/s      (min … max)           p75      p99     p995
-------------- ----------------------------- --------------------- --------------------------
array_100               4.8 µs       207,000 (  4.7 µs …   5.1 µs)   4.9 µs   5.1 µs   5.1 µs
switch_100             17.8 µs        56,030 (  8.2 µs … 322.9 µs)  16.3 µs  59.3 µs  76.8 µs
array_1000              5.0 µs       198,100 (  4.6 µs …   1.8 ms)   4.7 µs   8.4 µs  20.9 µs
switch_1000            60.5 µs        16,530 ( 55.5 µs … 437.3 µs)  57.7 µs 121.3 µs 149.7 µs
array_10000             4.6 µs       218,000 (  4.4 µs …   5.2 µs)   4.6 µs   5.2 µs   5.2 µs
switch_10000          257.6 µs         3,881 (202.7 µs …   3.0 ms) 257.3 µs 480.0 µs 585.6 µs

感想

動的なarrayよりも静的なswitchの方が早いと思ったが、結果は逆だった。arrayは定数時間で要素にアクセスできるのに対して、switchはほぼ要素数に比例してアクセス時間が伸びているように見える。

Discussion

dameyodamedamedameyodamedame

gcc11のC++でarrayとswitchがどういうコードに落ちるか検証してみました。

hoge.cpp
static const int ar[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99};
int func_array(int idx) {
    return ar[idx];
}
int func_switch(int idx) {
    switch (idx) {
        case 0: return 0;
        case 1: return 1;
        case 2: return 2;
        case 3: return 3;
        case 4: return 4;
        case 5: return 5;
        case 6: return 6;
        case 7: return 7;
        case 8: return 8;
        case 9: return 9;
        case 10: return 10;
        case 11: return 11;
        case 12: return 12;
        case 13: return 13;
        case 14: return 14;
        case 15: return 15;
        case 16: return 16;
        case 17: return 17;
        case 18: return 18;
        case 19: return 19;
        case 20: return 20;
        case 21: return 21;
        case 22: return 22;
        case 23: return 23;
        case 24: return 24;
        case 25: return 25;
        case 26: return 26;
        case 27: return 27;
        case 28: return 28;
        case 29: return 29;
        case 30: return 30;
        case 31: return 31;
        case 32: return 32;
        case 33: return 33;
        case 34: return 34;
        case 35: return 35;
        case 36: return 36;
        case 37: return 37;
        case 38: return 38;
        case 39: return 39;
        case 40: return 40;
        case 41: return 41;
        case 42: return 42;
        case 43: return 43;
        case 44: return 44;
        case 45: return 45;
        case 46: return 46;
        case 47: return 47;
        case 48: return 48;
        case 49: return 49;
        case 50: return 50;
        case 51: return 51;
        case 52: return 52;
        case 53: return 53;
        case 54: return 54;
        case 55: return 55;
        case 56: return 56;
        case 57: return 57;
        case 58: return 58;
        case 59: return 59;
        case 60: return 60;
        case 61: return 61;
        case 62: return 62;
        case 63: return 63;
        case 64: return 64;
        case 65: return 65;
        case 66: return 66;
        case 67: return 67;
        case 68: return 68;
        case 69: return 69;
        case 70: return 70;
        case 71: return 71;
        case 72: return 72;
        case 73: return 73;
        case 74: return 74;
        case 75: return 75;
        case 76: return 76;
        case 77: return 77;
        case 78: return 78;
        case 79: return 79;
        case 80: return 80;
        case 81: return 81;
        case 82: return 82;
        case 83: return 83;
        case 84: return 84;
        case 85: return 85;
        case 86: return 86;
        case 87: return 87;
        case 88: return 88;
        case 89: return 89;
        case 90: return 90;
        case 91: return 91;
        case 92: return 92;
        case 93: return 93;
        case 94: return 94;
        case 95: return 95;
        case 96: return 96;
        case 97: return 97;
        case 98: return 98;
        case 99: return 99;
    }
    return -1;
}
int func_switch_rand(int idx) {
    switch (idx) {
        case 0: return 89;
        case 1: return 35;
        case 2: return 6;
        case 3: return 46;
        case 4: return 79;
        case 5: return 57;
        case 6: return 5;
        case 7: return 52;
        case 8: return 39;
        case 9: return 35;
        case 10: return 43;
        case 11: return 2;
        case 12: return 38;
        case 13: return 75;
        case 14: return 84;
        case 15: return 57;
        case 16: return 22;
        case 17: return 70;
        case 18: return 49;
        case 19: return 35;
        case 20: return 24;
        case 21: return 46;
        case 22: return 6;
        case 23: return 38;
        case 24: return 47;
        case 25: return 76;
        case 26: return 34;
        case 27: return 90;
        case 28: return 97;
        case 29: return 12;
        case 30: return 64;
        case 31: return 1;
        case 32: return 37;
        case 33: return 0;
        case 34: return 2;
        case 35: return 51;
        case 36: return 12;
        case 37: return 43;
        case 38: return 41;
        case 39: return 76;
        case 40: return 77;
        case 41: return 27;
        case 42: return 55;
        case 43: return 16;
        case 44: return 3;
        case 45: return 38;
        case 46: return 28;
        case 47: return 91;
        case 48: return 48;
        case 49: return 45;
        case 50: return 15;
        case 51: return 32;
        case 52: return 96;
        case 53: return 11;
        case 54: return 25;
        case 55: return 32;
        case 56: return 25;
        case 57: return 19;
        case 58: return 1;
        case 59: return 68;
        case 60: return 89;
        case 61: return 64;
        case 62: return 42;
        case 63: return 35;
        case 64: return 38;
        case 65: return 3;
        case 66: return 61;
        case 67: return 54;
        case 68: return 49;
        case 69: return 41;
        case 70: return 2;
        case 71: return 60;
        case 72: return 42;
        case 73: return 94;
        case 74: return 60;
        case 75: return 29;
        case 76: return 95;
        case 77: return 5;
        case 78: return 78;
        case 79: return 1;
        case 80: return 34;
        case 81: return 51;
        case 82: return 7;
        case 83: return 63;
        case 84: return 35;
        case 85: return 93;
        case 86: return 94;
        case 87: return 15;
        case 88: return 95;
        case 89: return 46;
        case 90: return 49;
        case 91: return 15;
        case 92: return 93;
        case 93: return 71;
        case 94: return 13;
        case 95: return 12;
        case 96: return 75;
        case 97: return 66;
        case 98: return 1;
        case 99: return 10;
    }
    return -1;
}
$ g++ -c -g -O3 -Wall -pedantic -std=c++17 hoge.cpp
$ objdump -SC -M intel hoge.o

hoge.o:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

0000000000000000 <func_array(int)>:
static const int ar[] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99};
int func_array(int idx) {
   0:   f3 0f 1e fa             endbr64 
    return ar[idx];
   4:   48 63 ff                movsxd rdi,edi
   7:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]        # e <func_array(int)+0xe>
   e:   8b 04 b8                mov    eax,DWORD PTR [rax+rdi*4]
}
  11:   c3                      ret    
  12:   66 66 2e 0f 1f 84 00    data16 cs nop WORD PTR [rax+rax*1+0x0]
  19:   00 00 00 00 
  1d:   0f 1f 00                nop    DWORD PTR [rax]

0000000000000020 <func_switch(int)>:
int func_switch(int idx) {
  20:   f3 0f 1e fa             endbr64 
  24:   83 ff 63                cmp    edi,0x63
  27:   b8 ff ff ff ff          mov    eax,0xffffffff
  2c:   0f 46 c7                cmovbe eax,edi
        case 97: return 97;
        case 98: return 98;
        case 99: return 99;
    }
    return -1;
}
  2f:   c3                      ret    

0000000000000030 <func_switch_rand(int)>:
int func_switch_rand(int idx) {
  30:   f3 0f 1e fa             endbr64 
  34:   b8 ff ff ff ff          mov    eax,0xffffffff
  39:   83 ff 63                cmp    edi,0x63
  3c:   77 0d                   ja     4b <func_switch_rand(int)+0x1b>
  3e:   89 ff                   mov    edi,edi
  40:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]        # 47 <func_switch_rand(int)+0x17>
  47:   0f be 04 38             movsx  eax,BYTE PTR [rax+rdi*1]
        case 97: return 66;
        case 98: return 1;
        case 99: return 10;
    }
    return -1;
}
  4b:   c3                      ret

結論から言うと、(最大限最適化をしていますが)func_switch(int)は、実質引数をそのまま返すreturn idx;というコードに落ちていて、比較になりませんでした。なので、func_switch_rand(int)を作成し、昇順ではなく適当に乱数生成した定数を返す形に書き換えたところ、結局func_array(int)とほぼ同じコードに落ちていました。

JITコンパイルを行うV8ではそこまでの最適化は出来ないと思いますが、最初の予想として、最適化すれば同程度、最悪switchはif~else相当になるだろうというくらいが普通だと個人的には思います。

実際にif~elseにしたら桁違いに遅くなったので、その中間くらいのコードに落ちているのではないかと推測しています。

hoge.ts
--- a/hoge.ts
+++ b/hoge.ts
@@ -1,5 +1,5 @@
 await Deno.remove("array_switch.ts");
-const Ns = [100, 1000, 10000];
+const Ns = [100, 1000];
 for (const N of Ns) {
   const array_banch: string = `const arr${N} = [${Array.from(
     { length: N },
@@ -30,6 +30,23 @@ Deno.bench(
           get_val_switch${N}(i);
         }
     })
+`;
+  const if_bench: string = `
+const get_val_if${N} = (i: number) => {
+      ${
+        Array.from(
+          {length: N},
+          (_, i) => ((i !== 0) ? "else " : "") + `if (i === ${i}) return ${i};`
+        ).join("\n")
+      }
+    }
+Deno.bench(
+      "if_${N}",
+      () => {
+        for (let i = 0; i < ${N}; i++) {
+          get_val_if${N}(i);
+        }
+    })
 `;
   Deno.writeTextFile(`array_switch.ts`, array_banch, {
     append: true,
@@ -37,4 +54,7 @@ Deno.bench(
   Deno.writeTextFile(`array_switch.ts`, switch_bench, {
     append: true,
   });
+  Deno.writeTextFile(`array_switch.ts`, if_bench, {
+    append: true,
+  });
 }
benchmark     time/iter (avg)        iter/s      (min … max)           p75      p99     p995
------------- ----------------------------- --------------------- --------------------------
array_100             48.7 ns    20,520,000 ( 45.2 ns … 143.9 ns)  48.2 ns  73.3 ns  92.1 ns
switch_100           171.4 ns     5,834,000 (106.3 ns … 331.6 ns) 175.2 ns 185.0 ns 186.0 ns
if_100                 2.1 µs       487,300 (  2.0 µs …   2.3 µs)   2.1 µs   2.3 µs   2.3 µs
array_1000           427.1 ns     2,342,000 (418.0 ns … 573.6 ns) 425.3 ns 513.3 ns 573.6 ns
switch_1000           15.4 µs        65,020 ( 13.9 µs … 138.2 µs)  14.5 µs  38.3 µs  53.3 µs
if_1000              270.7 µs         3,694 (200.7 µs …   3.1 ms) 219.1 µs   1.9 ms   2.8 ms