[C/C++]C语言中math.h和cmath的pow()精度问题
来源:互联网 发布:haskell linux 编辑:程序博客网 时间:2024/05/22 15:34
帮小朋友们DEBUG的时候,他们有个题无论怎么提交OJ都不给过。
我回来后想了想,估计是因为math.h库返回值转int时精度丢失的问题。
>测试代码
#include <stdio.h>#include <math.h>//MinGW GCC 4.7.2 32-bit Releaseint main(){ printf("math.h - double pow(double, double) 精度测试\n"); int a=3; printf("%d\n",(int)pow(5,3));//1.输出125 printf("%d\n",(int)pow(5,a));//2.输出124 这里丢精度了,结合下面的[3],我估计最后的结果是float->int 124.999999999999 printf("%d\n",(int)round(pow(5,a)));//3.输出125 在[2]的基础上补上round()四舍五入函数,结果正常 printf("%lf\n",pow(5,a));//4.输出125.000000 显然,如果不转型成int,结果是没问题的 return 0;}
>pow的精度问题研究
math.h库里,pow函数是基于浮点运算的。
试着找了一圈,没有找到源码,只在一些犄角旮旯里看到有人提到是在x86指令机上利用log和exp运算求出来的。也就是:
double pow(double a, double b){ //我这里简化了这个过程 大概写个伪码 return x86_exp(a * x86_log(b));// 也就是:e^(a*ln(b))}stackoverflow上也有关于这个问题的描述:
https://stackoverflow.com/questions/14104711/what-algorithm-is-used-to-pow-function-in-c
这个函数的具体实现过程则不得而知了。
上面那段代码玄学的地方是这两行:
int a=3; printf("%d\n",(int)pow(5,3));//输出125 printf("%d\n",(int)pow(5,a));//输出124
这就是那种看上去都没错,但是一运行发现特么居然不对的神坑。
你要说是幂参数a转到double b时转类型除了问题导致值不一样吧...那为啥单独给个数字3就没有任何计算问题。
所以我想,要不然测试一下常量?
const int b = 3; printf("%d\n",(int)pow(5,b));//输出125 没错,这次是125了orz
看着好像没啥区别,就是存的地址不一样。那好吧,已知一个int四个字节,我多定义几个试试:
int a=3,a2=3,a3=3,a4=3;const int b=3,b2=3,b3=3,b4=3;
于是:
每次增加4,至少在内存大小分配上const int 和 int 真的没啥区别。
那好吧,我们再对比一下这几句的汇编:
int a=3; printf("%d\n",(int)pow(5,3));//输出125 I printf("%d\n",(int)pow(5,a));//输出124 II const int b = 3; printf("%d\n",(int)pow(5,b));//输出125 III
int a的汇编是:
0x004013be <+14>:movl $0x3,0x2c(%esp)首先是I:printf("%d\n",(int)pow(5,3));
0x004013c6 <+22>:movl $0x7d,0x4(%esp) 0x004013ce <+30>:movl $0x403064,(%esp) 0x004013d5 <+37>:call 0x401c80 <printf>然后是II:printf("%d\n",(int)pow(5,a));
0x004013da <+42>:fildl 0x2c(%esp) 0x004013de <+46>:fstpl 0x8(%esp) 0x004013e2 <+50>:mov $0x0,%eax 0x004013e7 <+55>:mov $0x40140000,%edx 0x004013ec <+60>:mov %eax,(%esp) 0x004013ef <+63>:mov %edx,0x4(%esp) 0x004013f3 <+67>:call 0x401c88 <pow>//注意这里是调用了pow函数 0x004013f8 <+72>:fnstcw 0x1e(%esp)// 开始转返回值(其实这个就是一般操作) 0x004013fc <+76>:mov 0x1e(%esp),%ax 0x00401401 <+81>:mov $0xc,%ah 0x00401403 <+83>:mov %ax,0x1c(%esp) 0x00401408 <+88>:fldcw 0x1c(%esp) 0x0040140c <+92>:fistpl 0x18(%esp) 0x00401410 <+96>:fldcw 0x1e(%esp) 0x00401414 <+100>:mov 0x18(%esp),%eax//最后存到0x18xxx去了 0x00401418 <+104>:mov %eax,0x4(%esp) 0x0040141c <+108>:movl $0x403064,(%esp) 0x00401423 <+115>:call 0x401c80 <printf>
嚯,真有够长的,再看看III吧:printf("%d\n",(int)pow(5,b));
0x00401428 <+120>:movl $0x3,0x28(%esp)//这行做的是const int b = 3; 0x00401430 <+128>:movl $0x7d,0x4(%esp)//接下来是printf 0x00401438 <+136>:movl $0x403064,(%esp) 0x0040143f <+143>:call 0x401c80 <printf>III和I看起来一模一样,也难怪输出的都是125 了。
那么问题又来了,为啥I和III都没有call <pow>。假如我把II注释掉,再去看汇编,是这样的:
=> 0x004013be <+14>:movl $0x3,0x1c(%esp) // I 0x004013c6 <+22>:movl $0x7d,0x4(%esp) 0x004013ce <+30>:movl $0x403064,(%esp) 0x004013d5 <+37>:call 0x401c30 <printf>=> 0x004013da <+42>:movl $0x3,0x18(%esp) // III 0x004013e2 <+50>:movl $0x7d,0x4(%esp) 0x004013ea <+58>:movl $0x403064,(%esp) 0x004013f1 <+65>:call 0x401c30 <printf>
是的,依旧没有调用pow。为啥???这结果是怎么运算出来的?
本着科学的精神,我做了这个测试:
果然依旧没有调用pow。好吧,先放过这个问题...毕竟我的专精不在C的编译和汇编上,也许是有什么我尙不了解的知识点我还没了解到,改天去问问写C的底层大佬。
还是回归正题,我们去考虑一下II中调用了pow进行幂运算的误差问题,毕竟I II III中,只有调用了pow的II输出了124这个错误的值。出于好奇,我去看了一下机器实现浮点运算的方法。另外,对于pow原生的参数类型double做以下测试:
double s=5.000,sb=3.0; printf("%d",(int)pow(s,sb));有:
0x004013be <+14>:mov $0x0,%eax 0x004013c3 <+19>:mov $0x40140000,%edx 0x004013c8 <+24>:mov %eax,0x28(%esp) 0x004013cc <+28>:mov %edx,0x2c(%esp) //double s=5.0 0x004013d0 <+32>:mov $0x0,%eax 0x004013d5 <+37>:mov $0x40080000,%edx 0x004013da <+42>:mov %eax,0x20(%esp) 0x004013de <+46>:mov %edx,0x24(%esp) //double sb=3.0 0x004013e2 <+50>:mov 0x20(%esp),%eax //s 0x004013e6 <+54>:mov 0x24(%esp),%edx 0x004013ea <+58>:mov %eax,0x8(%esp) 0x004013ee <+62>:mov %edx,0xc(%esp) 0x004013f2 <+66>:mov 0x28(%esp),%eax //sb 0x004013f6 <+70>:mov 0x2c(%esp),%edx 0x004013fa <+74>:mov %eax,(%esp) 0x004013fd <+77>:mov %edx,0x4(%esp) 0x00401401 <+81>:call 0x401c70 <pow> 0x00401406 <+86>:fnstcw 0x1e(%esp) 0x0040140a <+90>:mov 0x1e(%esp),%ax 0x0040140f <+95>:mov $0xc,%ah 0x00401411 <+97>:mov %ax,0x1c(%esp) 0x00401416 <+102>:fldcw 0x1c(%esp) 0x0040141a <+106>:fistpl 0x18(%esp) 0x0040141e <+110>:fldcw 0x1e(%esp) 0x00401422 <+114>:mov 0x18(%esp),%eax 0x00401426 <+118>:mov %eax,0x4(%esp) 0x0040142a <+122>:movl $0x403064,(%esp) 0x00401431 <+129>:call 0x401c78 <printf>对比一下,基本可以确定就是传参int a的时候的问题。传参的不同,在经过pow内部的运算会产生不同的结果。由于现在pow是一个黑盒(不知道源码),我们只好动手算算按int传参之后的后果了。我们假设stackoverflow上给出的pow内部运算方法是对的,按照IEEE754对单双精度的定义及刚刚stackoverflow里某人推测给出pow的运算方法:
fld1 // 1.0fld // yfld // x fyl2x // y * log2(x)fadd // add 1.0f2xm1 // 2 ^ (x-1)
- [C/C++]C语言中math.h和cmath的pow()精度问题
- C语言中关于.h和.c的问题
- eclipse编辑器中,如何配置编译命令-lm,使math.h 和pow(a,b)函数生效——C语言学习笔记4
- C语言中关于pow()函数的问题
- c语言math.h 库函数
- C语言math库#include<math.h>
- C/C++笔试必须熟悉掌握的头文件系列(二)——math.h/cmath
- C/C++笔试必须熟悉掌握的头文件系列(二)——math.h/cmath
- C语言中float和double的精度
- C语言中 *.c和*.h文件的区别!
- C语言中 *.c和*.h文件的区别!
- C语言中 *.c和*.h文件的区别!
- c语言中.c和.h文件的困惑
- C语言中 c和h文件的区别!
- C语言中 *.c和*.h文件的区别
- C语言标准库--math.h
- C语言标准库--math.h
- C语言math.h中的常用函数
- W: 无法下载 http://ppa.launchpad.net/fcitx-team/nightly/ubuntu/dists/jessie/main/binary-amd64/Packages
- 关于ListView中adapter调用notifyDataSetChanged失效的原因总结
- 使用table表格设计调查表
- 手机号验证,为jquery-validation添加自定义验证方式,以及Ajax提交form表单
- [题解]NOIP2017 Day2 Solution
- [C/C++]C语言中math.h和cmath的pow()精度问题
- MySQL通过.frm和.ibd恢复表结构和数据
- C#类的封装
- kotlin语法分析(一)
- spring boot实现session 同步共享及 根据sessionid 获取相应的session
- 瞎脑洞毒瘤题
- kotlin语法分析(二)
- 调试符号环境变量正确配置方法
- kotlin 语法分析(三) -- 类引用