Google Moog Synthesizerの衝撃
ちょっと前、googleのトップページがmoogのシンセもどきになっていたのは衝撃だった。
コードを見てみると、Flashで音を出しているわけではなく、基本すべてJavaScript上で行われているようだ。
ベースとなる波形を作っている部分はこんな感じ。前半がエンベロープの計算で、後半が鋸歯とかを作ってる。
そういえば、shader toyを見た時にも思ったのだけど、実はシンセサイザはJavaScriptで書くのが一番理に適うのでは。オシレータやらフィルタやらを柔軟に接続できるようプログラム上で工夫しなくとも、適当に数式を書いてevalすればJITでnativeに落とし込まれ実行されるとなれば、シンセの自由度では圧勝。速度面でも、JIT抜きのnativeのシンセサイザよりも、直接的に最終的な数式をnativeに落とせるJIT有りのJavaScriptが将来的には高速になる可能性がある。
コードを見てみると、Flashで音を出しているわけではなく、基本すべてJavaScript上で行われているようだ。
ベースとなる波形を作っている部分はこんな感じ。前半がエンベロープの計算で、後半が鋸歯とかを作ってる。
- 1var Db = function(a, b, c, d, e) {
- 2 var b = b.outputBuffer, f = b.getChannelData(1), h = b.getChannelData(0);
- 3 a.G && !a.c && (a.c = new Float32Array(b.length));
- 4 var m; m = a.p;
- 5 a.e || (m = 0);
- 6 m = 55 * a.w * Math.pow(2, (m + -4) / 12);
- 7 var p; p = Cb;
- 8 a.e || (p = 8);
- 9 p = Math.abs(a.m * (p - 1)) + 1;
- 10 p = 0 <= a.m ? p : 1 / p;
- 11 for (var k, z, r, t = 0; t < b.length; ++t) {
- 12 k = a.R;
- 13 switch (k.b) {
- 14 case 0:
- 15 k.a += k.m;
- 16 1 <= k.a && (k.a = 1, k.b = 1);
- 17 break;
- 18 case 1:
- 19 k.a -= k.l;
- 20 k.a <= k.e && (k.a = k.e, k.b = 2);
- 21 break;
- 22 case 3:
- 23 k.a -= k.h, 0 >= k.a && (k.a = 0, k.b = 4)
- 24 }
- 25 k = k.a;
- 26 var A = a;
- 27 r = p;
- 28 var $a = a, w;
- 29 w = a;
- 30 var Da = m;
- 31 !w.l || w.a === j || 0 >= w.h || Math.abs(w.a - Da) <= Math.abs(w.k) ? w = w.a = w.n = Da : (w.n != Da && (w.k = (Da - w.a) / (w.h / w.g), w.n = Da), w.a += w.k, w = w.a);
- 32 r *= !c || !$a.va ? w : (0.75 * c[t] + 1.25) * w;
- 33 $a = 2 / r;
- 34 A.b > $a && (A.b -= $a);
- 35 r = A.b * r / 2;
- 36 A.b += A.g;
- 37 switch (a.t) {
- 38 case 0:
- 39 z = 4 * ((0.5 < r ? 1 - r : r) - 0.25);
- 40 break;
- 41 case 1:
- 42 z = (z = 0.5 > r) ? 4 * r - 1 : -2 * r + 1;
- 43 break;
- 44 case 2:
- 45 z = 2 * (r - 0.5);
- 46 break;
- 47 case 3:
- 48 z = -2 * (r - 0.5);
- 49 break;
- 50 case 4:
- 51 z = 0.5 > r ? 1 : -1;
- 52 break;
- 53 case 5:
- 54 z = r < 1 / 3 ? 1 : -1;
- 55 break;
- 56 case 6:
- 57 z = 0.25 > r ? 1 : -1
- 58 }
- 59 a.G && (a.c[t] = z * a.v);
- 60 k *= z * a.r;
- 61 0 == d ? h[t] = k : 1 == d ? h[t] += k : (h[t] = (h[t] + k) / e, f && (f[t] = h[t]))
- 62 }
- 63};
そういえば、shader toyを見た時にも思ったのだけど、実はシンセサイザはJavaScriptで書くのが一番理に適うのでは。オシレータやらフィルタやらを柔軟に接続できるようプログラム上で工夫しなくとも、適当に数式を書いてevalすればJITでnativeに落とし込まれ実行されるとなれば、シンセの自由度では圧勝。速度面でも、JIT抜きのnativeのシンセサイザよりも、直接的に最終的な数式をnativeに落とせるJIT有りのJavaScriptが将来的には高速になる可能性がある。
何も工夫しないループ
JavaScriptが最もシンセ向きな言語の可能性は、ある。それは分かった。じゃあ現状はどうなのか、v8で簡単にテストしてみた。
まずは、音の基本、sin波(の7次多項式近似)をレンダリングするコードで見てみよう。シンセでも、サイン波はパイプオルガンの音を作ったり、FM変調の基礎になったりする重要な波形です。ただし、ホントは-PIからPIの範囲でしか近似できないにも関わらずクリッピングしてないため、このコード自体の実用価値はないので悪しからず。
なお、使ったコマンドは
自分が使ったのはr11554のtrunkを
とりあえず、頭で思い描いていた理想的コードとは掛け離れた効率の悪いコードが出てきたー!
まず、全然定数を外に追い出せてなくね?
と思ったけど、floatは演算順序が変わると量子化誤差のせいで結果が変わるので、致し方ないのだった。gccでもこれは同じだけど、-funsafe-math-optimizationsと指定すれば、その辺り無視してくれる選択肢はある点で違う。
JSの場合はこういったケースは丁寧に予め人力で最適化しておくのが必須だと分かる。
次に、実際の計算以外のコストが高い。573以降はすべて型チェックのコストだと思う。v8はタイトループは型決め打ちでコードを生成して、決め打った型とは違うものが出てきたらdeoptimizationとやらを呼んで、コードを作り直すようだ。その場合、当然もの凄いロスが発生する。
最後に、687からの定数用のSSEレジスタを復帰させているところ。これ毎回やる必要があるのかな?call TransitionElementsSmiToDoubleが実行されると、レジスタの値が保証されないということだろうか。詳しく追ってないのでよく分かりません。
おまけ。SSEを使ってるけど、ベクトル化はしてないです。てことでfpuで計算するのと速度は変わんないと思う。あと、初めて知ったんだけど、SSEって浮動小数の積和演算ないんですね。こりゃびっくりだ。何考えてチップ設計したんだ?浮動小数だと複雑になっちゃうからかな。
まずは、音の基本、sin波(の7次多項式近似)をレンダリングするコードで見てみよう。シンセでも、サイン波はパイプオルガンの音を作ったり、FM変調の基礎になったりする重要な波形です。ただし、ホントは-PIからPIの範囲でしか近似できないにも関わらずクリッピングしてないため、このコード自体の実用価値はないので悪しからず。
これをv8に食わせるとタイトループはこんなコードになる。
- 1function bench(buf,len) {
- 2 for(var i = 0; i < len; i++) {
- 3 var w = 2 * Math.PI * i / 44100;
- 4 var x = w * 440;
- 5 buf[i] = x -x*x*x/2/3 + x*x*x*x*x/2/3/4/5 - x*x*x*x*x*x*x/2/3/4/5/6/7;
- 6 }
- 7}
- 8var size = 10000;
- 9var buf = new Array(size);
- 10bench(buf, size);
なお、使ったコマンドは
d8_g --print_code test.jsshell_gだとtyped arrayが使えないのでd8_gを使ったほうが良いっぽい。
自分が使ったのはr11554のtrunkを
scons d8 mode=debug verbose=on disassembler=onでビルドしたものです。
0xb7b37446 390 894dc0 mov [ebp+0xc0],ecx 0xb7b37449 393 3bca cmp ecx,edx 0xb7b3744b 395 0f8d49010000 jnl 730 (0xb7b3759a) 0xb7b37451 401 3b252c1a4d08 cmp esp,[0x84d1a2c] 0xb7b37457 407 0f82ea010000 jc 903 (0xb7b37647) 0xb7b3745d 413 817fff21cc0045 cmp [edi+0xff],0x4500cc21 ;; object: 0x4500cc21 <Map(elements=1)> 0xb7b37464 420 0f85dc2bad7e jnz 0x3660a046 ;; deoptimization bailout 7 0xb7b3746a 426 f20f2af9 cvtsi2sd xmm7,ecx 0xb7b3746e 430 f20f105db8 movsd xmm3,[ebp+0xb8] 0xb7b37473 435 f20f59df mulsd xmm3,xmm7 0xb7b37477 439 f20f5eda divsd xmm3,xmm2 0xb7b3747b 443 f20f59dc mulsd xmm3,xmm4 0xb7b3747f 447 0f28fb movaps xmm7,xmm3 0xb7b37482 450 f20f59fb mulsd xmm7,xmm3 0xb7b37486 454 f20f59fb mulsd xmm7,xmm3 0xb7b3748a 458 0f28d7 movaps xmm2,xmm7 0xb7b3748d 461 f20f5ed1 divsd xmm2,xmm1 0xb7b37491 465 f20f5ed5 divsd xmm2,xmm5 0xb7b37495 469 0f28e3 movaps xmm4,xmm3 0xb7b37498 472 f20f5ce2 subsd xmm4,xmm2 0xb7b3749c 476 f20f59fb mulsd xmm7,xmm3 0xb7b374a0 480 f20f59fb mulsd xmm7,xmm3 0xb7b374a4 484 0f28d7 movaps xmm2,xmm7 0xb7b374a7 487 f20f5ed1 divsd xmm2,xmm1 0xb7b374ab 491 f20f5ed5 divsd xmm2,xmm5 0xb7b374af 495 f20f5ed6 divsd xmm2,xmm6 0xb7b374b3 499 f20f1075b0 movsd xmm6,[ebp+0xb0] 0xb7b374b8 504 f20f5ed6 divsd xmm2,xmm6 0xb7b374bc 508 f20f58e2 addsd xmm4,xmm2 0xb7b374c0 512 f20f59fb mulsd xmm7,xmm3 0xb7b374c4 516 f20f59fb mulsd xmm7,xmm3 0xb7b374c8 520 f20f5ef9 divsd xmm7,xmm1 0xb7b374cc 524 f20f5efd divsd xmm7,xmm5 0xb7b374d0 528 f20f105588 movsd xmm2,[ebp+0x88] 0xb7b374d5 533 f20f5efa divsd xmm7,xmm2 0xb7b374d9 537 f20f5efe divsd xmm7,xmm6 0xb7b374dd 541 f20f105da0 movsd xmm3,[ebp+0xa0] 0xb7b374e2 546 f20f5efb divsd xmm7,xmm3 0xb7b374e6 550 f20f104da8 movsd xmm1,[ebp+0xa8] 0xb7b374eb 555 f20f5ef9 divsd xmm7,xmm1 0xb7b374ef 559 f20f5ce7 subsd xmm4,xmm7 0xb7b374f3 563 f20f11a570ffffff movsd [ebp+0xffffff70],xmm4 0xb7b374fb 571 89d8 mov eax,ebx 0xb7b374fd 573 8178ff21c50045 cmp [eax+0xff],0x4500c521 ;; object: 0x4500c521 <Map(elements=0)> 0xb7b37504 580 0f850c000000 jnz 598 (0xb7b37516) 0xb7b3750a 586 bb41c50045 mov ebx,0x4500c541 ;; object: 0x4500c541 <Map(elements=2)> 0xb7b3750f 591 89c2 mov edx,eax 0xb7b37511 593 e80ab5fdff call TransitionElementsSmiToDouble (0xb7b12a20) ;; debug: position 150 ;; code: BUILTIN 0xb7b37516 598 8b45c8 mov eax,[ebp+0xc8] 0xb7b37519 601 8178ff41c50045 cmp [eax+0xff],0x4500c541 ;; object: 0x4500c541 <Map(elements=2)> 0xb7b37520 608 0f852a2bad7e jnz 0x3660a050 ;; deoptimization bailout 8 0xb7b37526 614 8b4807 mov ecx,[eax+0x7] 0xb7b37529 617 8b500b mov edx,[eax+0xb] 0xb7b3752c 620 f6c201 test_b dl,0x1 0xb7b3752f 623 0f8528010000 jnz 925 (0xb7b3765d) 0xb7b37535 629 d1fa sar edx,1 0xb7b37537 631 8b5dc0 mov ebx,[ebp+0xc0] 0xb7b3753a 634 3bda cmp ebx,edx 0xb7b3753c 636 0f83182bad7e jnc 0x3660a05a ;; deoptimization bailout 9 0xb7b37542 642 f20f108d70ffffff movsd xmm1,[ebp+0xffffff70] 0xb7b3754a 650 660f2ec9 ucomisd xmm1,xmm1 0xb7b3754e 654 0f8b08000000 jpo 668 (0xb7b3755c) 0xb7b37554 660 f20f100d309d4c08 movsd xmm1,[0x84c9d30] 0xb7b3755c 668 f20f114cd907 movsd [ecx+ebx*8+0x7],xmm1 0xb7b37562 674 83c301 add ebx,0x1 0xb7b37565 677 89d9 mov ecx,ebx 0xb7b37567 679 89c3 mov ebx,eax 0xb7b37569 681 8b55cc mov edx,[ebp+0xcc] 0xb7b3756c 684 8b7dc4 mov edi,[ebp+0xc4] 0xb7b3756f 687 f20f104d80 movsd xmm1,[ebp+0x80] 0xb7b37574 692 f20f105598 movsd xmm2,[ebp+0x98] 0xb7b37579 697 f20f106590 movsd xmm4,[ebp+0x90] 0xb7b3757e 702 f20f10ad78ffffff movsd xmm5,[ebp+0xffffff78] 0xb7b37586 710 f20f107588 movsd xmm6,[ebp+0x88] 0xb7b3758b 715 f20f105da0 movsd xmm3,[ebp+0xa0] 0xb7b37590 720 f20f107da8 movsd xmm7,[ebp+0xa8] 0xb7b37595 725 e9acfeffff jmp 390 (0xb7b37446)
とりあえず、頭で思い描いていた理想的コードとは掛け離れた効率の悪いコードが出てきたー!
まず、全然定数を外に追い出せてなくね?
と思ったけど、floatは演算順序が変わると量子化誤差のせいで結果が変わるので、致し方ないのだった。gccでもこれは同じだけど、-funsafe-math-optimizationsと指定すれば、その辺り無視してくれる選択肢はある点で違う。
JSの場合はこういったケースは丁寧に予め人力で最適化しておくのが必須だと分かる。
次に、実際の計算以外のコストが高い。573以降はすべて型チェックのコストだと思う。v8はタイトループは型決め打ちでコードを生成して、決め打った型とは違うものが出てきたらdeoptimizationとやらを呼んで、コードを作り直すようだ。その場合、当然もの凄いロスが発生する。
最後に、687からの定数用のSSEレジスタを復帰させているところ。これ毎回やる必要があるのかな?call TransitionElementsSmiToDoubleが実行されると、レジスタの値が保証されないということだろうか。詳しく追ってないのでよく分かりません。
おまけ。SSEを使ってるけど、ベクトル化はしてないです。てことでfpuで計算するのと速度は変わんないと思う。あと、初めて知ったんだけど、SSEって浮動小数の積和演算ないんですね。こりゃびっくりだ。何考えてチップ設計したんだ?浮動小数だと複雑になっちゃうからかな。
定数を追い出すなど最適化したループ
前節の反省を生かし、手動で最適化してみる。
すると、実際の計算部分は大分スリムになった。
しかし型チェックは変わらず。
- 1function bench(buf,len) {
- 2 var c3 = -1.0/2/3;
- 3 var c5 = 1.0/2/3/4/5;
- 4 var c7 = -1.0/2/3/4/5/6/7;
- 5 var cw = 2.0 * Math.PI / 44100 * 440;
- 6 for(var i = 0; i < len; i++) {
- 7 var x = cw * i;
- 8 var x2 = x*x;
- 9 buf[i] = x * (1 + x2 * (c3 + x2 * (c5 + x2 * c7)));
- 10 }
- 11}
- 12var size = 10000;
- 13var buf = new Array(size);
- 14bench(buf, size);
すると、実際の計算部分は大分スリムになった。
しかし型チェックは変わらず。
0x59636d5b 635 897dc0 mov [ebp+0xc0],edi 0x59636d5e 638 3bfa cmp edi,edx 0x59636d60 640 0f8de7000000 jnl 877 (0x59636e4d) 0x59636d66 646 3b252c1a4d08 cmp esp,[0x84d1a2c] 0x59636d6c 652 0f8288010000 jc 1050 (0x59636efa) 0x59636d72 658 f20f2af7 cvtsi2sd xmm6,edi 0x59636d76 662 0f28fb movaps xmm7,xmm3 0x59636d79 665 f20f59fe mulsd xmm7,xmm6 0x59636d7d 669 0f28f7 movaps xmm6,xmm7 0x59636d80 672 f20f59f7 mulsd xmm6,xmm7 0x59636d84 676 0f28ce movaps xmm1,xmm6 0x59636d87 679 f20f59cc mulsd xmm1,xmm4 0x59636d8b 683 f20f1055b8 movsd xmm2,[ebp+0xb8] 0x59636d90 688 f20f58d1 addsd xmm2,xmm1 0x59636d94 692 0f28ce movaps xmm1,xmm6 0x59636d97 695 f20f59ca mulsd xmm1,xmm2 0x59636d9b 699 f20f1055b0 movsd xmm2,[ebp+0xb0] 0x59636da0 704 f20f58d1 addsd xmm2,xmm1 0x59636da4 708 f20f59f2 mulsd xmm6,xmm2 0x59636da8 712 0f28cd movaps xmm1,xmm5 0x59636dab 715 f20f58ce addsd xmm1,xmm6 0x59636daf 719 0f28d7 movaps xmm2,xmm7 0x59636db2 722 f20f59d1 mulsd xmm2,xmm1 0x59636db6 726 f20f115590 movsd [ebp+0x90],xmm2 0x59636dbb 731 89d8 mov eax,ebx 0x59636dbd 733 8178ff21c5a043 cmp [eax+0xff],0x43a0c521 ;; object: 0x43a0c521 <Map(elements=0)> 0x59636dc4 740 0f850c000000 jnz 758 (0x59636dd6) 0x59636dca 746 bb41c5a043 mov ebx,0x43a0c541 ;; object: 0x43a0c541 <Map(elements=2)> 0x59636dcf 751 89c2 mov edx,eax 0x59636dd1 753 e84abcfdff call TransitionElementsSmiToDouble (0x59612a20) ;; debug: position 406 ;; code: BUILTIN 0x59636dd6 758 8b45d8 mov eax,[ebp+0xd8] 0x59636dd9 761 8178ff41c5a043 cmp [eax+0xff],0x43a0c541 ;; object: 0x43a0c541 <Map(elements=2)> 0x59636de0 768 0f858832cde7 jnz 0x4130a06e ;; deoptimization bailout 11 0x59636de6 774 8b4807 mov ecx,[eax+0x7] 0x59636de9 777 8b500b mov edx,[eax+0xb] 0x59636dec 780 f6c201 test_b dl,0x1 0x59636def 783 0f851b010000 jnz 1072 (0x59636f10) 0x59636df5 789 d1fa sar edx,1 0x59636df7 791 8b5dc0 mov ebx,[ebp+0xc0] 0x59636dfa 794 3bda cmp ebx,edx 0x59636dfc 796 0f837632cde7 jnc 0x4130a078 ;; deoptimization bailout 12 0x59636e02 802 f20f104d90 movsd xmm1,[ebp+0x90] 0x59636e07 807 660f2ec9 ucomisd xmm1,xmm1 0x59636e0b 811 0f8b08000000 jpo 825 (0x59636e19) 0x59636e11 817 f20f100d309d4c08 movsd xmm1,[0x84c9d30] 0x59636e19 825 f20f114cd907 movsd [ecx+ebx*8+0x7],xmm1 0x59636e1f 831 83c301 add ebx,0x1 0x59636e22 834 89df mov edi,ebx 0x59636e24 836 8b75d4 mov esi,[ebp+0xd4] 0x59636e27 839 89c3 mov ebx,eax 0x59636e29 841 8b55dc mov edx,[ebp+0xdc] 0x59636e2c 844 8b4de0 mov ecx,[ebp+0xe0] 0x59636e2f 847 f20f1065a0 movsd xmm4,[ebp+0xa0] 0x59636e34 852 f20f105da8 movsd xmm3,[ebp+0xa8] 0x59636e39 857 f20f1055b0 movsd xmm2,[ebp+0xb0] 0x59636e3e 862 f20f104db8 movsd xmm1,[ebp+0xb8] 0x59636e43 867 f20f106d98 movsd xmm5,[ebp+0x98] 0x59636e48 872 e90effffff jmp 635 (0x59636d5b)
typed array導入
この型チェックは何とかならんのか、とFloat32Arrayを使ってみる。
おお!かなりまともになりました。理想にかなり近づいた。頭の悪いCコンパイラの出力と言っても過言ではない程度には良いです。動的型付け言語でここまでできれば上出来かと。
- 1function bench(buf,len) {
- 2 var c3 = -1.0/2/3;
- 3 var c5 = 1.0/2/3/4/5;
- 4 var c7 = -1.0/2/3/4/5/6/7;
- 5 var cw = 2.0 * Math.PI / 44100 * 440;
- 6 for(var i = 0; i < len; i++) {
- 7 var x = cw * i;
- 8 var x2 = x*x;
- 9 buf[i] = x * (1 + x2 * (c3 + x2 * (c5 + x2 * c7)));
- 10 }
- 11}
- 12var size = 10000;
- 13var buf = new Float32Array(size);
- 14bench(buf, size);
おお!かなりまともになりました。理想にかなり近づいた。頭の悪いCコンパイラの出力と言っても過言ではない程度には良いです。動的型付け言語でここまでできれば上出来かと。
0x5e036a00 640 3bc2 cmp eax,edx 0x5e036a02 642 0f8d6b000000 jnl 755 (0x5e036a73) 0x5e036a08 648 3b252c1a4d08 cmp esp,[0x84d1a2c] 0x5e036a0e 654 0f823b010000 jc 975 (0x5e036b4f) 0x5e036a14 660 f20f2ac8 cvtsi2sd xmm1,eax 0x5e036a18 664 0f28fb movaps xmm7,xmm3 0x5e036a1b 667 f20f59f9 mulsd xmm7,xmm1 0x5e036a1f 671 0f28cf movaps xmm1,xmm7 0x5e036a22 674 f20f59cf mulsd xmm1,xmm7 0x5e036a26 678 0f28d1 movaps xmm2,xmm1 0x5e036a29 681 f20f59d5 mulsd xmm2,xmm5 0x5e036a2d 685 f20f105dbc movsd xmm3,[ebp+0xbc] 0x5e036a32 690 f20f58da addsd xmm3,xmm2 0x5e036a36 694 0f28d1 movaps xmm2,xmm1 0x5e036a39 697 f20f59d3 mulsd xmm2,xmm3 0x5e036a3d 701 0f28dc movaps xmm3,xmm4 0x5e036a40 704 f20f58da addsd xmm3,xmm2 0x5e036a44 708 f20f59cb mulsd xmm1,xmm3 0x5e036a48 712 0f28d6 movaps xmm2,xmm6 0x5e036a4b 715 f20f58d1 addsd xmm2,xmm1 0x5e036a4f 719 f20f59fa mulsd xmm7,xmm2 0x5e036a53 723 3bc1 cmp eax,ecx 0x5e036a55 725 0f831d369def jnc 0x4da0a078 ;; deoptimization bailout 12 0x5e036a5b 731 f20f5ac7 cvtsd2ss xmm0,xmm7 0x5e036a5f 735 f30f110487 movss [edi+eax*4],xmm0 0x5e036a64 740 83c001 add eax,0x1 0x5e036a67 743 f20f105db4 movsd xmm3,[ebp+0xb4] 0x5e036a6c 748 f20f1055bc movsd xmm2,[ebp+0xbc] 0x5e036a71 753 eb8d jmp 640 (0x5e036a00)
gccの場合
では普通にCで書いてみましょう。
objdumpしたせいでちょっと分かりにくいけど、fldsやfadds,fsubsのoperandの即値はアドレスです。しかし、この定数値のロードはループの外に追い出せるんだがな…まだ甘い。
とはいえ、forから抜けるジャンプをv8は字面通りループの開始時に判定していたけど、gccはちゃんと後ろに持ってきてjmp命令を1個省くなど人間なら当然やるだろう最適化をやっている。偉い。
- 1void bench(float* buf, int len) {
- 2 float c3 = -1.0f/2/3;
- 3 float c5 = 1.0f/2/3/4/5;
- 4 float c7 = -1.0f/2/3/4/5/6/7;
- 5 float cw = 2.0f * M_PI / 44100 * 440;
- 6 for(int i = 0; i < len; i++) {
- 7 float x = cw * i;
- 8 float x2 = x*x;
- 9 buf[i] = x * (1 + x2 * (c3 + x2 * (c5 + x2 * c7)));
- 10 }
- 11}
g++ -O3 -c bench.c
18: 89 45 fc mov %eax,-0x4(%ebp) 1b: db 45 fc fildl -0x4(%ebp) 1e: d8 c9 fmul %st(1),%st 20: d9 c0 fld %st(0) 22: d8 c9 fmul %st(1),%st 24: d9 05 04 00 00 00 flds 0x4 2a: d8 c9 fmul %st(1),%st 2c: d8 05 08 00 00 00 fadds 0x8 32: d8 c9 fmul %st(1),%st 34: d8 25 0c 00 00 00 fsubs 0xc 3a: de c9 fmulp %st,%st(1) 3c: d8 05 10 00 00 00 fadds 0x10 42: de c9 fmulp %st,%st(1) 44: d9 1c 81 fstps (%ecx,%eax,4) 47: 83 c0 01 add $0x1,%eax 4a: 39 d0 cmp %edx,%eax 4c: 75 ca jne 18 <_Z5benchPfi+0x18>そりゃ普通に書いたらこうなるよね。単純かつ速い。
objdumpしたせいでちょっと分かりにくいけど、fldsやfadds,fsubsのoperandの即値はアドレスです。しかし、この定数値のロードはループの外に追い出せるんだがな…まだ甘い。
とはいえ、forから抜けるジャンプをv8は字面通りループの開始時に判定していたけど、gccはちゃんと後ろに持ってきてjmp命令を1個省くなど人間なら当然やるだろう最適化をやっている。偉い。
gcc with sse
同じコードを
なんと!4並列で実行してくれてる!
ループ回数が多い場合は間違いなく最速でしょう。
という、やっぱasm最強という当たり前すぎる帰着で終了。
g++ -O3 -msse2 -mfpmath=sse -c bench.cとするとどうなるか。
なんと!4並列で実行してくれてる!
ループ回数が多い場合は間違いなく最速でしょう。
120: 0f 5b cb cvtdq2ps %xmm3,%xmm1 123: 89 d0 mov %edx,%eax 125: 83 c2 01 add $0x1,%edx 128: c1 e0 04 shl $0x4,%eax 12b: 39 ca cmp %ecx,%edx 12d: 0f 59 ce mulps %xmm6,%xmm1 130: 66 0f fe df paddd %xmm7,%xmm3 134: 0f 28 d1 movaps %xmm1,%xmm2 137: 0f 59 d1 mulps %xmm1,%xmm2 13a: 0f 28 c2 movaps %xmm2,%xmm0 13d: 0f 59 c5 mulps %xmm5,%xmm0 140: 0f 58 c4 addps %xmm4,%xmm0 143: 0f 59 c2 mulps %xmm2,%xmm0 146: 0f 5c 05 40 00 00 00 subps 0x40,%xmm0 14d: 0f 59 c2 mulps %xmm2,%xmm0 150: 0f 58 05 50 00 00 00 addps 0x50,%xmm0 157: 0f 59 c1 mulps %xmm1,%xmm0 15a: 0f 29 04 07 movaps %xmm0,(%edi,%eax,1) 15e: 72 c0 jb 120 <_Z5benchPfi+0x120>ただ、ここでは省きましたが、前後に端数となる部分をスカラーで計算するコードが入るため、全体としてかなり大きくなってオーバーヘッドも増えてます。シンセの場合定期的にエンベロープやLFO等の更新が必要なため、一度に大量にループすることはないことを考えると、オーバーヘッドを避けるために入力データは16byte align済みで4の倍数個決め打ちとして、asmで書いてしまうに越したことはないですね。
という、やっぱasm最強という当たり前すぎる帰着で終了。
まとめ
以上、シンセっぽいタイトループの最適化についてv8で簡単に調べて結果わかったこと。
- JavaScriptで高速化したいなら、数式は手動で最適化しておくこと
- 必ずtyped arrayを使うこと
- 簡単なループでもC(gcc)の出力と比べるとv8はまだまだ野暮ったい。つまりこれからまだまだ速くなるかも!?
長いループの場合途中からそうなりますが
bailoutがあるので最適化済みという認識です。
opt codeじゃなくて最適化前のfull codeだとツッコミを入れようがないくらい遅いコードが出てきた気がします。