Cracking C++(6): 准确打印浮点数

news2024/11/26 14:38:20

文章目录

    • 1. 目的
    • 2. 准确打印浮点数: 使用 fmt 库
    • 3. 准确算出被表示的值
      • 3.1 直观感受IEEE-754: float-toy
      • 3.2 获取浮点数二进制表示
      • 3.3 float 类型
      • 3.4 double 类型
      • 3.5 fp16 类型
      • 3.6 验证
    • 4. 结论和讨论
    • 5. References

1. 目的

给 float 或 double 类型的变量赋值后, 打印出来的值和赋值时传入的值可能不一样, 也就是说有有精度损失。常见的几个疑问是:

  • 为啥有精度损失?
  • 为啥 float 类型精度损失这么大, 我们老师以前说是1e-6的?
  • 为啥明明有好几位小数, printf 和 cout 只打印出6位?

答案:

  • float 和 double 类型是 IEEE 754 标准规定的, 首先要转为二进制表示, 再按格式算出能表示的值, 而转为二进制表示的过程中受限为位数, 存在精度损失的问题;
  • 在得到有精度损失的数值后, printf 和 cout 默认打印的精度不是很友好, 可以用 fmt 库执行打印

本文不涉及浮点数转二进制时的 rounding 细节, 考虑的是得到 rounding 后的二进制后, 逐步算出被表示的浮点数数值的过程, 以及各个部分的二进制表示。支持 float, double, fp16 三种类型.

2. 准确打印浮点数: 使用 fmt 库

CMakeLists.txt

cmake_minimum_required(VERSION 3.25)
project(x)
set(CMAKE_CXX_STANDARD 20)
add_executable(testbed
    coutRealNumber.cpp
)
add_subdirectory("/Users/chris/work/github/fmt" fmt.out)
target_link_libraries(testbed PUBLIC fmt::fmt)

C++ 关键代码:

    float pi_f32 = 3.1415926;
    std::cout << fmt::format("{}", pi_f32);

运行结果

printf pi=3.141593
std::cout pi=3.14159
fmt::format pi=3.1415925

3. 准确算出被表示的值

3.1 直观感受IEEE-754: float-toy

如果你关心”为什么赋值和打印结果不一样“, 那就需要按 IEEE 754 标准, 按步骤算出取值。

不妨先直观感受下 π 的 fp16, float32, float64 类型的二进制表示, 以及计算出的结果, 用到的在线工具是 float-toy:

在这里插入图片描述

3.2 获取浮点数二进制表示

使用 std::bitset<N> 来表示浮点数的二进制表示。其实整数也可以用它来获取二进制表示。

以 float32 类型为例


class Float32
{
public:
    explicit Float32(float _f) :
        f(_f)
    {
        memcpy(&u, &f, sizeof(float));
        b = std::bitset<32>(u);
    }

    int getSignBit() const
    {
        return b[31];
    }

    std::bitset<8> getExponent() const
    {
        std::bitset<8> exponent;
        for (int i = 31, j = 7; i >= 24; i--, j--)
        {
            exponent[j] = b[i-1];
        }
        return exponent;
    }

    std::bitset<23> getSignificand() const
    {
        std::bitset<23> significand;
        for (int i = 23, j = 22; i >= 1; i--, j--)
        {
            significand[j] = b[i-1];
        }
        return significand;
    }

    std::bitset<32> getBinary() const
    {
        return b;
    }

private:
    std::bitset<32> b;
    unsigned int u;
    float f;
};

int main()
{
    float pi_f32 = 3.141592653589793;

    {
        std::cout << "IEEE 754 single precision example" << std::endl;
        
        Float32 r(pi_f32);

        std::cout << "sign: " << r.getSignBit() << "\n";
        
        std::cout << "exponent: " << r.getExponent().to_string() << "\n";

        std::cout << "significand: " << r.getSignificand().to_string() << "\n";
    }
}

运行结果如下:

IEEE 754 single precision example
sign: 0
exponent: 10000000
significand: 10010010000111111011011

3.3 float 类型

核心公式是:

V = SP * FP * EP
  = (-1)^s * M * 2^E

其中 SP 意思是 sign part, 符号部分;
FP 意思是 fraction part, 小数部分;
EP 意思是 exponent part, 指数部分。

M, E 的具体计算可以翻《CSAPP》这本书。这里只考虑常规的浮点数, 也就是说像 NAN, INF 这样的这里没考虑。

对应的代码实现,在 Float32 类类型中增加成员函数


    float getValue() const
    {
        //return value;
        //return f;
        // V = SP * FP * EP
        //   = (-1)^s * M * 2^E
        
        // SP: OK
        int s = getSignBit();
        int SP = (s == 0) ? 1 : -1;

        // FP: OK
        unsigned int significand = getSignificand().to_ulong();
        float f = significand * (1.0 / (1 << 10));
        float FP = 1.0f + f;
        printf("significand: %d\n", significand);

        // EP: OK
        unsigned int e = getExponent().to_ulong();
        unsigned int Bias = 15; // 2^(k-1) - 1, k = 5
        unsigned E = e - Bias;
        float EP = (1 << E);

        printf("SP: %d\n", SP);
        printf("FP: %lf\n", FP);
        printf("EP: %f\n", EP);

        // TODO: 这里打印的结果, 和 float-toy 对不上
        // 考虑使用 https://github.com/Maratyszcza/FP16/blob/master/include/fp16/fp16.h 作为验证

        float V = SP * FP * EP;
        return V;
    }

3.4 double 类型

和 float 类型的 getValue() 函数大同小异。

这里的插曲是,原版 float-toy 有 bug,至少对于页面默认显示的 π 的 fp16 类型来说, 结果是错的。具体讨论见 https://github.com/evanw/float-toy/issues/9。


class Float64
{
public:
    explicit Float64(double _lf) :
        lf(_lf)
    {
        memcpy(&u, &lf, sizeof(double));
        b = std::bitset<64>(u);
    }

    int getSignBit() const
    {
        return b[63];
    }

    std::bitset<11> getExponent() const
    {
        std::bitset<11> exponent;
        for (int i = 63, j = 10; i >= 53; i--, j--)
        {
            exponent[j] = b[i-1];
        }
        return exponent;
    }

    std::bitset<52> getSignificand() const
    {
        std::bitset<52> significand;
        for (int i = 52, j = 51; i >= 1; i--, j--)
        {
            significand[j] = b[i-1];
        }
        return significand;
    }

    std::bitset<64> getBinary() const
    {
        return b;
    }

    double getValue() const
    {
        // return lf;

        //return f;
        // V = SP * FP * EP
        //   = (-1)^s * M * 2^E
        
        // SP: OK
        int s = getSignBit();
        int SP = (s == 0) ? 1 : -1;

        // FP: OK
        unsigned long long significand = getSignificand().to_ullong();
        double f = significand * (1.0 / (1ULL << 52));
        double FP = 1.0 + f;
        // printf("significand: %d\n", significand);

        // EP: OK
        unsigned long long int e = getExponent().to_ullong();
        unsigned long long int Bias = 1023;
        unsigned long long E = e - Bias;
        double EP = (1ULL << E);

        // printf("SP: %d\n", SP);
        // printf("FP: %lf\n", FP);
        // printf("EP: %f\n", EP);

        double V = SP * FP * EP;

        return V;
    }

private:
    std::bitset<64> b;
    uint64_t u;
    double lf;
};

3.5 fp16 类型

class Float16
{
public:
    explicit Float16(float f)
    {
        memcpy(&u, &f, sizeof(float));
        std::bitset<32> b32(u);

        b[15] = b32[31];

        for (int i = 0; i < 5; i++)
        {
            b[15 - 1 - i] = b32[31 - 1 - i];
        }

        for (int i = 0; i < 10; i++)
        {
            b[10 - 1 - i] = b32[23 - 1 - i];
        }
    }

    int getSignBit() const
    {
        return b[15];
    }

    std::bitset<5> getExponent() const
    {
        std::bitset<5> exponent;
        for (int i = 15, j = 4; i >= 11; i--, j--)
        {
            exponent[j] = b[i-1];
        }
        return exponent;
    }

    std::bitset<10> getSignificand() const
    {
        std::bitset<10> significand;
        for (int i = 10, j = 9; i >= 1; i--, j--)
        {
            significand[j] = b[i-1];
        }
        return significand;
    }

    std::bitset<16> getBinary() const
    {
        return b;
    }

    float getValue() const
    {
        //return value;
        //return f;
        // V = SP * FP * EP
        //   = (-1)^s * M * 2^E
        
        // SP: OK
        int s = getSignBit();
        int SP = (s == 0) ? 1 : -1;

        // FP: OK
        unsigned int significand = getSignificand().to_ulong();
        float f = significand * (1.0 / (1 << 10));
        float FP = 1.0f + f;
        printf("significand: %d\n", significand);

        // EP: OK
        unsigned int e = getExponent().to_ulong();
        unsigned int Bias = 15; // 2^(k-1) - 1, k = 5
        unsigned E = e - Bias;
        float EP = (1 << E);

        printf("SP: %d\n", SP);
        printf("FP: %lf\n", FP);
        printf("EP: %f\n", EP);

        float V = SP * FP * EP;
        return V;
    }

private:
    std::bitset<16> b;
    unsigned int u;
    float value;
};

3.6 验证

int main()
{

    float pi_f32 = 3.141592653589793;
    double pi_f64 = 3.141592653589793;

    {
        std::cout << "IEEE 754 single precision example" << std::endl;
        
        Float32 r(pi_f32);

        std::cout << fmt::format("{}", r.getValue()) << "(0x" << std::hex << r.getBinary().to_ulong() << ")\n";

        std::cout << "sign: " << r.getSignBit() << "\n";
        
        std::cout << "exponent: " << r.getExponent().to_string() << "\n";

        std::cout << "significand: " << r.getSignificand().to_string() << "\n";
    }

    if (1)
    {
        std::cout << "\n";
        std::cout << "IEEE 754 double precision example" << std::endl;

        Float64 r(pi_f64);

        std::cout << fmt::format("{}", r.getValue()) << "(0x" << std::hex << r.getBinary().to_ulong() << ")\n";

        std::cout << "sign: " << r.getSignBit() << "\n";
        
        std::cout << "exponent: " << r.getExponent().to_string() << "\n";

        std::cout << "significand: " << r.getSignificand().to_string() << "\n";
    }

    if (1)
    {
        std::cout << "\n";
        std::cout << "IEEE 754 half precision example" << std::endl;
        
        Float16 r(pi_f32);

        std::cout << fmt::format("{}", r.getValue()) << "(0x" << std::hex << r.getBinary().to_ulong() << ")\n";

        std::cout << "sign: " << r.getSignBit() << "\n";
        
        std::cout << "exponent: " << r.getExponent().to_string() << "\n";

        std::cout << "significand: " << r.getSignificand().to_string() << "\n";
    }

    return 0;
}

结果:

IEEE 754 single precision example
3.1415927(0x40490fdb)
sign: 0
exponent: 10000000
significand: 10010010000111111011011

IEEE 754 double precision example
3.141592653589793(0x400921fb54442d18)
sign: 0
exponent: 10000000000
significand: 1001001000011111101101010100010001000010110100011000

IEEE 754 half precision example
significand: 584
3.140625(0x4248)
sign: 0
exponent: 10000
significand: 1001001000

4. 结论和讨论

  • 使用 format 库获得准确的 float/double 类型的打印
  • 先用 bitset 获得浮点数的二进制表示, 然后根据 IEEE754 标准里的步骤, 算出精确的取值
  • 获取二进制表示的时候,是偷懒做法, 是已经包含了 rounding 处理的过程; 如果打算从头算出二进制表示, 需要对整数和小数部分分别处理, 并手动 rounding。

5. References

  • https://www.cnblogs.com/zjutzz/p/10140559.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/629611.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python教程:如何倒序输出字典?

Python教程&#xff1a;如何倒序输出字典&#xff1f; Python是一种强大的编程语言&#xff0c;广泛用于各种类型的应用程序开发。在开发应用程序时&#xff0c;访问和操作数据是至关重要的一步&#xff0c;而字典是Python中最有用的数据结构之一。字典允许开发人员使用键值对…

Linux安装myql8.0操作步骤

1. 创建software目录&#xff0c;目录可以自定义 mkdir /usr/local/softwar 2. 进入目录software&#xff0c;获取安装包文件 wget https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz 3. 解压文件 tar -vxf mysql-8.0.32-…

【MySQL数据库 | 第十六篇】存储引擎

目录 前言&#xff1a; MySQL体系结构图&#xff1a; 存储引擎简介&#xff1a; 1. InnoDB存储引擎&#xff1a; 2. MyISAM存储引擎&#xff1a; 3. MEMORY存储引擎&#xff1a; 4. NDB Cluster存储引擎&#xff1a; 5. ARCHIVE存储引擎&#xff1a; 存储引擎语法&#…

chatgpt赋能python:Python再次运行快捷键介绍及使用技巧

Python再次运行快捷键介绍及使用技巧 如果你是一名经验丰富的Python工程师&#xff0c;你一定知道如何使用Python快捷键加速代码调试和开发。然而&#xff0c;在实际开发中有许多情况下&#xff0c;你需要再次运行刚刚输入的代码块或函数&#xff0c;这个时候&#xff0c;你必…

JavaWeb开发聊天功能 聊天信息如何实现自动将其他消息上移 最新消息出现在界面下方

问题 JavaWeb开发聊天功能 聊天信息如何实现自动将其他消息上移 最新消息出现在界面下方 详细问题 笔者基于开发JavaWeb开发聊天功能&#xff0c;当用户处于聊天室中&#xff0c;若用户发送一条信息或用户接收到聊天对象的信息&#xff0c;若要查看信息&#xff0c;需要下滑…

Storm超实用教程详解-附示例

一、理论基础 Storm 是一个免费并开源的分布式实时计算系统。利用 Storm 可以很容易做到可靠地处理无限的 数据流&#xff0c;像 Hadoop 批量处理大数据一样&#xff0c;Storm 可以实时处理数据。在Storm中&#xff0c;topology的构建是一个有向无环图。结点就是Spout或者Bolt&…

CKA 04_部署 harbor 仓库 containerd 连接 harbor 仓库 kubeadm 引导集群

文章目录 1. 清空之前的策略1.1 kubeadm 重置1.2 刷新 IPtables 表 2. 查看 Kubernetes 集群使用的镜像3. 搭建 harbor 仓库3.1 部署 docker3.1.1 准备镜像源3.1.2 安装 docker3.1.3 开机自启 docker3.1.4 修改内核参数&#xff0c;打开桥接3.1.5 验证生效 3.2 准备 harbor 仓库…

chatgpt赋能python:Python如何写网站的SEO

Python如何写网站的SEO 在当今的数字时代&#xff0c;网站是公司或个人吸引更多目标受众和客户的重要媒介之一。然而&#xff0c;拥有优秀内容和设计不足以保证流量。搜寻引擎优化(SEO)是一项重要的工作&#xff0c;它帮助网站排名更高&#xff0c;被更多人看到。Python是一种…

chatgpt赋能python:Python技术分享:如何再建立一个文档的SEO

Python技术分享&#xff1a;如何再建立一个文档的SEO Python作为一种高级编程语言&#xff0c;被业内大量使用。它的易用性、跨平台性、语法简单易懂、代码可读性高等特性进一步增强了它的流行度。在使用Python编程时&#xff0c;经常会需要生成文档&#xff0c;使得我们的项目…

软考A计划-系统架构师-官方考试指定教程-(12/15)

点击跳转专栏>Unity3D特效百例点击跳转专栏>案例项目实战源码点击跳转专栏>游戏脚本-辅助自动化点击跳转专栏>Android控件全解手册点击跳转专栏>Scratch编程案例 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&am…

chatgpt赋能python:Python如何免费群发短信

Python如何免费群发短信 在数字化时代&#xff0c;短信成为了快速高效的沟通方式之一。针对群发短信需求&#xff0c;市场上存在着多种短信群发软件&#xff0c;而Python作为一个强大的编程工具&#xff0c;也可以轻松实现免费的短信群发功能。本篇文章将为大家介绍如何通过Py…

从QGIS图层中裁剪需要的区域

GiS数据裁切&#xff0c;创建一个临时图层&#xff0c;通过矢量裁切的方法&#xff0c;将Gis图层进行裁切&#xff1b;影像裁切&#xff0c;将影像图层放置在Gis中&#xff0c;截取影像图以及临时图层的轮廓&#xff0c;放入PS中进行对比&#xff0c;然后将影像图裁切下来。 1…

软件测试01:软件及分类和缺陷的定义

软件测试&#xff1a;软件及分类和缺陷的定义 软件 程序数据文档 软件分类 层次分类 系统软件应用软件组织分类 商业软件开源软件结构分类 单机软件分布式软件(两种&#xff1a;BS服务端架构模型和CS客户端架构模型) 软件缺陷 软件缺陷的由来 起源于上世纪70年代中期 《测…

javaScript蓝桥杯---新增地址

目录 一、介绍二、准备三、目标四、代码五、完成 一、介绍 网购大家应该再熟悉不过&#xff0c;网购中有个很难让人忽略的功能就是地址管理&#xff0c;接下来就让我们通过完善代码来探索下地址管理中常用功能的实现吧&#xff5e; 本题需要在已提供的基础项目中使用 JS 知识…

chatgpt赋能python:Python输入:优秀的函数、方法和技巧

Python输入&#xff1a;优秀的函数、方法和技巧 引言 从文件读取数据&#xff0c;在终端上接收用户输入&#xff0c;在Web应用程序中处理表单数据&#xff0c;这些都是Python中经常需要进行的一些任务。Python内置了许多函数和方法来处理这些任务&#xff0c;但是从众多的选项…

CSS 谈谈你对重排和重绘的理解

一、前言 当我们给我们的DOM结构改变或者给DOM结构设置样式时&#xff0c;会触发回流和重绘&#xff0c;但不同的样式改变&#xff0c;是否触发重排和重绘是不确定的。我们有必要深度理解重排和重绘&#xff0c;通过减少重排可以提高性能。 了解浏览器的解析渲染机制: (1).解析…

C++中的数组理解与应用

数组的数据结构 数组是最基本的数据结构&#xff0c;关于数组的面试题也屡见不鲜&#xff0c;本文罗列了一些常见的面试题&#xff0c;仅供参考。目前有以下18道题目。 数组求和 求数组的最大值和最小值 求数组的最大值和次大值 求数组中出现次数超过一半的元素 求数组中元…

详解c++---二叉搜索树的讲解和模拟实现

目录标题 二分查找的优缺点搜索二叉树的规则搜索二叉树的特性二叉搜索树的性能分析准备工作二叉搜索树的插入函数二叉搜索树的打印函数二叉搜索树的查找函数二叉搜索树的删除函数拷贝构造函数赋值重载析构函数递归版本的find函数递归版本的插入递归的删除方法搜索树的应用模型 …

burpsuite+xray实现联动测试(手动分析和自动化测试同时进行)

目的&#xff1a;安全测试过程中手动分析测试与xray自动化扫描测试结合&#xff0c;这样可以从多层保障安全测试的分析&#xff0c;针对平台业务接口量大的安全测试是十分有用的&#xff0c;可以实现双向测试同时开始。 xray简介 xray 是一款功能强大的安全评估工具&#xff…

MySQL数据库基础 05

第五章 排序与分页 1. 排序数据1.1 排序规则1.2 单列排序1.3 多列排序 2. 分页2.1 背景2.2 实现规则2.3 拓展 1. 排序数据 1.1 排序规则 使用 ORDER BY 子句排序 ASC&#xff08;ascend&#xff09;: 升序DESC&#xff08;descend&#xff09;:降序 ORDER BY 子句在SELECT语句…