在我的代码中,我经常计算类似下面的内容(这里为简单的C代码):
float cos_theta = /* some simple operations; no cosf call! */;
float sin_theta = sqrtf(1.0f - cos_theta * cos_theta); // Option 1
对于此示例,请忽略由于不精确而导致平方根的参数可能为负。我通过额外的fdimf
电话修正了这个问题。但是,我想知道以下是否更精确:
float sin_theta = sqrtf((1.0f + cos_theta) * (1.0f - cos_theta)); // Option 2
cos_theta
介于-1
和+1
之间,因此对于每个选项都会有一些情况我会减去相似的数字,因此会失去精度,对吗? 最准确的是什么?为什么?
答案 0 :(得分:4)
浮点数最精确的方法是使用单个x87指令计算sin和cos, fsincos 。
但是,如果您需要手动进行计算,最好对具有相似幅度的参数进行分组。这意味着第二个选项更精确,特别是当 cos_theta
接近0时,精度最重要。
正如文章所述 What Every Computer Scientist Should Know About Floating-Point Arithmetic注意:
表达式x 2 - y 2 是另一个表现出灾难性的公式 消除。将其评估为(x - y)(x + y)更准确。
编辑:它比这更复杂。虽然上述情况一般都是正确的,但是当x和y的幅度差别很大时,(x - y)(x + y)稍微不那么准确,正如声明的脚注所解释的那样:
在这种情况下,(x - y)(x + y)有三个舍入误差,但x 2 - y 2 只有两个因为舍入错误已提交当计算较小的x 2 和y 2 时,不会影响最终的减法。
换句话说,取x - y,x + y和乘积(x - y)(x + y)各自引入舍入误差(舍入误差的3个步骤)。 x 2 ,y 2 ,减法x 2 - y 2 也各自引入舍入误差,但是通过对相对较小的数字(x和y中的较小者)求平方而得到的舍入误差可以忽略不计,实际上只有两个舍入误差的步骤,使得平方差更加精确。
因此选项1实际上会更精确。 dev.brutus的Java测试证实了这一点。
答案 1 :(得分:3)
Algorithm: FloatTest$1
option 1 error = 3.802792362162126
option 2 error = 4.333273185303996
Algorithm: FloatTest$2
option 1 error = 3.802792362167937
option 2 error = 4.333273185305868
Java代码:
import org.junit.Test;
public class FloatTest {
@Test
public void test() {
testImpl(new ExpectedAlgorithm() {
public double te(double cos_theta) {
return Math.sqrt(1.0f - cos_theta * cos_theta);
}
});
testImpl(new ExpectedAlgorithm() {
public double te(double cos_theta) {
return Math.sqrt((1.0f + cos_theta) * (1.0f - cos_theta));
}
});
}
public void testImpl(ExpectedAlgorithm ea) {
double delta1 = 0;
double delta2 = 0;
for (double cos_theta = -1; cos_theta <= 1; cos_theta += 1e-8) {
double[] delta = delta(cos_theta, ea);
delta1 += delta[0];
delta2 += delta[1];
}
System.out.println("Algorithm: " + ea.getClass().getName());
System.out.println("option 1 error = " + delta1);
System.out.println("option 2 error = " + delta2);
}
private double[] delta(double cos_theta, ExpectedAlgorithm ea) {
double expected = ea.te(cos_theta);
double delta1 = Math.abs(expected - t1((float) cos_theta));
double delta2 = Math.abs(expected - t2((float) cos_theta));
return new double[]{delta1, delta2};
}
private double t1(float cos_theta) {
return Math.sqrt(1.0f - cos_theta * cos_theta);
}
private double t2(float cos_theta) {
return Math.sqrt((1.0f + cos_theta) * (1.0f - cos_theta));
}
interface ExpectedAlgorithm {
double te(double cos_theta);
}
}
答案 2 :(得分:1)
顺便说一句,当theta很小时你总是会遇到问题,因为余弦在θ= 0附近是平坦的。如果theta在-0.0001和0.0001之间,那么浮点数中的cos(theta)恰好是1,所以你的sin_theta将完全为零。
要回答你的问题,当cos_theta接近1时(对应于小的θ),你的第二次计算显然更准确。这由以下程序显示,该程序列出了各种cos_theta值的两种计算的绝对和相对误差。通过使用GNU MP库比较使用200位精度计算的值,然后将其转换为浮点数来计算错误。
#include <math.h>
#include <stdio.h>
#include <gmp.h>
int main()
{
int i;
printf("cos_theta abs (1) rel (1) abs (2) rel (2)\n\n");
for (i = -14; i < 0; ++i) {
float x = 1 - pow(10, i/2.0);
float approx1 = sqrt(1 - x * x);
float approx2 = sqrt((1 - x) * (1 + x));
/* Use GNU MultiPrecision Library to get 'exact' answer */
mpf_t tmp1, tmp2;
mpf_init2(tmp1, 200); /* use 200 bits precision */
mpf_init2(tmp2, 200);
mpf_set_d(tmp1, x);
mpf_mul(tmp2, tmp1, tmp1); /* tmp2 = x * x */
mpf_neg(tmp1, tmp2); /* tmp1 = -x * x */
mpf_add_ui(tmp2, tmp1, 1); /* tmp2 = 1 - x * x */
mpf_sqrt(tmp1, tmp2); /* tmp1 = sqrt(1 - x * x) */
float exact = mpf_get_d(tmp1);
printf("%.8f %.3e %.3e %.3e %.3e\n", x,
fabs(approx1 - exact), fabs((approx1 - exact) / exact),
fabs(approx2 - exact), fabs((approx2 - exact) / exact));
/* printf("%.10f %.8f %.8f %.8f\n", x, exact, approx1, approx2); */
}
return 0;
}
输出:
cos_theta abs (1) rel (1) abs (2) rel (2)
0.99999988 2.910e-11 5.960e-08 0.000e+00 0.000e+00
0.99999970 5.821e-11 7.539e-08 0.000e+00 0.000e+00
0.99999899 3.492e-10 2.453e-07 1.164e-10 8.178e-08
0.99999684 2.095e-09 8.337e-07 0.000e+00 0.000e+00
0.99998999 1.118e-08 2.497e-06 0.000e+00 0.000e+00
0.99996835 6.240e-08 7.843e-06 9.313e-10 1.171e-07
0.99989998 3.530e-07 2.496e-05 0.000e+00 0.000e+00
0.99968380 3.818e-07 1.519e-05 0.000e+00 0.000e+00
0.99900001 1.490e-07 3.333e-06 0.000e+00 0.000e+00
0.99683774 8.941e-08 1.125e-06 7.451e-09 9.376e-08
0.99000001 5.960e-08 4.225e-07 0.000e+00 0.000e+00
0.96837723 1.490e-08 5.973e-08 0.000e+00 0.000e+00
0.89999998 2.980e-08 6.837e-08 0.000e+00 0.000e+00
0.68377221 5.960e-08 8.168e-08 5.960e-08 8.168e-08
当cos_theta不接近1时,两种方法的准确性非常接近,并且与舍入误差非常接近。
答案 3 :(得分:1)
推理某些表达式的数值精度的正确方法是:
考虑到这一点,version_1:sqrt(1 - x * x)和version_2:sqrt((1 - x)*(1 + x))会产生明显不同的结果。如下图所示,version_1演示了x接近1的灾难性能,错误&gt; 1_000_000 ulps,而另一方面版本2的错误表现良好。
这就是为什么我总是建议使用version_2,即利用方差公式。
生成square_diff_error.csv文件的Python 3.6代码:
from fractions import Fraction
from math import exp, fabs, sqrt
from random import random
from struct import pack, unpack
def ulp(x):
"""
Computing ULP of input double precision number x exploiting
lexicographic ordering property of positive IEEE-754 numbers.
The implementation correctly handles the special cases:
- ulp(NaN) = NaN
- ulp(-Inf) = Inf
- ulp(Inf) = Inf
Author: Hrvoje Abraham
Date: 11.12.2015
Revisions: 15.08.2017
26.11.2017
MIT License https://opensource.org/licenses/MIT
:param x: (float) float ULP will be calculated for
:returns: (float) the input float number ULP value
"""
# setting sign bit to 0, e.g. -0.0 becomes 0.0
t = abs(x)
# converting IEEE-754 64-bit format bit content to unsigned integer
ll = unpack('Q', pack('d', t))[0]
# computing first smaller integer, bigger in a case of ll=0 (t=0.0)
near_ll = abs(ll - 1)
# converting back to float, its value will be float nearest to t
near_t = unpack('d', pack('Q', near_ll))[0]
# abs takes care of case t=0.0
return abs(t - near_t)
with open('e:/square_diff_error.csv', 'w') as f:
for _ in range(100_000):
# nonlinear distribution of x in [0, 1] to produce more cases close to 1
k = 10
x = (exp(k) - exp(k * random())) / (exp(k) - 1)
fx = Fraction(x)
correct = sqrt(float(Fraction(1) - fx * fx))
version1 = sqrt(1.0 - x * x)
version2 = sqrt((1.0 - x) * (1.0 + x))
err1 = fabs(version1 - correct) / ulp(correct)
err2 = fabs(version2 - correct) / ulp(correct)
f.write(f'{x},{err1},{err2}\n')
产生最终情节的Mathematica代码:
data = Import["e:/square_diff_error.csv"];
err1 = {1 - #[[1]], #[[2]]} & /@ data;
err2 = {1 - #[[1]], #[[3]]} & /@ data;
ListLogLogPlot[{err1, err2}, PlotRange -> All, Axes -> False, Frame -> True,
FrameLabel -> {"1-x", "error [ULPs]"}, LabelStyle -> {FontSize -> 20}]
答案 4 :(得分:0)
[主要思考编辑]在我看来,选项2会更好,因为对于像0.000001
这样的数字,例如选项1会将正弦返回为1,而选项将返回一个更小的数字比1。
答案 5 :(得分:0)
我的选项没有区别,因为(1-x)保留了不影响携带位的精度。然后对于(1 + x)同样如此。那么影响进位精度的唯一因素就是乘法。因此,在这两种情况下都只有一个乘法运算,因此它们都可能产生相同的进位误码。