圆整就是获得一个浮点数最接近的整数,所谓的“四舍五入”便是指的圆整。C++ 中可以调用 std::round
来实现。看起来很简单的操作是吧,其实里面有一些小细节的。
std::round
不能在编译期间计算
编译期(compile-time)计算和运行期(run-time)计算的概念想必大家都懂,如果一个函数能够在编译期间就计算得到结果(这样的函数叫做常量表达式函数 constexpr funciton),那就能够节省运行期的计算时间。某些情况下必须要求在编译期计算,比如模板参数:
template <int N>
void printN() { std::cout << N << '\n'; }
int main()
{
constexpr int n = std::round(0.5);
printN<n>();
}
上面这段代码在 MSVC 和 Clang 中是无法编译的,因为 std::round
不能在编译期计算!这听起来难以置信,为什么这么简单的函数却只能在运行期计算呢?
答案是错误处理,当圆整失败(圆整结果超出整数范围)时,std::round
将错误信息存放到全局变量(其实是 thread-local,即在单个线程中可以看作是全局,为了线程安全) math_errhandling
中,以便开发者检验是否出错。
显然这样的错误处理机制是运行期间执行的。与圆整一样,std cmath 中所有的数学函数都用了 math_errhandling
,因此都不能在编译期间计算。
不过需要注意的是,GCC 能够在编译期间计算 std 数学函数,所以上面的代码能够用 GCC 正常编译运行。但 C++ 标准 (c++draft) 明确说了:
An implementation shall not declare any standard library function signature as constexpr except for those where it is explicitly required.
大白话就是:标准库中没说是常量表达式的函数被给我瞎改成常量表达式!所以 GCC 的做法算是自作主张吧。为了程序的兼容性,还是尽量不要利用 GCC 这一特性为好。
圆整计算可能比较慢
如果你非常在意计算速度的话,std::round
可能并不是一个好的选择,原因同样是错误处理。
一方面,错误处理会本身会带来额外的时间消耗(错误判断和错误信息写入全局变量)。另一方面,math_errhandling
这一全局变量的存在导致 std::round
无法进行向量化(编译器会自动地将一些计算向量化加速)。
假如你希望获得极致的计算速度,同时能确保圆整结果不会溢出,可以自己实现一个没有错误处理的圆整:
inline static int myRound(double val)
{
return (int)(val + (val >= 0 ? 0.5 : -0.5));
}
或者利用 SSE 指令,应该更快一些:
inline static int myRound(double val)
{
__m128d t = _mm_set_sd(val);
return _mm_cvtsd_si32(t);
}
(其实上面这两个实现来自 OpenCV 的 cvRound
,benchmark).
圆整并不一定是“五入”
如果你用 OpenCV,会发现 1.5 和 2.5 通过 cvRound
圆整后都是 2,说好的四舍五入呢?
这是因为,cvRound
优先采用 SSE 实现,而 SSE 中的圆整采用的是 bankers’ rounding 策略(又称 statistician’s rounding、Gaussian rounding),这种策略将 x.5 圆整到最接近的偶数 (round half to even),为的是解决统计学中四舍五入带来的结果偏高问题。
默认情况 (FE_TONEAREST) 下,std::round
是四舍五入圆整,std::rint
是 bankers’ rounding。