浮動小数点型でビット演算

人間には波があり、調子のいい日、悪い日があると思いますが、どうやら今日は調子の悪い日のようでした。それでもGPUPPURを開発し続けなければならないので、できるだけ自分の好きなことをやることにしました。そこで最適化をやることにしました。もちろんタイミング的に早過ぎますが。

GPUPPURではPPUで計算した結果を受け取り、GPUへそのデータを送る部分があります。データの量が数MB〜数十MBとなることがあり、GPUへデータを送る部分がボトルネックとなります。ですのでデータの量を減らしたり、データのフォーマットを工夫するなどして最適化する必要があります。

PPUから受け取るデータは基本的にfloat型で、そのうち法線ベクトルは値が-1.0f〜1.0fの範囲となります。また、GPUへ物体の色を送るとき、その値は0.0f~1.0fの範囲となります。float型をそのまま送るとデータがでかいし、そもそもそんなに精度が必要だとは思えないので16bitのhalf float型にするといいかもしれません。half float型では指数部に5bitぐらい使うのですが、値の範囲がほぼ程度決まっているのに指数部に5bitも使うのは勿体ない気がしたので、unsigned short型のデータに変換してGPUへ送るとよさそうです。OpenGLではテクスチャの色としてデータを送るとき、符号無し整数型の場合だと入力値iをi/(2^n-1)(nは整数型のビット数)として0~1の間の値を持つ固定小数点へと変換します。

というわけで、値が0.0f~1.0fの範囲となるfloat型を値が0~65535の範囲となる符号無しshort型へ変換する処理と、-1.0f〜1.0fの範囲のfloat型の値を0~65535の範囲の符号無しshort型へ変換する処理について考えてみます。ここで前者の場合は0.0fが0へ、1.0fが65535へ必ず変換され、後者の場合は-1.0fが必ず0へ、1.0fは必ず65535へ変換されるという条件を付けます。

まず0.0f~1.0fを0~65535へ変換するほうですが、単純にやると以下のようなコードになります。


typedef unsigned short ui16;

ui16 to_ui16_from_0_1(float v)
{
return v*0xffff;
}

次にビット演算を駆使して少しでも高速化できるようにもがきます。

typedef unsigned short ui16;
typedef unsigned int ui32;

union f_i
{
float f;
ui32 i;
};

int get_exp(float v)
{
return 127 - ((*(union f_i*)&v).i >> 23);

}

ui32 get_frac(float v)
{
return (*(union f_i*)&v).i&0x7fffff | 0x800000;
}

ui16 to_ui16_from_0_1(float v)
{
int exp = get_exp(v);

ui32 frac = get_frac(v);

return (frac-1) >> exp >> 7;
}

自分のPCでテストした結果、後者のコードで正しい結果が得られました。また、VisualC++2005のコンパイラで/Ox /Ot /fp:fastのオプション付きでコンパイルした場合、ビット演算を使ったほうが1.3倍ほど高速でした。

  • 1.0f~1.0fを0~65535へ変換するほうですが、単純なやり方は


ui16 to_ui16_from_n1_1(float v)
{
return (v+1.0f)/2.0f*0xffff;
}
ビット演算を使って高速化しようとすると

union f_i
{
float f;
ui32 i;
};

int get_exp(float v)
{
return 127 - ((*(union f_i*)&v).i >> 23);
}

ui32 get_frac(float v)
{
return (*(union f_i*)&v).i&0x7fffff | 0x800000;
}

ui16 to_ui16_from_n1_1(float v)
{
int exp = get_exp(v);
ui32 frac = get_frac(v);

ui32 tmp = (frac-1) >> (exp&0xff) >> 8;
exp = exp>>8;
return (0x7fff - tmp) >> ((16 << exp)&16) |
(tmp | 0x8000) >> (exp&16);
}

先ほどと同様の条件ではビット演算を使ったほうのコードが約1.07倍速かったです。苦労したわりにはあまり速くなってない。

bit演算を使って符号無しshortへ変換しようとすると、思ったり複雑な処理になったのでもしかするとhalf floatへ変換するほうが高速かもしれません。そもそも8bitでも精度が十分かもしれません。

もし、上記の方法より高速に変換する方法を見つけた方は是非教えて下さい。