关于 Math.atan2 的保证

2022-09-03 02:29:06

Math.atan2的文档

计算结果必须在精确结果的 2 ulps 范围内。

它说2 ulps的事实可能意味着在某些情况下,返回值不是最接近真实结果的。有谁知道是否保证为等效的参数对返回相同的值?换言之,如果 和 是正值,并且既不溢出也不溢出,则可保证doubleintabkinta * kb * k

Math.atan2(a, b) == Math.atan2(a * k, b * k) 

编辑

请注意,对于非溢出乘法,情况绝对不是这样。例如long

long a = 959786689;
long b = 363236985;
long k = 9675271;
System.out.println(Math.atan2(a, b));
System.out.println(Math.atan2(a * k, b * k));

指纹

1.2089992287797169
1.208999228779717

但我在值中找不到一个例子。int


答案 1

有谁知道它是否保证为等效的int参数对返回相同的值?

简单地说,没有。文档事实的来源,它不提供超出您引用的 2 ulp 限制的保证。这是设计使然(我们将在下面看到),因此任何其他来源要么暴露了实现细节,要么就是错误。Math

尝试启发式查找下限是不切实际的,因为 Math 的行为被记录为特定于平台的:

与类的一些数值方法不同,类的等价函数的所有实现都没有被定义为返回比特对位相同的结果。这种放宽允许在不需要严格可重复性的情况下实现性能更好。StrictMathMath

因此,即使您在测试中看到更严格的界限,也没有理由相信这些界限是跨平台、处理器或Java版本的可移植的。

但是,正如 的文档所指出的,StrictMath 具有更明确的行为。 被记录为跨平台一致地执行,并且预计具有与参考实现fdlibm相同的行为。该项目的自述文件指出:MathStrictMath

FDLIBM旨在提供一个合理的便携式...参考质量(对于 sin、cos、exp、log 等主要函数,低于 1 ulp)数学库。

您可以引用 atan2 的源代码,并通过检查其实现来确定精确的边界;的任何其他实现都需要提供与参考实现相同的结果。StrictMath.atan2()

有趣的是,它不包含与 相同的 2 ulp 注释。虽然如果它明确地重复“低于一个ulp”的评论会很好,但我将这个评论的缺失解释为意味着 的实现不需要包括这个警告 - 它将始终低于一个ulp。StrictMath.atan2()Math.atan2()fdlibmStrictMath

tl;dr 如果您需要精确的结果或稳定的结果跨平台使用。 以精度换取速度。StrictMathMath


答案 2

编辑:起初我认为这可以使用javadoc的“结果必须是半单调的”要求来回答,但它实际上无法应用,所以我重写了答案。

我所能说的几乎所有内容都已经包含在dimo414的答案中。我只想补充一点:在同一平台上使用时,甚至在使用时,都没有正式的保证(从文档中) .当然,的实现实际上使用了 ,所以当值完全相同时,结果将是相等的(这里我合理地暗示函数是确定性的),但请记住这一点。Math.atan2StrictMath.atan2atan2(y, x) == atan2(y * k, x * k)StrictMathy / xy / xdouble

回答关于参数的部分:保持32位(实际上,更像是31位加一位符号),并且可以用类型表示而不会损失任何精度,因此没有新的问题。intintdouble


您在问题中描述的差异(对于非溢出值)是由将值转换为 时精度损失引起的,它与自身无关,甚至在调用函数之前就发生了。 type 只能包含 53 位尾数,但在您的情况下需要 54 位,因此它被舍入为最接近的数字 a 可以表示(不过没关系,它只需要 52 位):longlongdoubleMath.atan2doublea * kdoubleb * k

long a = 959786689;
long b = 363236985;
long k = 9675271;

System.out.println(a * k);
System.out.println((double) (a * k));
System.out.println((long) (double) (a * k));
System.out.println((long) (double) (a * k) == a * k);

System.out.println(b * k);
System.out.println((double) (b * k));
System.out.println((long) (double) (b * k));
System.out.println((long) (double) (b * k) == b * k);

输出:

9286196318267719
9.28619631826772E15
9286196318267720
false
3514416267097935
3.514416267097935E15
3514416267097935
true

并解决注释中的示例:

我们有。在这种情况下,任何 、 、 都不能表示为不损失精度。我将用它来演示它,因为它可以显示的真实(未舍入)值:double a = 1.02551177480084, b = 1.12312341356234, k = 5;aba * kb * kdoubleBigDecimaldouble

double a = 1.02551177480084;
System.out.println("a is            " + new BigDecimal(a));
System.out.println("a * 5 is        " + new BigDecimal(a * 5));
System.out.println("a * 5 should be " + new BigDecimal(a).multiply(new BigDecimal("5")));

输出

a is            1.0255117748008399924941613789997063577175140380859375
a * 5 is        5.12755887400420018451541182002983987331390380859375   // precision loss here
a * 5 should be 5.1275588740041999624708068949985317885875701904296875

并且可以清楚地看到差异(也可以用代替)ba

有一个更简单的测试(因为本质上是使用):atan2()a/b

double a = 1.02551177480084, b = 1.12312341356234, k = 5;
System.out.println(a / b == (a * k) / (b * k));

输出

false