在32位小数中分析浮点/双精度的精度

4

我看到另一个人的.c文件中有这个:

const float c = 0.70710678118654752440084436210485f;

他想避免计算sqrt(1/2),这与IT技术有关。

这是否真的可以使用纯C/C++存储?我是说不丢失精度。对我来说似乎不可能。

我正在使用C ++,但我不相信这两种语言之间的精度差异太大(如果有的话),所以我没有测试它。

因此,我编写了这几行代码,以查看代码的行为:

std::cout << "Number:    0.70710678118654752440084436210485\n";

const float f = 0.70710678118654752440084436210485f;
std::cout << "float:     " << std::setprecision(32) << f << std::endl;

const double d = 0.70710678118654752440084436210485; // no f extension
std::cout << "double:    " << std::setprecision(32) << d << std::endl;

const double df = 0.70710678118654752440084436210485f;
std::cout << "doublef:   " << std::setprecision(32) << df << std::endl;

const long double ld = 0.70710678118654752440084436210485;
std::cout << "l double:  " << std::setprecision(32) << ld << std::endl;

const long double ldl = 0.70710678118654752440084436210485l; // l suffix!
std::cout << "l doublel: " << std::setprecision(32) << ldl << std::endl;

输出结果如下:
                   *       ** ***
                   v        v v
Number:    0.70710678118654752440084436210485    // 32 decimal digits
float:     0.707106769084930419921875            // 24 >>      >>
double:    0.70710678118654757273731092936941
doublef:   0.707106769084930419921875            // same as float
l double:  0.70710678118654757273731092936941    // same as double
l doublel: 0.70710678118654752438189403651592    // suffix l

其中*float最后一个准确数字,**double最后一个准确数字,***long double最后一个准确数字。

double的输出有32个小数位,因为我已将std::cout的精度设置为该值。

float的输出有24个小数位,如此处所述:

float has 24 binary bits of precision, and double has 53.

我希望最后的输出结果与倒数第二个输出结果相同,即f后缀不会阻止数字变成double类型。我认为当我写下这段代码时:

const double df = 0.70710678118654752440084436210485f;

发生的情况是,首先将数字变成一个 float,然后存储为一个 double,因此在第24位小数之后,它有零,这就是为什么 double 精度停止的原因。
我理解正确吗?
这个答案中,我找到了一些相关信息。
float x = 0 has an implicit typecast from int to float.
float x = 0.0f does not have such a typecast.
float x = 0.0 has an implicit typecast from double to float.

[编辑]

关于__float128,它不是标准的,因此已经退出了竞争。更多信息请参见这里


2
你缺少一个类型:long double - Some programmer dude
1
一旦您使用基本表示法来计算sqrt(1/2),无论如何都会失去精度!!! - barak manos
尝试使用 long double,它是 扩展双精度(80到96位宽)的。 - 0xF1
你可以轻松地将它们存储为文本。你需要这个做什么?也许你需要一些高精度浮点库? - zch
1
@别担心,长双精度在x86上是80位,在x86上填充后变为96位,而在x86_64上可能是128位,因此剩下的位没有意义。 - phuclv
显示剩余3条评论
3个回答

5
从标准来看:
有三种浮点类型:float、double和long double。类型double提供的精度至少与float相同,而类型long double提供的精度至少与double相同。类型float的值集是类型double的值集的子集;类型double的值集是类型long double的值集的子集。浮点类型的值表示是实现定义的。
因此,您可以看到这个问题的难点在于:标准实际上并没有说明浮点数的精度。
就标准实现而言,您需要查看IEEE754,这意味着Irineau和Davidmh的另外两个答案是解决问题的完全有效的方法。
至于后缀字母指示类型,再次查看标准:
浮点文字的类型为double,除非通过后缀明确指定。后缀f和F指定float,后缀l和L指定long double。
因此,除非使用L后缀,否则您尝试创建的long double将具有与分配给它的double文字相同的精度。
我理解这些答案中的一些可能看起来不令人满意,但是在您可以驳回答案之前,必须对相关标准进行大量的背景阅读。这个答案已经比预期的长了,所以我不会在这里尝试解释所有内容。
最后一点:由于精度没有明确定义,为什么不使用比所需精度更长的常量呢?似乎总是定义一个足够精确以表示任何类型的常量是有意义的。

我认为如果您在答案中写下这个评论,答案会更好。我已经点赞了,谢谢。 - gsamaras
我总体上同意“为什么不使用比所需更长的常量?” 但这里有一个缺陷。虽然给定的常量对于经典的32位浮点数(binary32)和64位双精度浮点数(binary64)肯定是好的,但对于二进制128则短了约2-3个十进制数字。当看到这段代码时,很容易说“看起来适用于长双精度-它有很多数字”,但实际上它是不足的。 - chux - Reinstate Monica
最好将大小调整为辅音类型的精度(+ 3个小数位)或下一个更大的类型(+ 3个小数位),但是粘贴大量数字(这恰好正确到下一个典型精度减去3个数字)是一个隐藏的问题。 - chux - Reinstate Monica
@chux,我认为在大多数情况下,long double 实际上是双倍扩展精度,因为大多数处理器都有 80 位 FPU 寄存器。从快速测试中可以看出,在我的电脑上,long double 存储为 16 字节,但仅使用双倍扩展精度(80 位)进行数学运算,此外,float.h 也证实了这一点,将 LDBL_MAX_10_EXP 给出为 4932,而不是二进制 128 浮点数的 16383。然而,我同意你的原则,即该常量不留下未来潜在类型的空间... - Matt
@chux,C仍然支持类型后缀。这意味着即使您对32位浮点数进行算术运算,它仍将加载到80位fpu寄存器中。在算术运算之后,它会被截断回32位浮点数。 C标准有一个类似的段落,与C++中的浮点常量后缀相似。 - Matt
显示剩余7条评论

1
Python的数字库numpy有一个非常方便的float info函数。所有类型都等同于C语言:
对于C语言的float类型:
print numpy.finfo(numpy.float32)
Machine parameters for float32
---------------------------------------------------------------------
precision=  6   resolution= 1.0000000e-06
machep=   -23   eps=        1.1920929e-07
negep =   -24   epsneg=     5.9604645e-08
minexp=  -126   tiny=       1.1754944e-38
maxexp=   128   max=        3.4028235e+38
nexp  =     8   min=        -max
---------------------------------------------------------------------

对于C语言中的double类型:
print numpy.finfo(numpy.float64)
Machine parameters for float64
---------------------------------------------------------------------
precision= 15   resolution= 1.0000000000000001e-15
machep=   -52   eps=        2.2204460492503131e-16
negep =   -53   epsneg=     1.1102230246251565e-16
minexp= -1022   tiny=       2.2250738585072014e-308
maxexp=  1024   max=        1.7976931348623157e+308
nexp  =    11   min=        -max
---------------------------------------------------------------------

对于 C 语言的长浮点数:

print numpy.finfo(numpy.float128)
Machine parameters for float128
---------------------------------------------------------------------
precision= 18   resolution= 1e-18
machep=   -63   eps=        1.08420217249e-19
negep =   -64   epsneg=     5.42101086243e-20
minexp=-16382   tiny=       3.36210314311e-4932
maxexp= 16384   max=        1.18973149536e+4932
nexp  =    15   min=        -max
---------------------------------------------------------------------

因此,即使是长浮点数(128位),也无法为您提供所需的32位数字。但是,您真的需要全部吗?


1
四倍精度的小数精度约为34。http://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format - lrineau
我使用Python是因为它有这个非常方便的函数。所有类型都对应于C类型,所以对它们来说真实的东西也适用于你。事实上,它们只是遵循IEEE标准。 - Davidmh
2
@Davidmh:NumPy的float128并不一定对应于C语言中的long doublelong double使用的位数没有规定。在许多(但不是所有)x86平台上,它使用由其FPU提供的80位扩展精度模式。当然,没有规定它提供四倍精度。因此,基于NumPy对其float128类型精度的(确实有用的)分析得出结论是徒劳无功的。话虽如此,其“128位”数据类型报告的参数更像是80位所期望的。 - Jason R
2
@Davidmh:在 C 语言中不存在 long float 这种数据类型。 - Jason R
1
这个标题为"For C's float:", "For C's double:"等的信息是误导性的。这些数据代表了C浮点数特性的一个样本。这些特性在不同的C机器之间是不同的。C规范并没有像所示的那样定义这些值。 - chux - Reinstate Monica
显示剩余5条评论

1
一些编译器实现了符合IEEE 754-2008标准的binary128浮点格式,例如使用gcc时,该类型为__float128。这种浮点格式具有约34位十进制精度(log(2^113)/log(10))。
您可以使用Boost Multiprecision库,使用其包装器float128。该实现将使用本机类型(如果可用),或使用一个替代品。
让我们通过最新的g++(4.8)扩展您的实验,使用这种新的非标准类型__float128
// Compiled with g++ -Wall -lquadmath essai.cpp
#include <iostream>
#include <iomanip>
#include <quadmath.h>
#include <sstream>

std::ostream& operator<<(std::ostream& out, __float128 f) {
  char buf[200];
  std::ostringstream format;
  format << "%." << (std::min)(190L, out.precision()) << "Qf";
  quadmath_snprintf(buf, 200, format.str().c_str(), f);
  out << buf;
  return out;
}

int main() {
  std::cout.precision(32);
  std::cout << "Number:    0.70710678118654752440084436210485\n";

  const float f = 0.70710678118654752440084436210485f;
  std::cout << "float:     " << std::setprecision(32) << f << std::endl;

  const double d = 0.70710678118654752440084436210485; // no f extension
  std::cout << "double:    " << std::setprecision(32) << d << std::endl;

  const double df = 0.70710678118654752440084436210485f;
  std::cout << "doublef:   " << std::setprecision(32) << df << std::endl;

  const long double ld = 0.70710678118654752440084436210485;
  std::cout << "l double:  " << std::setprecision(32) << ld << std::endl;

  const long double ldl = 0.70710678118654752440084436210485l; // l suffix!
  std::cout << "l doublel: " << std::setprecision(32) << ldl << std::endl;

  const __float128 f128 = 0.70710678118654752440084436210485;
  const __float128 f128f = 0.70710678118654752440084436210485f; // f suffix
  const __float128 f128l = 0.70710678118654752440084436210485l; // l suffix
  const __float128 f128q = 0.70710678118654752440084436210485q; // q suffix

  std::cout << "f128:      " << f128 << std::endl;
  std::cout << "f f128:    " << f128f << std::endl;
  std::cout << "l f128:    " << f128l << std::endl;
  std::cout << "q f128:    " << f128q << std::endl;
}

输出如下:
                   *       ** ***        ****
                   v        v v             v
Number:    0.70710678118654752440084436210485
float:     0.707106769084930419921875
double:    0.70710678118654757273731092936941
doublef:   0.707106769084930419921875
l double:  0.70710678118654757273731092936941
l doublel: 0.70710678118654752438189403651592
f128:      0.70710678118654757273731092936941
f f128:    0.70710676908493041992187500000000
l f128:    0.70710678118654752438189403651592
q f128:    0.70710678118654752440084436210485

其中,*float的最后一个准确数字,**double的最后一个准确数字,***long double的最后一个准确数字,****__float128的最后一个准确数字。
正如另一个答案所说,C++标准并没有规定各种浮点类型(就像它没有规定整数类型的大小一样)的精度。它只指定了这些类型的最小精度/大小。但是,IEEE754规范确实规定了这一切!很多体系结构的FPU都实现了该规范IEEE745,并且最近的gcc版本通过扩展引入了规范的binary128类型__float128
关于你的代码或者我的代码的解释,像 0.70710678118654752440084436210485f 这样的表达式是浮点型字面量。它有一个类型,由后缀定义,在这里是 f 表示 float。因此,字面量的值对应于给定类型从给定数字中最接近的值。这就解释了为什么在你的代码中 "doublef" 的精度与 "float" 相同。在最新的 gcc 版本中,有一种扩展,允许使用 Q 后缀(四倍精度)定义类型为 __float128 的浮点型字面量。

请看我的编辑。我无法打印__float128,因为std::cout会显示“operator<<”的歧义重载。此外,即使我们打印它(https://dev59.com/Am035IYBdhLWcg3wYfAF也没有帮助),即使它达到了精确的精度,我仍然想了解上面实验中发生了什么。 - gsamaras
说实话,我真的想打印它,这样我就可以在上面增加我的分析了。如果我们能做到这一点,我认为这个答案至少值得一个+1。 - gsamaras
1
除了存储浮点值本身之外,您还有另一个问题:如何指定具有足够精度以提供所有所需数字的文字初始化程序。如果您正在使用C++11,则可以使用用户定义字面量来实现此目的。我不确定Boost.Multiprecision是否已经添加了这个功能。 - Jason R
2
即使您提供了足够的精度,使用 f 后缀也将像浮点数一样被截断为 24 位,而不是 106 位。 - phuclv
@Lưu Vĩnh Phúc,“f”并不一定截断到24位。它确保该数字将被视为“float”。C语言没有定义“float”具有24位,但至少有6个十进制数字的精度。考虑一个将所有FP视为相同大小且该大小为“binary128”的系统。在这种情况下,不会发生截断,并且常量与sqrt(0.5)的距离不如应该的那样接近。 - chux - Reinstate Monica
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接