RSS Twitter Facebook

2020/07/04 (2020年07月 のアーカイブ)

デノーマル問題は今どうなっているのか


もう今から 10 年近く前の話になりますが、ソフトシンセ等の楽器系ソフトウェアではデノーマル問題というのがちらほらと起こっていました。これは扱うデータが通常の浮動小数点では表現できないほど小さい ( 0 に近い) 状態になると、単に 0 に切り捨てるのではなくて CPU がデノーマルモードという特殊なモードで近似的に扱う代わりにパフォーマンスが極端に悪い状態になる事を指します。

ソフトウェアのジャンルによってはこれでも特に問題はなかったのですが、楽器系ソフトウェアはリアルタイム処理が命で処理が間に合わないとノイズが発生する等の結果になったりしますので特に敏感だったわけです。そしてこの問題はネイティブアプリだけではなく、Javascript 上でも同じように存在していて Web アプリでシンセを作ろうとするとやはり気にしないといけなった、というのが大体 9 年前。↓の記事でブラウザ上のデノーマル問題について少し検証しています。

「Javascriptでもデノーマル問題はあるんだよ!」

その後、Chrome のバグトラッカーにこの問題があがって対策されたのが 5、6年前くらい。これでもう Web アプリでデノーマルに悩まされる事は無くなるのかなー、やったね。
という事があって、もうデノーマル問題の存在すら忘れかけていたのですが、今になってちょっと引っかかった問題をきっかけにこのあたりを掘って見ると、見つけてしまいました。デノーマル再来。

問題は TypedArray です。

倍精度の場合は
\( 2.22 \times 10 ^{-308} \)、

単精度の場合は
\( 1.17 \times 10 ^{-38} \)

あたりよりも 0 に近くなるとデノーマルモードに入ります。Javascript の通常の変数の場合はすべて倍精度浮動小数点として扱われ、10のマイナス308乗あたりよりも小さくなるとデノーマル状態になりますが、どうやら何らかの対策が行われていて特にパフォーマンスが落ちるという事はないように見えます。しかし、これが TypedArray、Float32Array や Float64Array の場合は、昔のようにデノーマル状態でパフォーマンスが 5~6 倍程度落ちるようです (昔よりはパフォーマンスのペナルティは低くなっているようですが)

下に検証用のコードがあります。javascript の通常の変数、Float32Array、Float64Array についてそれぞれ変数の値に 0.1 を掛けて行き、時間を測定しています。
これをそのまま Javascript コンソールに貼り付ければ実際に結果を見る事ができます。


for (f = 0.1; f != 0; f *= 0.1) {
  Time1 = performance.now();
  for (var i = 0; i < 1000000; ++i) {
      result = f * 0.1;
  }
  Time2 = performance.now();
  document.write((Time2 - Time1)+"mSec , "+result+"<br/>");
}
document.write("Loop Exit!! (Normal Variable) <hr/>");

a=new Float32Array(3);
for (a[0] = 0.1,a[1] = 0.1; a[0] != 0; a[0] *= a[1]) {
Time1 = performance.now();
for (var i = 0; i < 1000000; ++i) {
a[2] = a[0] * a[1];
}
Time2 = performance.now();
document.write((Time2 - Time1)+"mSec , "+a[2]+"<br/>");
}
document.write("Loop Exit!! (Float32Array) <hr/>");

b=new Float64Array(3);
for (b[0] = 0.1, b[1]=0.1; b[0] != 0; b[0] *= b[1]) {
Time1 = performance.now();
for (var i = 0; i < 1000000; ++i) {
b[2] = b[0] * b[1];
}
Time2 = performance.now();
document.write((Time2 - Time1)+"mSec , "+b[2]+"<br/>");
}
document.write("Loop Exit!! (Float64Array) <hr/>");

さて、この結果は下の図のようになりました。左側が通常の変数の場合です。デノーマル領域を超えて 0 になるまで特に処理時間に変わりはないようです。しかし、

右側上 Float32Array の場合は \(1.17 \times 10^{-38}\)
右側下 Float64Array の場合は \(2.22 \times 10^{-308}\)

よりも小さいデノーマル領域では処理時間が落ちているのがわかります。

昔みたいに何十倍も遅くなるという程ではないので致命的な問題にはなりにくそうではありますが、逆に、パフォーマンスいまいちだなーとか思いながら気が付かずに使ってしまうという事がありそうですね。 TypedArray を使う時には気を付けた方が良さそうです。

-----(2020/07/04) さっそく追記ですが-----

昔よりデノーマルのペナルティが低くて 5~6 倍というのは勘違いだったようです。事態はもっと深刻とも言えます。
という事で検証用の式と結果を訂正します。

検証で使用した計算式が元は、 通常の変数 = Float32Array * 通常の変数 になっていました。
この場合、通常の変数にデノーマル数をロードするのが遅いという事はわかりますが演算自体はデノーマルのペナルティが発生しない通常変数で行われています。

Float32Array = Float32Array * Float32Array のように TypedArray のままで演算自体が完結する場合、デノーマル数が発生すると通常の場合よりも 20 倍程度時間がかかるようです。

ただし、TypedArray の演算は通常の変数よりも高速ですのでそれに比べて、という話になります。高速なはずの TypedArray なのにデノーマルを発生させてしまうと通常の変数よりも遅くなってしまうという事でもあります。TypedArray を使う場合は十分に注意する必要がありますね。
なお、この件は Chrome、Firefox 共同じ現象になるようです。


Posted by g200kg : 2020/07/04 11:47:58