V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
kaler
V2EX  ›  C++

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

  •  
  •   kaler · 2019-10-24 15:17:11 +08:00 · 4118 次点击
    这是一个创建于 1850 天前的主题,其中的信息可能已经有所发展或是发生改变。
    这样一行代码
    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   ·   实用小工具   ·   2834 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 09:37 · PVG 17:37 · LAX 01:37 · JFK 04:37
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.