Jerkwin分享 http://blog.sciencenet.cn/u/Jerkwin

博文

[译文]任意精度运算的使用原因及方法

已有 7763 次阅读 2013-11-18 04:00 |个人分类:我的工具箱|系统分类:科研笔记| 任意精度, MPFR

[译文]任意精度运算的使用原因及方法
2013-11-17 12:17:02

: 最近需要使用任意精度计算, 查阅资料时发现一篇文章(原文链接)写得浅显易懂, 并有示例代码. 遂译之, 为来者鉴.

 

There's a credibility gap: We don't know how much of thecomputer's answers to believe. Novice computer users solve this problem byimplicitly trusting in the computer as an infallible authority; they tend tobelieve that all digits of a printed answer are signifcant. Disillusionedcomputer users have just the opposite approach; they are constantly afraid thattheir answers are almost meaningless.
                       --Donald E.Knuth

这里存在信任度差异: 我们不知道计算机给出的答案有多少是可信的. 计算机新手完全信任计算机, 把它当作绝不出错的权威, 他们相信打印出的答案中所有的数字都有意义; 大失所望的计算机用户恰恰相反, 他们始终担心自己答案几乎毫无意义.
                       --Donald E.Knuth (高德纳)

 

任意精度运算的使用原因及方法
Kaveh R. Ghazi, Vincent Lefèvre Philippe, Théveny Paul Zimmermann

当今的浮点运算绝大多数采用双精度进行, 具有53比特的有效数字(significand或mantissa,参看附注名词解释). 然而, 有些应用需要使用扩展双精度(64比特或更多), 四精度(113比特)或更高精度. 在发表于2001Astronomical Journal的一篇文章中, Toshio Fukushima写道:"在计算机日益强大的今天, 源于数值积分的误差仍是研究复杂动力系统--如太阳系和一些地外行星的长期稳定性--的主要障碍."他还通过一个实例指出, 对太阳系模拟而言, 使用双精度导致的舍入误差积累起来能够超过1弧度! 此外, 对运行于直升机或核反应堆电子控制系统的浮点程序进行静态分析, 使用任意精度也很必要.

假如我们要求出如下常数的10位有效数字(此例将贯穿本文始终),

$173746a + 94228b - 78487c$
其中$ a = sin(10^{22}), b = log(17.1), c =exp(0.42)$

在这个简单的例子里, 没有输入误差, 因为所有数字的值都精确知道, 具有无限精度.

下面是我们的第一个C语言程序,

#include <stdio.h>
#include <math.h>
int main (void)
{
   double a = sin (1e22), b = log(17.1);
   double c = exp (0.42);
   double d = 173746*a + 94228*b -78487*c;
   printf ("d = %.16en", d);
   return 0;
}

64Core2机器上, 操作系统Fedora10, 利用GCC 4.3.2(GNU libc 2.9)进行编译运行(下文测试平台相同), 我们得到

d = 2.9103830456733704e-11

这个结果完全错误, 因为正确结果为$-1.341818958 cdot 10^{-12}$. 我们可以将上面程序中的double换为long double以使用扩展双精度(64比特有效数字), 并将sin(1e22)换为sinl(1e22L),log换为logl, exp换为expl,%.16e换为%.16Le(1),这样我们会得到

d = -1.3145040611561853e-12

这个新结果几乎错得和第一个一样离谱. 很显然我们计算时使用的精度不够.

1: ARM计算机上,long double与双精度相同; Power PC, long double相应于双双计算(参看附注); Solaris,long double对应于四精度.

1 哪里会出错?

在上面的例子中有些地方能够导致错误.

首先, 常数如1e22,17.10.42等可能无法以二进制精确表示. 对常数1e22,假定编译器能将它转换为正确的二进制数, 并符合IEEE 754的要求(参看附注IEEE 754), 它就能够在双精度下精确表示, 所以不存在这个问题. 然而,17.1并不能以二进制准确表示, 和它最接近的双精度值为$2406611050876109 cdot 2^{-47}$, 这与真值相差$1.4 cdot 10^{-15}$. 0.42也存在类似的问题.

其次, 对一个数学函数(sin)和一个浮点输入($x=10^{22}$),真值$sin x$通常并不能以双精度准确表示. 我们最多能将其舍入为最接近的双精度数$y=-7675942858912663 cdot 2^{-53}$, 误差$y-sin x$约为$6.8cdot 10^{-18}$.

再次, IEEE 754不仅没有规定对数学函数如$sin,log, exp$要进行正确的舍入, 就连精度也没有要求, 这导致计算结果完全取决于所用的平台[3].然而, 虽然IEEE1985版本没有提及这些数学函数, 2008版本中却将正确舍入列为推荐做法. 这样我们计算的$a,b, c$可能会和使用正确舍入方法得到的结果相差一些ulps(units inlast place, 参看附注名词解释).

在我们使用的测试平台上, 无论优化与否(参看第三节), 对由十进制输入舍入为相应二进制数的$x$,三个函数值都进行了正确的舍入.

最后, 在求$173746a+94228b-78487c$值的过程中出现抵消. 假定从左到右进行计算, $ 173746a+94228b $和$ 78487c $分别舍入为
$begin{split}
x&=1026103735669971·2^{-33}
119454.19661583972629 \
y&=4104414942679883
·2^{-35}119454.19661583969719
end{split}$

根据
Sterbenz定理(参看附注名词解释), 计算$x-y$不存在舍入误差. 但最终结果的精确度明显地由计算$x$$y$的舍入误差决定, 约为$ 2^{-36}1.5·10^{-11} $.由于要求的真值与此误差大小相近, 这样就导致了我们的最终计算结果d完全错误.

2 GNU MPFR

论及任意精度, 我们指的是用户能选择每个运算的计算精度(也被称为多精度, 因为这意味着大的有效数字被分储在几个机器字中, 现代计算机一个字中最多能储存64比特, 20位数字). 有一些程序和库能够用于任意精度计算, 大多数计算机代数系统, Mathematica,Maplesage都支持任意精度计算. 本文中我们将重点介绍GNUMFPR, 一个用于任意精度浮点运算的C语言库(其他语言请参看附注其他语言).MFFR的特点在于运算时能够保证正确的舍入. 我们前面的程序可利用MPFR改写为:

#include <stdio.h>
#include <stdlib.h>
#include "mpfr.h"
int main (int argc, char *argv[])
{
   mp_prec_t p = atoi (argv[1]);
   mpfr_t a, b, c, d;
   mpfr_inits2 (p, a, b, c, d,(mpfr_ptr) 0);
   mpfr_set_str (a, "1e22",10, GMP_RNDN);
   mpfr_sin (a, a, GMP_RNDN);
   mpfr_mul_ui (a, a, 173746, GMP_RNDN);
   mpfr_set_str (b, "17.1",10, GMP_RNDN);
   mpfr_log (b, b, GMP_RNDN);
   mpfr_mul_ui (b, b, 94228, GMP_RNDN);
   mpfr_set_str (c, "0.42",10, GMP_RNDN);
   mpfr_exp (c, c, GMP_RNDN);
   mpfr_mul_si (c, c, -78487, GMP_RNDN);
   mpfr_add (d, a, b, GMP_RNDN);
   mpfr_add (d, d, c, GMP_RNDN);
   mpfr_printf ("d =%1.16Ren", d);
   mpfr_clears (a, b, c, d, NULL);
   return 0;
}

这个程序的输入参数为计算精度. $p=53$,运行结果为

d = 2.9103830456733704e-11

这和我们使用双精度得到的结果完全一样.$p=64$, 结果为

d = -1.3145040611561853e-12

与扩展双精度结果一致.$p=113$, 相应于IEEE-754的四精度, 结果为

d = -1.3418189578296195e-12

此结果与真值完全一致.

3 常数合并

在一个给定的程序中, 若一个表达式是常数, $3+ (17 times 42)$, 很可能在编译时就被替换为它的计算值. 这对浮点值也是一样, 却另有难处: 编译器需要能够确定使用的舍入模式. 这种编译器所做的替换被称为常数合并[4].使用正确舍入的常数合并, 所产生的常数只取决于目标平台的浮点格式, 而与创建平台的处理器, 系统, 数学库等无关. 这既保证了正确性(产生的常数具有正确的精度和舍入模式)也保证了可重复性(去除了平台相关性).GCC 4.3版本使用MPFR对内部数学函数, sin,cos, log, sqrt, 实现常数合并. 考虑下面的例子

#include <stdio.h>
#include <math.h>
int main (void)
{
   double x = 2.522464e-1;
   printf ("sin(x) = %.16en",sin (x));
   return 0;
}

使用GCC 4.3.2, 如果编译时关闭优化(使用-O0),我们得到结果2.4957989804940914e-01. 若打开优化(-O1),我们将得到2.4957989804940911e-01. 结果为什么不同? 使用-O0, 表达式sin(x)由数学库计算(使用的是GNUC语言库, 也称为GNUlibcglibc). 使用-O1,GCC将表达式sin(x)识别为常数, 利用最接近舍入模式, 调用MPFR计算sin(x)的值, 并直接将其替换为正确的舍入值(2).正确的值是使用-O1时的值. 需要注意的是, 即便GNUC库并没有对上面的例子进行正确地舍入, 它对大多数值都能进行正确地舍入(在没有扩展精度的计算机上), 正如IEEE 754-2008所推荐的. 在将来, 我们希望对每个输入和函数都能进行正确的舍入.

注意: x86处理器上, GNUC语言库使用x87处理器的fsin实现, $x= 0.2522464$能够给出正确的舍入值. 然而, 这只是巧合, 因为在0.25及更大的$10^7$个双精度数字中, 2452fsin不能给出正确的舍入.

2: 当使用-O1编译时, 我们甚至可以忽略链接数学库. 运行gcc-O1 test.c根本就没有使用数学库. 与此相对, 运行gcc-O0 test.c将会得到一个编译错误, gcc-O0 test.c -lm则正常. 这说明使用-O0时需要数学库. 取消常数合并和其他对内部函数的优化, 可使用gcc-fno-builtin, 或使用更具体的gcc -fno-builtin-sin取消对sin函数的优化.

4 结论

在本文中, 我们可以看到, 使用具有53比特有效数字的双精度变量能够导致远低于53比特精度的最终结果. 导致精度丢失的可能原因包括舍入误差, 数字抵消, 二进制-十进制转换误差, 低数值品质的数学函数,...使用任意精度运算, 例如GNUMPFR, 能够提高最终计算结果的精确度. 更重要的是,MPFR能够对数学函数进行正确地舍入, 这有助于提高在不同处理器, 编译器和操作系统下进行的浮点运算的可重复性, 正如我们在上面GCC常数合并的例子中所展示那样.

 

参考文献
[1] Fousse, L., Hanrot, G., Lefèvre, V., Pélissier, P., and Zimmermann, P.MPFR: A multiple-precision binary floating-point library with correct rounding.ACM Trans. Math. Softw. 33, 2 (2007), article 13.
[2] IEEE standard for floating-point arithmetic. ANSI-IEEE standard 754-2008,2008. Revision of ANSI-IEEE Standard 754-1985, approved June 12, 2008: IEEEStandards Board.
[3] Lefèvre, V. Test of mathematical functions of the standard C library.http://www.vinc17.org/research/testlibm/.
[4] Wikipedia. Constant folding. http://en.wikipedia.org/wiki/Constant_folding.

 

附注

IEEE 754标准
IEEE 754
是一个广泛使用的关于浮点数表示和操作的标准(你的计算机每天都在使用它, 你甚至都没注意到). 它非常重要, 因为其中定义了浮点数格式--能够使两台计算机互相交换浮点值而不损失任何精度--并要求对算术运算正确舍入, 这保证了同样的程序在不同计算机上能够得到完全相同的结果(3).IEEE 7541985年首先通过, 并在2008年进行了修订[2].我们将修订后的这个版本称为IEEE 754-2008. 它定义了基本格式(用于运算)和交换格式(用于不同实现间的数据交换). 共有五种基本格式:binary32, binary64, binary128, decimal64decimal128.binary32binary64分别促生了单精度和双精度, 通常对应于ISOC语言的floatdouble数据类型. 十进制格式是IEEE754-2008中新定义的, GCC只提供了初步的支持. 例如,decimal64GCC中被标记为_Decimal64,与目前TR 24732草稿中关于C语言十进制浮点算术的规定一致(4).把我们的示例程序改写成:

#include <stdio.h>
#include <math.h>
int main (void)
{
   _Decimal64 a = sin (1e22);
   _Decimal64 b = log (17.1);
   _Decimal64 c = exp (0.42);
   _Decimal64 d =173746*a+94228*b-78487*c;
   printf ("d = %.16en",(double) d);
   return 0;
}

得到结果

d = 0.0000000000000000e+00

(注意必须把最终结果d转换为double,因为GNU C语言库还不支持十进制格式输出).

IEEE 754-2008要求对四类基本算术运算(+,-,×,÷), 平方根和基数转换(如将十进制字符串读入为二进制格式, 或将二进制格式输出为十进制字符串)进行正确地舍入. 这意味着计算结果好像应该以无限精度进行, 然后再按相应的舍入模式进行舍入.IEEE 754-2008明确了五种舍入模式(或属性): roundTowardPositive, roundTowardNegative, roundTowardZero, roundTiesToEvenroundTiesToAway.

3: 需要一些我们在这里忽略的条件
4:http://www.open-std.org/jtc1/sc22/wg14/www/projects

 

扩展精度和Linux
传统的32x86处理器的浮点运算单元能够设定为对计算结果以双精度(53比特有效数字)或扩展精度(64比特有效数字)进行舍入. 大多数操作系统, FreeBSD,NetBSDMicrosoft Windows, 默认都选择以双精度进行舍入. Linux默认以扩展精度进行舍入. 这是一个坏的选择, 详细的原因请参看http://www.vinc17.org/research/extended.en.html.

 

名词解释

基数, 有效数字和指数
$x$是一个浮点数, 精度为$p$,基数为$beta$, 它能被写为$x= pm 0.d_1d_2 . . . d_p cdot beta^e$, $s = pm 1$$x$的符号.$m = 0.d_1d_2 . . . d_p$$x$的有效数字,$e$$x$的指数. 注意, 这种表示方法并不唯一, 除非要求$d_1$为非零值. 此外, 有效数字可以采用不同的约定, 并因此而改变指数的值. 例如,IEEE 754-2008使用$m = d_1.d_2 . . . d_p$约定, 使得其指数比我们定义的小1. 它也使用了第三个约定,$m$是一个整数.

Unit in lastplace
$x = pm 0.d_1 d_2 . . . d_p cdotbeta^e$是一个浮点数, 定义$mathrm{ulp}(x)$$x$最小有效数字的权重, $beta^{e-p}$.(注意$mathrm{ulp}(x)$的值并不取决于$(s,m,e)$约定)

Sterbenz定理
$x$$y$是两个浮点数, 精度为$p$,并满足$y/2 ll x ll 2y$, $x-y$能够以精度$p$准确表示. 作为推论, 计算$x-y$时没有舍入误差.

双双算术
将一个实数$r$近似为两个双精度数的和,$x+y$. $x$$r$的最近舍入,$y$$r-x$的最近舍入, 双双算术的精度是使用单个双精度书的两倍.

其他语言
CC++, 其他一些语言也支持任意精度浮点运算.Perl, Python, Haskell, LispUrsala都有MPFR的接口(详情请参看mpfr.org).Fortran中使用MPFR并不容易, 因为必须首先将MPFR的数据类型转换为Fortran支持的. 但是, 如果你需要计算的只是双精度数的指数函数的正确舍入, 很容易, 参看http://www.loria.fr/~zimmerma/mpfr/fortran.html.

如何在Fortran中使用MPFR
Credit: thanks to Olivier Cessenat (CEA) for his help.

首先, 创建一个C文件,crexp.c

#include <mpfr.h>
double exp_(double *x) {
 double y ;
 mpfr_t z ;
 mpfr_init2 (z, 53);
 mpfr_set_d(z, *x, GMP_RNDN) ;
 mpfr_exp(z, z, GMP_RNDN) ;
 y = mpfr_get_d(z, GMP_RNDN) ;
 mpfr_clear(z) ;
 return(y) ;
}

然后, 创建一个Fortran程序,toto.f90

program toto
 real(kind=8) :: x, y
 real(kind=8),external :: exp
 x = 1.D0
 y = exp(x)
 print*,'y=',y
end program toto

编译

gcc -c crexp.c -o crexp.o
gfortran -c toto.f90 -o toto.o
gfortran -o a.out toto.o crexp.o -lmpfr

运行

./a.out

 

译后记:

查看MPFR, 发现awk4.1版本已经支持MPFR, 而且使用非常简单. 这对我这个awk的深度用户来说, 值得一记, 也让我对awk的喜爱更添了几分.




https://wap.sciencenet.cn/blog-548663-742642.html

上一篇:微通道板出射电子云团的弹道模型
下一篇:卷积与自由能
收藏 IP: 72.204.51.*| 热度|

2 张昕尧 阎凯

该博文允许注册用户评论 请点击登录 评论 (0 个评论)

数据加载中...
扫一扫,分享此博文

Archiver|手机版|科学网 ( 京ICP备07017567号-12 )

GMT+8, 2024-5-15 11:22

Powered by ScienceNet.cn

Copyright © 2007- 中国科学报社

返回顶部