问一个 c++中四舍五入的问题

2019-10-24 15:17:11 +08:00
 kaler
这样一行代码
cout << fixed << setprecision(1) << 1.25 << endl;
在 VS2015 当中运行的结果为:
1.3
使用 g++ 5.4.0 得到的结果为:
1.2
是什么原因造成的呢?按理说 1.25 能够被浮点数精确表示啊。
4164 次点击
所在节点    C++
10 条回复
RHxW
2019-10-24 15:33:29 +08:00
不懂 c++
但是这个 setprecision 看着有点可疑,改成 2 试试
关于舍入,我记得 gcc 是向零舍入,可能 VS 那个编译器是向上吧
Akiyu
2019-10-24 15:38:55 +08:00
你用了 setprecision(1)
http://www.cplusplus.com/reference/iomanip/setprecision/?kw=setprecision

至于为什么不同的环境下数值不同, 那可能和自身平台有关
kaler
2019-10-24 15:53:59 +08:00
setprecision(1)是我故意加上去的,就是为了看不同编译器是怎么四舍五入的。
kaler
2019-10-24 15:56:16 +08:00
但如果把 1.25 改成 1.2500001 的话,两个编译器的结果都为 1.3
wutiantong
2019-10-24 18:18:37 +08:00
@kaler 看四舍五入不是应该用 std::round 么
yuikns
2019-10-24 18:37:08 +08:00
set precision 这个有假设是四舍五入了么?
kaler
2019-10-24 18:55:29 +08:00
@yuikns 我在 setprecision 的一些文档里也没看到关于四舍五入的信息,但从输出结果来看还是存在这个过程的,所以我现在只是好奇这一步是在哪做的,也想知道这样不同编译器的输出差异有没有文档记录。
happydezhangning
2019-10-24 19:44:21 +08:00
pyton 里也遇到过类似情况,round 四舍五入规则不一样,是四舍六入,五要看前面一位的奇偶,据说这样的四舍五入才是可靠的,如果逢五都进位会导致整体偏大
by73
2019-10-24 23:24:34 +08:00
啊,花了一个晚上,大概总结出了一些东西。先说结论吧,这算是 crt 不同而导致的,windows sdk 中的 printf 函数( cout 应该是一致的)调用的是 windows crt 的内容,默认四舍五入; mingw 使用的是自己整的一套 mingw-w64-crt,默认直接截断。

-----

tl;dr:windows crt 使用的是四舍五入,mingw crt 是直接截断。

( v2 排版可能不太好,源代码我有给定位,可以自己开着编辑器去看)

首先是 Windows 部分,进入 Windows SDK ucrt 文件夹(我的版本是 10.0.18362.0 )从 printf() 函数开始追,能够一路追到 convert/_fptostr.cpp 这个 CRT 源码,找到 `__acrt_fp_strflt_to_string` 函数 61 行就能看到四舍五入的策略,就是只要读完 precision 后还有数字,如果这个数字大于等于 5,就往前进一。这个逻辑是完全写进 CRT 的,所以我之前尝试了半天用 `fesetround()` 都没有任何用处(或者说,对 printf 不起作用)。

~~~ cpp
// Do any rounding which may be needed. Note: if digits < 0, we don't do
// any rounding because in this case, the rounding occurs in a digit which
// will not be output because of the precision requested.
if (digits >= 0 && *mantissa_it >= '5')
{
buffer_it--;

while (*buffer_it == '9')
{
*buffer_it-- = '0';
}

*buffer_it += 1;
}
~~~

p.s. 顺带感叹一句,printf 实现原来原来是状态机,DFA 牛逼。而且 windows crt 对 print 这部分似乎比较罗嗦,可能是我见识的太少。打印浮点大致的流程是:设置状态 -> 读取浮点数 -> 根据浮点数转换成高精度小数字符串(这个算法有点看不太懂,我太菜了)-> 根据状态对该字符串进行四舍五入 -> 最后处理该字符串 buffer,输出到 output 设备。不知道为啥要绕这么个弯(可能是算法问题)。

同理,不过 mingw crt 的源码默认没带,要去 https://git.code.sf.net/p/mingw-w64/mingw-w64 克隆一份,我是今天克隆的,不清楚版本(不过 crt 应该不会有太大变化);可以一路追到 mingw-w64/mingw-w64-crt/stdio/mingw_pformat.c,定位到 `__pformat_float_decimal`,处理截断的就是 1796 行的 easy mode 地方。相对 windows crt 的复杂,mingw crt 在这方面倒是比较简单,直接输出 precision 个字符,输出完就行,不做任何其他处理,简而言之就是“截断”。

~~~ cpp
if(decimal_place <= 0){ /* easy mode */
__pformat_putc( '0', stream );
points:
__pformat_emit_radix_point(stream);
for(int32_t written = 0; written < prec; written++){
if(decimal_place < 0){ /* leading 0s */
decimal_place++;
__pformat_putc( '0', stream );
/* significand */
} else if ( sig_written < max_prec ){
__pformat_putc( str_sig[sig_written], stream );
sig_written++;
} else { /* trailing 0s */
__pformat_putc( '0', stream );
}
}
} else // 后面是 hard mode,即小数部分不定长
~~~

p.p.s. mingw 的原理也是状态机,但是相对 windows 简化了许多,没有见到像 windows crt 那样使用的跳转表。大致 printf 流程为:读取 format 根据符号设置状态 -> 根据 IEEE 754 提出指数、小数、符号 -> 直接根据截断进行输出;相比之下没有 windows 那样频繁对 buffer 的操作。

-----

干,搞了一晚上,一开始以为只是单纯的 compiler 的问题,但发现无论怎么调 flag 都没用,才考虑到是 crt 的问题,果然我还是 too young。不过读大厂的代码还是挺舒服的,除了代码定位只能靠 vscode 搜索,其他该有的注释都有,还很详细,不愧是 m$。相比之下 mingw 的代码要逊一点,可能是我不太习惯 c 吧 emm
by73
2019-10-25 09:50:14 +08:00
@by73 补充一下 cout 吧,昨天光顾着分析 printf 了:msvc 实现的 stl 中(源码在 https://github.com/microsoft/STL/tree/master/stl/inc ),`src/cout.cpp` 可以找到 cout 的定义

~~~ cpp
__PURE_APPDOMAIN_GLOBAL static filebuf fout(_cpp_stdout);
__PURE_APPDOMAIN_GLOBAL extern ostream cout(&fout);
~~~

知道 cout 就是一个 ostream,所以可以去 `inc/ostream` 找相关的 `operator<<`,然后继续追下去,可以找到真正进行输出的函数 `do_put`,位于 `inc/xlocnum` 1294 行,发现其核心为

~~~ cpp
const auto _Ngen = static_cast<size_t>(_CSTD sprintf_s(
&_Buf[0], _Buf.size(), _Ffmt(_Fmt, 0, _Iosbase.flags()), static_cast<int>(_Precision), _Val));
~~~

`sprintf_s`。到这里之后,后面的内容就交给 crt 处理了,而 sprintf_s 跟 printf 都是用的同一套 `processor_type` 模板,所以 cout 最终输出还是要依赖 crt,也因此会受到干扰。mingw 应该也是一样的,这个大家有兴趣可以自己找找,读源码还是挺有意思的。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/612564

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX