惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
The Exploit Database - CXSecurity.com
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Cloudflare Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security Affairs
N
News and Events Feed by Topic
S
Security @ Cisco Blogs
Schneier on Security
Schneier on Security
Attack and Defense Labs
Attack and Defense Labs
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
N
News and Events Feed by Topic
Recent Commits to openclaw:main
Recent Commits to openclaw:main
WordPress大学
WordPress大学
Security Latest
Security Latest
Apple Machine Learning Research
Apple Machine Learning Research
L
LINUX DO - 最新话题
博客园 - 三生石上(FineUI控件)
Scott Helme
Scott Helme
U
Unit 42
Cyberwarzone
Cyberwarzone
博客园_首页
F
Full Disclosure
月光博客
月光博客
罗磊的独立博客
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
小众软件
小众软件
Recent Announcements
Recent Announcements
Stack Overflow Blog
Stack Overflow Blog
C
Check Point Blog
The Hacker News
The Hacker News
Help Net Security
Help Net Security
GbyAI
GbyAI
Y
Y Combinator Blog
博客园 - 聂微东
Security Archives - TechRepublic
Security Archives - TechRepublic
Jina AI
Jina AI
S
Schneier on Security
Martin Fowler
Martin Fowler
The Last Watchdog
The Last Watchdog
D
DataBreaches.Net
T
Tor Project blog
V
Visual Studio Blog
博客园 - 【当耐特】
T
Troy Hunt's Blog
G
Google Developers Blog
有赞技术团队
有赞技术团队
Google DeepMind News
Google DeepMind News

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
又一个类型提升引起的Bug
重归混沌 · 2018-05-20 · via 重归混沌的BLOG

在好几年前我已经中招过一次了, 没想到最近一不留神又中招一次。不过这次的花样和上次又不太一样。

Bug的起因是,我需要一个函数,根据指定速度(可能不是整数)和距离来获取到达目的点的时间,于是就有了下面这样一段代码。

//#define TIME time(NULL)
#define TIME 1526796356
time_t foo(int distance, float speed)
{
        return TIME + distance / speed;
}
int main()
{
        printf("%ld\n", foo(30, 3.0f));
}

咋一看这代码几乎没毛病,严格遵循牛顿大神给出来的公式来算。

然而他的输出却是`1526796416`这样一个值。在思考了将近20分钟之后,我才恍然大悟。

根据《The C Programming Language》第二版中的A6.5节算术转换一节的内容

`
First, if either operand is long double, the other is converted to long double. Otherwise, if either operand is double, the other is converted to double.

Otherwise, if either operand is float, the other is converted to float.

Otherwise, the integral promotions are performed on’ both operands; then, if either operand is unsigned long int, the other is converted to unsigned long int

Otherwise, if one operand is long int and the other is unsigned int, the effect depends on whether a long int can represent all values of an unsigned int; if so, the unsigned int operand is converted to long int; if not, both are converted to unsigned long int

Otherwise, if one operand is long int, the other is converted to long int

Otherwise, if either operand is unsigned int, the other is converted to unsigned int

Otherwise, both operands have type int

There are two changes here. First, arithmetic on float operands may be done in single precision, rather than double; the first edition specified that all floating arithmetic was double precision. Second, shorter unsigned types, when combined with a larger signed type, do not propagate the unsigned property to the result type; in the first edition, the unsigned always dominated. The new rules are slightly more complicated, but reduce somewhat the surprises that may occur when an unsigned quantity meets signed. Unexpected results may still occur when an unsigned expression is compared to a signed expression of the same size.
`

代码`return TIME + distance / speed`在实际执行时会被转换成`return (float)TIME + (float)distance / speed`来执行。

之所以思考了那么久才发现问题,是因为在写代码时就知道编译器会进行类型提升。而从理论上来讲,编译器在进行数学运算时,总是会向最大值更大的类型进行转换,以保证隐式转换的正确性。所以首先就把这种错误是类型提升造成的可能给排除了。

这种理论本身也是正确的,因为float的最大值为`340282346638528859811704183484516925440`,可是我忽略了一个很重要的事实,就是float即然叫单精度,就说明它本身是有精度的。


来看一下float的构成.


Float example.svg

float由1个符号位,8个指数位和23位尾数位构成,而精度完全是由23位尾数控制的,因此虽然float可以表示很大,但那是通过指数位进行跳跃(乘以2的N次方)来得到了,换句话说,float能表示的数其实是不连续的,而我们现在的time(NULL)值`1526796356(0101 1011 0000 0001 0000 0100 0100b)`已经使用了31位二进制了,它的二进制有效数字位为29位,已经超出了float中的尾数位。因此在int转向float时必然会丢失精度。

弄清楚了问题之后,只需要加个类型强制转换就可以解决问题了:

//#define TIME time(NULL)
#define TIME 1526796356
time_t foo(int distance, float speed)
{
        return TIME + (time_t)(distance / speed);
}

BTW. 在查阅文档时,发现了一个有意思的事,《The C Program Language》第二版 在算术类型转换一节中特意标注,在第一版中,所有的float类型算术运算都是先转换成double再进行,而第二版的描述是float类型算术运算‘有可能’直接进行,而不是转换成double之后再进行。