问一个 c++中四舍五入的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kaler
V2EX    C++

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

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

    至于为什么不同的环境下数值不同, 那可能和自身平台有关
    kaler
        3
    kaler  
    OP
       2019-10-24 15:53:59 +08:00
    setprecision(1)是我故意加上去的,就是为了看不同编译器是怎么四舍五入的。
    kaler
        4
    kaler  
    OP
       2019-10-24 15:56:16 +08:00
    但如果把 1.25 改成 1.2500001 的话,两个编译器的结果都为 1.3
    wutiantong
        5
    wutiantong  
       2019-10-24 18:18:37 +08:00
    @kaler 看四舍五入不是应该用 std::round 么
    yuikns
        6
    yuikns  
       2019-10-24 18:37:08 +08:00
    set precision 这个有假设是四舍五入了么?
    kaler
        7
    kaler  
    OP
       2019-10-24 18:55:29 +08:00
    @yuikns 我在 setprecision 的一些文档里也没看到关于四舍五入的信息,但从输出结果来看还是存在这个过程的,所以我现在只是好奇这一步是在哪做的,也想知道这样不同编译器的输出差异有没有文档记录。
    happydezhangning
        8
    happydezhangning  
       2019-10-24 19:44:21 +08:00
    pyton 里也遇到过类似情况,round 四舍五入规则不一样,是四舍六入,五要看前面一位的奇偶,据说这样的四舍五入才是可靠的,如果逢五都进位会导致整体偏大
    by73
        9
    by73  
       2019-10-24 23:24:34 +08:00   7
    啊,花了一个晚上,大概总结出了一些东西。先说结论吧,这算是 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
        10
    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 应该也是一样的,这个大家有兴趣可以自己找找,读源码还是挺有意思的。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2835 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 14:20 PVG 22:20 LAX 06:20 JFK 09:20
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86