DataOutputStreamでfloatのデータを大量に書き込みたい時に、
float[] data = ...;
DataOutputStream out = ...;
for(int i = 0; i < data.length; i++){
out.writeFloat(data[i]);
}
というように書くのが常套手段ですが、これは非常に遅いです。何故遅いかを調べてみました。
中の実装はどうなっているか
(ざっくり再現したコードで、JDKと全く同じではありません)
floatをintに変換
書き込むためにはバイナリにしなければいけないため、float型の値を同じビットレイアウトで表現されたint型に変換されます。そのためにFloat#floatToIntBits(float)が呼ばれます。
float f = ...;
int v = Float.floatToIntBits(f);
intをbyteに変換
JavaのOutputStreamは原則byteかbyte配列の書き込みしかサポートされていません。そのためintはbyteに変換されます。
float f = ...;
int v = Float.floatToIntBits(f);
byte b1 = (byte) ((v >>> 24) & 0xff);
byte b2 = (byte) ((v >>> 16) & 0xff);
byte b3 = (byte) ((v >>> 8 ) & 0xff);
byte b4 = (byte) ((v >>> 0 ) & 0xff);
書き込む
最後に1byte毎に書き込みます。floatだけではなくintも同じ挙動になります。
float f = ...;
int v = Float.floatToIntBits(f);
byte b1 = (byte) ((v >>> 24) & 0xff);
byte b2 = (byte) ((v >>> 16) & 0xff);
byte b3 = (byte) ((v >>> 8 ) & 0xff);
byte b4 = (byte) ((v >>> 0 ) & 0xff);
out.write(b1);
out.write(b2);
out.write(b3);
out.write(b4);
doubleだと・・・?
doubleやlongの場合も同じようなコードになりますが、OutputStream#write(byte[])が呼び出されるためかなり速いです。
double d = ...;
long v = Double.doubleToLongBits(d);
buffre[0] = (byte) (v >>> 56) & 0xff);
buffre[1] = (byte) (v >>> 48) & 0xff);
buffre[2] = (byte) (v >>> 40) & 0xff);
buffre[3] = (byte) (v >>> 32) & 0xff);
buffre[4] = (byte) (v >>> 24) & 0xff);
buffre[5] = (byte) (v >>> 16) & 0xff);
buffre[6] = (byte) (v >>> 8) & 0xff);
buffre[7] = (byte) (v >>> 0) & 0xff);
out.write(buffer);
何が結局速いのか
自分が実装したコードでは、
- intへの変換(Float#floatToRawIntBits(float)を呼び出す)
- byte配列に直接書き込む
- byte配列の終端に来たらOutputStream#write(byte[])を呼び出す
を行って高速化しました。面倒なのは1byte書き込むことに発生する境界チェックです。
もし書き込むデータが4bytes単位しかなく、バッファ用の配列サイズも4bytesの倍数であれば、floatの書き込み1回ごとに配列の境界チェックができます。