ぬうぱんの備忘録

技術系のメモとかいろいろ

作った曲一覧はこちら

C/C++で四捨五入で浮動小数点を整数に丸める

なにがあった

 負数込みで丸めようと思うと思いの外回り道をしてしまったのでメモ。結論だけ知りたい人は記事の末尾に飛んでオナシャス!

そもそもなんで四捨五入なのか

 画像の幾何的な変換を行おうとしたら、変換元の画像のピクセルに実数座標でアクセスすることになった。画像のあるピクセルに実数座標でアクセスしようと思うと、NN(Nearest Neighbor)でアクセスするのが一番らくちんなのだが、これは``x, y各座標値(実数値)を最も近い整数値に丸めてそこにアクセス''というものなので、実数値を最も近い整数値に丸める=四捨五入を実装しなくてはならない、という話。

すぐに見つかる簡単な方法

 簡単のために(0.0, 1.0)の区間で考えると

    • (0.0, 0.5) のとき 0
    • [0.5, 1.0) のとき 1

という事になる。
この時、丸めたい値に0.5を足して小数点以下を切り捨てると四捨五入になる。境界値だけを例示すると

x x+0.5 round(x+0.5)
0.499... 0.99... 0
0.5 1.0 1

確かに四捨五入になっている

もし負数になったら

 実は0.5を足して切り捨てというやりかたでは負数を処理するときに問題が起きる。というのも、C++のコードで書くと

float x; //丸めたい値
int i;   //丸めた値
i = static_cast<int>(x+0.5f);

というような処理になるのだが、もしxが負数だった場合

    • x=-0.6 のとき i=static_cast(-0.1)=0

にというような事になる。が、今したいのは``実数値を最も近い整数値に丸める''なので-0.6は-1に丸まってほしい。ああこれではいけない-0.6が0に丸まってしまった。

一応の解決方法

``負数なのに+0.5するからいけないんだよ-0.5しちゃえばいいんだよ''と思うかもしれない。この考え方に沿って解決しようとしてみると

float x;
float half=0.5;
unsigned mask = 0x800000;
int i;

half |= *reinterpret_cast<float*>(&mask)&x;
i = static_cast<int>(x+half);

floatのMSBが符号ビットであることを利用してビット演算を使えばあまりコストをかけずに丸めたい実数値と同じ符号の0.5を作ることはできる。が、これでは論理演算が余計に増えてしまう。

そもそも

標準ライブラリにround関数あるだろ! いい加減にしろ!

コード的にはこんな感じ

#include <cmath>

float x;
int i;

i = roundf(x);

round関数はC言語の関数なのでfloat/double/long doubleでの切り替えが必要な場合は関数名を変える必要があります。なので

/*! roundf C言語関数のラッパー
 */
inline float RoundOL(float x){
	return roundf(x);
}

/*! round C言語関数のラッパー
 */
inline double RoundOL(double x){
	return round(x);
}

/*! roundl C言語関数のラッパー
 */
inline long double RoundOL(long double x){
	return roundl(x);
}

みたいなラッパーでも書いておけば幸せになれるんじゃないですかね?(適当)

ちなみに


gcc-4.7で-O0だけ付けた状態で吐き出されたアセンブラを見てみると``i = roundf(x)''からvcvttss2si(SSEのintrinsicで言う_mm_cvtss_si32に相当)が生成されていて、これはfloatからintに一つの命令で変換されているということなので、速度云々とかは心配しなくてよさそうです。

しかしながら

round()関数は死ぬほど遅い・・・。関数呼び出しがインライン展開されないからだとは思うのですがそれにしたっていくら何でも遅い。で、今の御時世SSE2ぐらいは使えて当然ということにしてSSE2で丸め処理を書いちゃうとこんな感じ

#include <xmmintrin.h>
#include <emmintrin.h>

float f;
double d;
int i;

i = _mm_cvtss_si32(_mm_load_ss(&f));
i = _mm_cvtsd_si32(_mm_load_sd(&d));

これだと+0.5して切り捨てる方法よりも速いです。floatのだけの場合はSSE2無しでもいけます。_mm_load_ss()と_mm_load_sd()はアラインされていなくてもロードできるのでアラインメントは気にしなくてもいいです。
これを関数化してしまうと

/*! floatのラウンド関数
 */
inline int32_t Round(const float& x){
	return _mm_cvtss_si32(_mm_load_ss(&x));
}

/*! doubleのラウンド関数
 */
inline int32_t Round(const double& x){
	return _mm_cvtsd_si32(_mm_load_sd(&x));
}