浮動小数点演算の回避
-
22-07-2019 - |
質問
iPhone用の小さなソフトウェアシンセを作成しました。
パフォーマンスをさらに調整するために、Sharkを使用してアプリケーションを測定しましたが、float / SInt16変換で多くの時間が失われていることがわかりました。
そこで、「すぐに使える」を返すルックアップテーブルを事前に計算して、変換を回避するために一部を書き直しました。 SInt16サンプル。これはこれまでのところうまくいきます。
現在、いくつかのフィルターとADSRエンベロープの実装を書き換えて整数演算のみを使用しようとしていますが、浮動小数点なしで乗算/除算を実行するためのヒントを使用できます。
私は iPhoneの標準形式:
- LPCM
- 16ビット整数サンプル
フロートを使用せずに最終サンプルに振幅を適用する良い方法は何ですか?
編集:
これまでにわかったのは、現在のサンプルを右シフトすることで2の累乗で除算できることだけです。
inBuffer[frame] = wavetable[i % cycleLengthInSamples] >> 4;
しかし、それを使ってスムーズなADSRエンベロープを作成するエレガントな方法は考えられません。
Edit2:
すばらしい回答をありがとうございます!
私の現在のアプローチ:
- すべてのADSRエンベロープ値を取得する 正のSInt16範囲に
- ウェーブテーブルの現在の値と乗算します(中間体をSInt32として保存します)
- 結果を右に16シフト
これは動作するようです:)
解決
固定小数点は適切です。この場合、16ビットを使用しているためです。最も簡単な方法は、必要な精度に応じて10の累乗を掛けることです。中間体として32ビット整数を使用できる場合、適切な精度を得ることができるはずです。最後に、16ビット整数に変換して、必要に応じて丸めたり切り捨てたりすることができます。
編集: 値を大きくするために、左にシフトします。シフトの結果をより精度の高い型に格納します(必要に応じて32ビットまたは64ビット)。 署名タイプを使用している場合、単純なシフトは機能しません
2つの固定小数点数を乗算または除算する場合は注意してください。乗算は(a * n)*(b n)になり、a b nの代わりに b n ^ 2になります。除算は(a n)/(b n)であり、これは((a n)/ b)ではなく(a / b)です。そのため、固定小数点に精通していない場合でも、10の累乗を使用することで、間違いを見つけやすくなりました。
計算が完了したら、右に戻り、16ビット整数に戻ります。 おしゃれにしたい場合は、シフトする前に丸めることもできます。
効率的な固定小数点の実装に本当に興味がある場合は、読んでおくことをお勧めします。 http://www.digitalsignallabs.com/fp.pdf
他のヒント
このSO質問に対する回答は実装の面でかなり包括的な。ここで見た以上の説明があります:
1つのアプローチは、すべての数値を強制的に範囲に入れることです(たとえば[-1.0,1.0)。次に、これらの数値を[-2 ^ 15、(2 ^ 15)-1]の範囲にマッピングします。たとえば、
Half = round(0.5*32768); //16384
Third = round((1.0/3.0)*32768); //10923
これら2つの数値を乗算すると、取得されます
Temp = Half*Third; //178962432
Result = Temp/32768; //5461 = round(1.0/6.0)*32768
最後の行で32768で割ることは、パトロに関するポイントです。追加のスケーリングステップを必要とする乗算。 2 ^ Nスケーリングを明示的に記述する場合、これはより理にかなっています。
x1 = x1Float*(2^15);
x2 = x2Float*(2^15);
Temp = x1Float*x2Float*(2^15)*(2^15);
Result = Temp/(2^15); //get back to 2^N scaling
これが算術です。実装の場合、2つの16ビット整数の乗算には32ビットの結果が必要なので、Tempは32ビットにする必要があることに注意してください。また、32768は16ビット変数では表現できないため、コンパイラは32ビットの即値を作成することに注意してください。また、既に述べたように、2の累乗で乗算/除算にシフトできるため、次のように記述できます
N = 15;
SInt16 x1 = round(x1Float * (1 << N));
SInt16 x2 = round(x2Float * (1 << N));
SInt32 Temp = x1*x2;
Result = (SInt16)(Temp >> N);
FloatResult = ((double)Result)/(1 << N);
しかし、[-1,1)が正しい範囲ではないと仮定しますか?数値を[-4.0,4.0)などに制限する場合は、N = 13を使用できます。1つの符号ビット、2進小数点の前に2ビット、後に13ビットがあります。これらは、それぞれ1.15および3.13固定小数点小数型と呼ばれます。ヘッドルームの分数の精度を犠牲にします。
彩度を調べる限り、分数型の加算と減算は正常に機能します。除算については、パトロスが言ったように、スケーリングは実際にキャンセルされます。だからあなたはしなければならない
Quotient = (x1/x2) << N;
または、精度を維持する
Quotient = (SInt16)(((SInt32)x1 << N)/x2); //x1 << N needs wide storage
整数による乗算と除算は正常に機能します。たとえば、6で割るには、次のように書くことができます
Quotient = x1/6; //equivalent to x1Float*(2^15)/6, stays scaled
そして、2の累乗で割る場合
Quotient = x1 >> 3; //divides by 8, can't do x1 << -3 as Patros pointed out
ただし、整数の加算と減算は単純には機能しません。最初に整数がx.y型に収まるかどうかを確認し、同等の小数型を作成して続行する必要があります。
これがアイデアに役立つことを願っています。クリーンな実装については、他の質問のコードを見てください。
高速乗算アルゴリズムについて説明しているこのページをご覧ください。
一般に、符号付き16.16固定小数点表現を使用するとします。そのため、32ビット整数には、符号付き16ビット整数部と16ビット小数部が含まれます。その場合、iPhone開発で使用されている言語はわかりません(おそらく、Objective-Cですか?)が、この例はCで記述されています:
#include <stdint.h>
typedef fixed16q16_t int32_t ;
#define FIXED16Q16_SCALE 1 << 16 ;
fixed16q16_t mult16q16( fixed16q16_t a, fixed16q16_t b )
{
return (a * b) / FIXED16Q16_SCALE ;
}
fixed16q16_t div16q16( fixed16q16_t a, fixed16q16_t b )
{
return (a * FIXED16Q16_SCALE) / b ;
}
上記は単純な実装であり、算術オーバーフローからの保護を提供しないことに注意してください。たとえば、div16q16()では、精度を維持するために除算の前に乗算しますが、オペランドによっては演算がオーバーフローする場合があります。これを克服するために64ビットの中間体を使用できます。また、整数除算を使用するため、除算は常に切り捨てられます。これにより最高のパフォーマンスが得られますが、反復計算の精度に影響する場合があります。修正は簡単ですが、オーバーヘッドが増えます。
一定の2のべき乗で乗算または除算する場合、ほとんどのコンパイラーは単純な最適化を見つけてシフトを使用することに注意してください。ただし、Cは負の符号付き整数の右シフトの動作を定義しないため、安全性と移植性のためにそれを解決するためにコンパイラに任せています。使用している言語にかかわらずYMV。
OO言語では、fixed16q16_tは当然、演算子がオーバーロードするクラスの候補になるため、通常の算術型のように使用できます。
タイプ間で変換すると便利な場合があります:
double fixed16q16_to_double( fixed16q16_t fix )
{
return (double)fix / FIXED16Q16_SCALE ;
}
int fixed16q16_to_int( fixed16q16_t fix )
{
// Note this rounds to nearest rather than truncates
return ((fix + FIXED16Q16_SCALE/2)) / FIXED16Q16_SCALE ;
}
fixed16q16_t int_to_fixed16q16( int i )
{
return i * FIXED16Q16_SCALE ;
}
fixed16q16_t double_to_fixed16q16( double d )
{
return (int)(d * FIXED16Q16_SCALE) ;
}
これが基本です。より洗練されたトリガーや他の数学関数を追加することができます。
固定の加算および減算は、組み込みの+および-演算子とそのバリアントで機能します。