+===========+ |P/ECE研究室| +===========+ P/ECE研究記録 2017年 ==================== * Wed Aug 30 02:13:02 JST 2017 Naoyuki Sawa - gcc33.exeがswitch〜caseを無駄に大きなジャンプテーブルに展開する □問題点 P/ECE開発環境のgcc33.exeは、switch〜caseを、無駄に大きなジャンプテーブルに展開する場合が有ります。 caseの値がある程度狭い範囲に収まっていて,尚且つ,飛び飛びである場合に、そうなる事が多いです。 たとえば、以下の例のように、ASCII文字の一部の文字に対して、switch〜caseで処理する場合に、そうなる事が多いです。 □C言語ソース │ int test(char c) { │ int n; │ switch(c) { │ default: n = 0; break; │ case '0': n = 1; break; │ case 'A': n = 2; break; │ case 'B': n = 2; break; │ case 'a': n = 3; break; │ case 'b': n = 3; break; │ } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ xsub %r12,%r12,48 │ ld.b %r10,%r12 │ xcmp %r10,50 │ xjrugt __L72 │ xsll %r10,2 │ xld.w %r10,[%r10+__L78] │ jp %r10 │ .code │ .align 2 │ __L78: │ .word __L73 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L74 │ .word __L75 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L72 │ .word __L76 │ .word __L77 │ .code │ __L72: │ ld.w %r10,0 │ xjp __L71 │ __L73: │ xld.w %r10,1 │ xjp __L71 │ __L74: │ __L75: │ xld.w %r10,2 │ xjp __L71 │ __L76: │ __L77: │ xld.w %r10,3 │ __L71: │ ret 実行速度の最適化のためとは言え、上記のアセンブラコードは、いくらなんでも無駄が多過ぎます。 一見、そんなに大きくないように見えるかも知れませんが、機械語命令の行は1行2バイトであるのに対して、ジャンプテーブルの行は1行4バイトなので、見た目以上に大きくて無駄が多いです。 実際の所、特に高速化が必要な処理を除いて、ほとんどのswitch〜caseの処理は、if〜else ifで逐次比較しても充分な実行速度が得られる事が多いです。 だから、速度よりもサイズ節約を優先してアセンブラコードを生成して欲しいのですが、そうはなっていないようです。 ちなみに、上記のアセンブラコードは、「-O2」オプション(速度最適化)を指定してコンパイルしたものですが、試しに「-O1」オプション(サイズ最適化)を指定してみても同じコードが生成されました。 どうやら、P/ECE開発環境のgcc33.exeは、常に、サイズ節約よりも、速度を優先して最適化を行っているようです。 ×対策案1(失敗) 無駄に大きなジャンプテーブルが生成されるのを防ぐ方法を、考えてみました。 最初に考えた方法は、gccに、ジャンプテーブルを生成させないオプションを指定する方法です。 □「3 GCC Command Options - 3.18 Options for Code Generation Conventions」(https://gcc.gnu.org/onlinedocs/gcc-5.3.0/gcc/Code-Gen-Options.html)より引用: │-fno-jump-tables │ Do not use jump tables for switch statements even where it would be more efficient than other code generation strategies. │ This option is of use in conjunction with -fpic or -fPIC for building code that forms part of a dynamic linker and cannot reference the address of a jump table. │ On some targets, jump tables do not require a GOT and this option is not needed. まさに今回の目的にぴったりなオプションなのですが・・・残念ながら、使えませんでした。 どうやら、P/ECE開発環境のgcc33.exeは、バージョンが古いためか、「-fno-jump-tables」オプションに対応していないみたいです。 △対策案2(いまいち) 次に考えた方法は、switch〜caseを、if〜else ifに書き換える方法です。 □C言語ソース │ int test(char c) { │ int n; │ if(c == '0') { n = 1; } │ else if(c == 'A') { n = 2; } │ else if(c == 'B') { n = 2; } │ else if(c == 'a') { n = 3; } │ else if(c == 'b') { n = 3; } │ else { n = 0; } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ ld.b %r12,%r12 │ xcmp %r12,48 │ xjrne __L2 │ xld.w %r10,1 │ xjp __L3 │ __L2: │ xcmp %r12,65 │ xjreq __L12 │ xcmp %r12,66 │ xjrne __L6 │ __L12: │ xld.w %r10,2 │ xjp __L3 │ __L6: │ xcmp %r12,97 │ xjreq __L13 │ ld.w %r10,0 │ xcmp %r12,98 │ xjrne __L3 │ __L13: │ xld.w %r10,3 │ __L3: │ ret 生成されるアセンブラコードはこれで良いのですが・・・ 既存のソースの中の、switch〜caseで書かれている部分を、手作業でif〜else ifに書き換える手間の問題が有ります。 だいたい、C言語ソースの中でswitch〜caseを使っている理由は、速度の目的よりも、書き易さ,見易さのためにそうしている事が多いので、if〜else ifに書き換えると見辛くなったりエンパグの危険が有ります。 だから、この方法も望ましくありません。 ○対策案3(成功) なるべくswitch〜caseを書き換えずに、最小限の手間で対策出来る方法を見付けました。 □C言語ソース │ int test(char c) { │ int n; │ switch(c) { │ case CHAR_MIN: /* FALLTHRU */ //おまじない ←←←←←←←←←←←←←←←←この行を追加しただけ。 │ default: n = 0; break; │ case '0': n = 1; break; │ case 'A': n = 2; break; │ case 'B': n = 2; break; │ case 'a': n = 3; break; │ case 'b': n = 3; break; │ } │ return n; │ } □gcc33.exeが生成するアセンブラコード │ .code │ .align 1 │ .global test │ test: │ ld.b %r12,%r12 │ xld.w %r10,65 │ cmp %r12,%r10 │ xjreq __L75 │ xjrgt __L80 │ xcmp %r12,-128 │ xjreq __L73 │ xcmp %r12,48 │ xjreq __L74 │ xjp __L73 │ __L80: │ xld.w %r10,97 │ cmp %r12,%r10 │ xjreq __L77 │ xjrgt __L81 │ xcmp %r12,66 │ xjreq __L76 │ xjp __L73 │ __L81: │ xcmp %r12,98 │ xjreq __L78 │ __L73: │ ld.w %r10,0 │ xjp __L71 │ __L74: │ xld.w %r10,1 │ xjp __L71 │ __L75: │ __L76: │ xld.w %r10,2 │ xjp __L71 │ __L77: │ __L78: │ xld.w %r10,3 │ __L71: │ ret 「対策案2」のアセンブラコードに比べると、多少、無駄なコードが生成されていますが、元の無駄に大きなジャンプテーブルよりはだいぶんましなので、対策としてはこれで充分だと思います。 この方法で変更した点は、「default:」の上にダミーの「case CHAR_MIN:」を追加しただけです。 処理上は「default:」にまとめられるはずなので、元と同じコードが生成されても不思議ではないのですが、何故か上手く行って、ジャンプテーブルには展開されなくなるようです。 どうやら、gcc33.exeは、処理がまとめられるかどうかに関係無く、caseの値の中にある程度狭い範囲から外れる値(この場合はCHAR_MIN)が有ると、ジャンプテーブルに展開しなくなるようです。 最適化が弱いとも言えるのですが、今回はその特性を利用して、上手く、無駄に大きなジャンプテーブルに展開されるのを防ぐ事が出来ました。 ちなみに、今回は「case CHAR_MIN:」としましたが、この値には特に意味はありません。 既存のcaseの値として使用されておらず、他のcaseの値からある程度離れている値ならば、何でも構いません。 ただし、今回のケースで「case 1000:」や「case -1000:」等としてしまうと、「warning: case value out of range」という警告が出ます。 今回のケースでは、「switch(c)」の変数cがchar型なので、1000や-1000には成り得ないからです。 今回は、変数cの範囲内で、なるべく他のcaseの値から離れている値として、CHAR_MINにしました。 実際には、対策を行うソース毎に、適切な値を検討して、試してみて、生成されたアセンブラコードを見て、無駄に大きなジャンプテーブルが生成されていないかを確認して下さい。 * Mon Aug 14 08:16:30 JST 2017 Naoyuki Sawa - ドラム音色のノイズ ■問題点 P/ECE開発環境に付属している、音楽ライブラリのドラム音色は、一部の音色でノイズが鳴ります。 ノイズが鳴る音色は、「crush cymbal」と「synth tom (hi)」と「synth tom (mid)」です。 【ノイズが鳴る事を確認するMML】 │ ;ドラムパート用マクロ設定(C:\usr\PIECE\docs\TUTORIAL\AYAKA\sample.mmlからコピー) │ !!bg @6 V125 ?o4c ; bass drum │ !!sg @8 V120 ?o4c ; snare drum 1 │ !!Sg @7 V115 ?o4c ; snare drum 2 │ !!og @9 V90 ?o4c ; open H.H. │ !!hg @10 V90 ?o4c ; closeH.H. │ !!cg @11 V110 ?o4c ; crush cymbal │ !!tg @13 V110 ?o4c ; synth tom (hi) │ !!ug @14 V110 ?o4c ; synth tom (mid) │ !!vg @15 V110 ?o4c ; synth tom (low) │ !!Cg @16 V120 ?o4c ; clap │ │ F =1 T150 l1 L !bg!sg!Sg!og!hg!cg!tg!ug!vg!Cg 上記のMMLを再生すると、6音目の「crush cymbal」と7音目の「synth tom (hi)」と8音目の「synth tom (mid)」の、鳴り終わる辺りでブチッという音が鳴る事が確認出来ます。 これまで気付きませんでした・・・ 或いは、気付いていたけれど、実用上あまり問題にならないので、見過ごしていたかも知れません。 たいていドラムパートは1トラックだけで複数のドラム音を連続で鳴らすので、上記の音色のノイズが鳴る前に次のドラム音を鳴らしていれば、実質ノイズは鳴らないからです。 ■原因 でもやっぱり気になるので、原因を調べて修正する事にしました。 ドラム音色の配列データを見てみると、最後の方に、不自然な値が入っているようです。 http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/cymbd_h.png http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/tomh1_h.png http://www.piece-me.org/piece-lab/wavetbl_bug/1_before_fix_h/tomm1_h.png これらの不自然な値を削除すれば、きっと、ノイズは消えるはずです。 しかし、なぜ不自然な値が入っていたのかがわからないので、単純にq削除して良いものかどうか、少し迷います。 そこで、これらの不自然な値が何なのか、調べてみる事にしました。 まず、ドラム音色の配列データをバイナリデータに出力して、バイナリエディタで見てみました。 http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/cymbd_bin.png http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/tomh1_bin.png http://www.piece-me.org/piece-lab/wavetbl_bug/2_before_fix_bin/tomm1_bin.png う〜む、わかりません。 次に、(思い付きで)、ドラム音色の配列データの値を「+128」して、バイナリデータに出力して、バイナリエディタで見てみました。 http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/cymbd_bin_bias.png http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/tomh1_bin_bias.png http://www.piece-me.org/piece-lab/wavetbl_bug/3_before_fix_bin_bias/tomm1_bin_bias.png 文字列が見えました。 どうやら、WAVファイルのツール情報のチャンクのようです。 だいたい、原因が推測出来ました。 きっと、音楽ライブラリのドラム音色を作る時に、WAVファイルを手作業で切り出すか、簡易的なスクリプト等を使って、変換したのでは無いかと思います。 その時に、WAVファイルに含まれている、音データ以外の、ツール情報のチャンクを、(うっかり?)削除し忘れてしまったのではないかと思います。 ちなみに、さきほどなぜ、ドラム音色の配列データの値を「+128」するとツール情報のチャンクが見えたのかと言うと、 WAVファイルの8ビット形式の音データは符号なし(0〜255)の値で、P/ECE音楽ライブラリの音色データは8ビット符号付き(-128〜127)の値だからです。 音楽ライブラリのドラム音色を作る時に、切り出したデータ全体を128バイアスしたために、ツール情報のチャンクも128バイアスされて紛れてしまったのだと思います。 ■対策 ノイズ部分のデータが、不要なデータだという事がわかったので、心配なく削除できます。 削除する方法は、以下の通りです。 自作プログラムに音楽ライブラリの音色データを含める時は、P/ECE開発環境のフォルダから、音色データのファイル一式をコピーして来ていると思います。 例えば、「/usr/PIECE/docs/TUTORIAL/AYAKA/wavetbl/」フォルダ等からです。(左記以外にも何箇所かのフォルダに置かれていますが、どれでも同じです。) コピーして来たファイルのうち、「i_CYMBD.h」と「i_TOMH1.h」と「i_TOMM1.h」を、下図のように書き換えればokです。 http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/cymbd_h_fix.png http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/tomh1_h_fix.png http://www.piece-me.org/piece-lab/wavetbl_bug/4_after_fix_h/tomm1_h_fix.png 上記の対策を行った後、先程と同じMMLを再生して、ノイズが鳴らなくなった事を確認出来ました。 ■ダウンロード 今回使用した検証プログラム一式の、ダウンロードはこちらです: ダウンロードはこちら: http://www.piece-me.org/piece-lab/wavetbl_bug/wavetbl_bug-20170814-src.zip