高效计算 (a - K) / (a + K),精度更高

Efficiently computing (a - K) / (a + K) with improved accuracy

提问人:njuffa 提问时间:2/16/2016 最后编辑:njuffa 更新时间:3/7/2016 访问量:651

问:

在各种上下文中,例如对于数学函数的参数约简,需要计算 ,其中 是正变量参数,并且是常数。在许多情况下,是 2 的幂,这是与我的工作相关的用例。我正在寻找有效的方法来更准确地计算这个商,而不是通过简单的除法来实现。可以假定硬件支持融合乘加 (FMA),因为目前所有主要的 CPU 和 GPU 体系结构都提供了此操作,并且可以通过函数和 在 C/C++ 中使用。(a - K) / (a + K)aKKfma()fmaf()

为了便于探索,我正在尝试算术。由于我计划将该方法也移植到算术中,因此不得使用高于参数和结果的本机精度的运算。到目前为止,我最好的解决方案是:floatdouble

 /* Compute q = (a - K) / (a + K) with improved accuracy. Variant 1 */
 m = a - K;
 p = a + K;
 r = 1.0f / p;
 q = m * r;
 t = fmaf (q, -2.0f*K, m);
 e = fmaf (q, -m, t);
 q = fmaf (r, e, q);

对于区间中的参数,上面的代码计算了所有输入几乎正确舍入的商(最大误差非常接近 0.5 ulps),前提是该值为 2 的幂,并且中间结果中没有溢出或下溢。对于不是 2 的幂,此代码仍然比基于除法的朴素算法更准确。在性能方面,此代码可能比平台上的朴素方法更快,因为在平台上,浮点倒数的计算速度可能比浮点除法更快a[K/2, 4.23*K]KK

当 = 2n 时,我做了以下观察:当工作间隔的上限增加到 , , ...最大误差逐渐增加,并开始从下面缓慢逼近朴素计算的最大误差。不幸的是,对于区间的下限,情况似乎并非如此。如果下限降至 ,则上述改进方法的最大误差等于朴素方法的最大误差。K8*K16*K0.25*K

与朴素方法和上述代码序列相比,有没有一种计算 q = (a - K) / (a + K) 的方法可以在更宽的区间内实现更小的最大误差(以 ulp 与数学结果为单位),特别是对于下限小于 0.5*K 的区间?效率很重要,但可以容忍比上述代码中使用的操作多一些。


在下面的一个答案中,有人指出,我可以通过将商返回为两个操作数的未计算和来提高准确性,即作为头尾对,即类似于众所周知的双精度和双精度格式。在上面的代码中,这意味着将最后一行更改为 .q:qlofloatdoubleqlo = r * e

这种方法当然很有用,我已经考虑过将其用于扩展精度对数。但它从根本上无助于扩展增强计算提供更准确商的区间。在我正在研究的特定情况下,我想使用(对于单精度)或(对于双精度)来保持初级近似区间较窄,并且 的区间大约为 [0,28]。我面临的实际问题是,对于< 0.25*K 的参数,改进除法的准确性并不比朴素方法好得多。pow()K=2K=4a

c 算法 点浮 点精度

评论

0赞 paddy 2/16/2016
您是否尝试过对算法的平均误差曲线进行建模并将其添加到结果中?
0赞 njuffa 2/16/2016
我不确定你说的“平均误差曲线”是什么意思。我感兴趣的是最小化以 ulps 为单位测量的最大误差。我通过在测试间隔内进行详尽测试来确定误差,这就是我使用单精度算术进行探索性工作的原因。
2赞 Brett Hale 2/16/2016
我想知道是否值得查看以下相对错误:?(a / (a + k)) - (k / (a + k))
0赞 njuffa 2/16/2016
@BrettHale 以这种方式重写表达式将导致最大 ulp 误差爆炸,这是由于 接近 时的减法抵消。aK
1赞 njuffa 2/17/2016
不幸的是,在某些平台上,操作成本要高得多(是操作成本的 32 倍)。由于我也想使用相同的算法,因此没有廉价的“四重”操作可以使用。因此,要求仅使用“原生”宽度操作(这也使矢量化更容易)。doublefloatdouble

答:

3赞 Simon Byrne 2/16/2016 #1

我真的没有答案(适当的浮点误差分析非常乏味),但有一些观察结果:

  • 快速倒数指令(如 RCPSS)不如除法准确,因此如果使用这些指令,可能会降低准确性。
  • m如果 a ∈ [0.5×K b, 21+n×K b),则精确计算,其中 Kb K 低于 K 的 2 的幂(如果 K 是 2 的幂,则为 K 本身),n 是 K 有效数中尾随零的数目(即,如果 K 是 2 的幂,则 n=23)。
  • 这类似于 Dekker (1971) 算法的简化形式:为了扩大范围(尤其是下限),您可能需要从中加入更多修正项(即存储为 2 秒的总和,或使用 )。div2mfloatdouble

评论

2赞 njuffa 2/17/2016
我熟悉快速互惠的权衡取舍。通常,硬件指令与适当数量的 NR 步长的组合可以得到几乎完全舍入的倒数,即最大误差非常接近 0.5 ulps,这使得这成为可能。在其他平台上,从性能上讲,使用适当的除法加上一些 FMA 相对较小的开销仍然是可以接受的。我知道 Dekker 的工作,但几乎只使用了它的加法和乘法部分。我会再看一眼,看看是否适应。div2
1赞 Simon Byrne 2/17/2016
你是对的:由于修正项,快速倒数不会产生巨大的差异。
0赞 njuffa 2/17/2016
我看了一下双除法,看起来至少需要 13 次操作。如果我只需要一个结果,我可以保存两个。但是我至少需要 6 个操作来计算和 ,因此这种方法至少需要 17 个操作,而我当前的代码需要 7 个操作。似乎是最后的后备手段,性能影响很难证明是合理的。floatfloata+Ka-K
0赞 njuffa 2/17/2016
我根据在双算术中进行所有中间计算来编写方法。不幸的是,我需要 11 个运算来计算,并且需要两个双操作数。然后,这些操作的除法需要 11 个操作,只需要一个倒数,总共 22 个操作,比问题中使用 7 个操作的代码多 15 个。为了快速测试,我选择了间隔 [K/128, 128*K],效果很好,最大误差非常接近 0.5 ulp。floata+Ka-Kfloat
1赞 user3528438 2/16/2016 #2

如果可以放宽 API 以返回另一个对错误进行建模的变量,则解决方案将变得更加简单:

float foo(float a, float k, float *res)
{
    float ret=(a-k)/(a+k);
    *res = fmaf(-ret,a+k,a-k)/(a+k);
    return ret;
}

该解决方案仅处理除法的截断误差,但不处理 和 的精度损失。a+ka-k

为了处理这些错误,我认为我需要使用双精度,或者 bithack 来使用定点。

测试代码已更新为人为生成非零最低有效位 在输入中

测试代码

https://ideone.com/bHxAg8

评论

0赞 njuffa 2/17/2016
我假设“用于对误差进行建模的其他变量”是指基本上将商返回为头尾对(双浮点数、双双双数)?我可以很容易地做到这一点(在上面的代码中,这意味着用 替换最后一行),但我不明白它如何解决随着区间下限降至以下而迅速增加的误差问题。在任何平台上,分区通常都很昂贵,我想避免不得不做其中的两个;倒数后跟两个反向乘法提供了更好的性能,所以我使用了它。我将查看您的代码以探索详细信息。qlo = r * e0.5*K
0赞 njuffa 2/17/2016
我的测试框架通过对区间 [0.5*K, 4*K] 的详尽测试表明,上述代码计算了商(被视为未计算的总和:)最大误差略低于 1 ULP,这比朴素计算(大约 1.62 ULP)要好,但不如我问题的代码(接近 0.5 ulp)。我使用 = 2 进行测试,但只要没有发生下溢/溢出,任何 2 的幂都应该同样有效。如果您的测试结果与我的测试结果存在重大差异,请告诉我。retresK
0赞 user3528438 2/17/2016
@njuffa 不,我同意你的测试结果。这就是为什么我之前删除了这个答案,因为我认为它不能很好地解决问题。
4赞 gnasher729 2/17/2016 #3

如果 a 与 K 相比很大,则 (a-K)/(a+K) = 1 - 2K / (a + K) 将给出一个很好的近似值。如果 a 与 K 相比较小,则 2a / (a + K) - 1 将给出一个很好的近似值。如果 K/2 ≤ ≤ 2K,那么 a-K 是一个精确的操作,因此进行除法将得到一个不错的结果。

评论

0赞 njuffa 2/17/2016
如果您可以建议在三个建议的代码路径之间切换点,我很乐意通过我的测试框架运行它。虽然多分支代码不一定对矢量化友好,因此可能效率低下,但在这种情况下,这个问题可以通过谓词来解决。
0赞 njuffa 2/17/2016
对不起,我忽略了切换点已经足够指定了。我将算法转换为 C 代码,如下所示,发现 [0.5*K,4*K) 上的最大 ulp 误差略低于 2.5 ulps,这比使用朴素方法要大:m = a - K; p = a + K; if ((0.5f*K <= a) && (a <= 2.0f*K)) { q = m / p; } else if (a < 0.5f*K) { q = 1.0f - 2.0f*K / p; } else { q = (2.0f * a) / p - 1.0f; }
4赞 aka.nice 2/17/2016 #4

一种可能性是使用经典的 Dekker/Schewchuk 将 m 和 p 的误差跟踪到 m1 和 p1:

m=a-k;
k0=a-m;
a0=k0+m;
k1=k0-k;
a1=a-a0;
m1=a1+k1;

p=a+k;
k0=p-a;
a0=p-k0;
k1=k-k0;
a1=a-a0;
p1=a1+k1;

然后,纠正幼稚的划分:

q=m/p;
r0=fmaf(p,-q,m);
r1=fmaf(p1,-q,m1);
r=r0+r1;
q1=r/p;
q=q+q1;

这将花费你 2 个师,但如果我没有搞砸,应该接近一半。

但是这些除法可以用p的倒数乘法代替,没有任何问题,因为第一个错误舍入的除法将由余数r补偿,而第二个错误舍入的除法并不重要(修正q1的最后一位不会改变任何东西)。

评论

1赞 njuffa 2/17/2016
这似乎基本上是西蒙·伯恩(Simon Byrne)建议的方法,使用了包括两个部门在内的18个操作。但是,这是完全编码的。我的实验表明,在 [0.5*K,32*K] 上,最大误差非常接近 0.5 ulp,因此当区间的上限增加时,这似乎做得很好。然而,将下限降低到 0.25*K 会使最大 ulp 误差增加到略小于 2 ulps,比朴素方法的最大误差 ~ 1.625 ulp 。这是可以解决的吗?div2
0赞 aka.nice 2/17/2016
啊,看来我搞砸了错误 m1 的标志......让我再检查一次。现在我编辑了我的答案应该更好。
0赞 njuffa 2/17/2016
在 FMA 的帮助下,可以对双除法进行编码,这样只需要一个倒数运算,而不是两个完整的除法。我怀疑这里可以进行类似的优化。float
1赞 Brendan 2/17/2016 #5

问题在于 中的添加。任何精度的损失都会被除法放大。问题不在于分裂本身。(a + K)(a + K)

如果 和 的指数相同(几乎),则不会丢失精度,并且如果指数之间的绝对差大于有效大小,则(如果具有更大的量级)或(如果具有更大的量级)。aK(a + K) == aa(a + K) == KK

没有办法防止这种情况发生。增加有效大小(例如,在 80x86 上使用 80 位“扩展双精度”)只会有助于稍微扩大“准确结果范围”。要了解原因,请考虑(32 位浮点数的最小正非正态数在哪里)。在这种情况下(对于 32 位浮点数),您需要大约 260 位的有效大小才能获得结果,以完全避免精度损失。做(例如)也不会有太大帮助,因为你仍然有完全相同的问题(但它可以避免类似的问题)。你也不能这样做,因为除法不是那样工作的。smallest + largestsmallesttemp = 1/(a + K); result = a * temp - K / temp;(a + K)(a - K)result = anything / p + anything_error/p_error

我能想到的只有 3 种替代方案可以接近 0.5 ulps,以获得适合 32 位浮点的所有可能的正值。没有一个是可以接受的。a

第一种选择是为每个值预先计算一个查找表(使用“大实数”数学),这(通过一些技巧)最终对于 32 位浮点约为 2 GiB(对于 64 位浮点则完全疯狂)。当然,如果 的可能值范围小于“任何可以容纳在 32 位浮点数中的正值”,则查找表的大小将减小。aa

第二种选择是在运行时使用其他东西(“大实数”)进行计算(并转换为/从 32 位浮点数转换)。

第三种选择涉及“某物”(我不知道它叫什么,但它很贵)。将舍入模式设置为“四舍五入到正无穷大”并计算,然后切换到“四舍五入到负无穷大”并计算。接下来,以尽可能小的量执行和减少,然后重复“lower_bound”计算,并继续这样做,直到得到不同的值,然后恢复到以前的值。之后,您执行基本相同的操作(但相反的舍入模式,并且递增而不是递减)来确定 和(从 的原始值 开始)。最后,插值,如.请注意,您需要计算初始上限和下限,如果它们相等,则跳过所有这些。还要注意的是,这都是“理论上,完全未经测试”,我可能在某个地方厌倦了它。temp1 = (a + K); if(a < K) temp2 = (a - K);if(a >= K) temp2 = (a - K); lower_bound = temp2 / temp1;a_lower = aa_lowerlower_bounda_lowerupper_bounda_upperaa_range = a_upper - a_lower; result = upper_bound * (a_upper - a) / a_range + lower_bound * (a - a_lower) / a_range;

我主要想说的是(在我看来)你应该放弃并接受你无能为力来接近 0.5 ulp。不好意思。。:)

2赞 njuffa 2/18/2016 #6

由于我的目标只是扩大获得准确结果的间隔,而不是找到一个适用于所有可能值的解决方案,因此将双算术用于所有中间计算似乎成本太高。afloat

仔细思考这个问题,很明显,在我问题的代码中,除法余数的计算是获得更准确结果的关键部分。在数学上,余数是 (a-K) - q * (a+K)。在我的代码中,我只是简单地将 (a-K) 表示为 (a-K) 并表示为 ,因为这提供了比直接表示更好的数值结果。emm + 2*K

以相对较小的额外计算成本,(a+K)可以表示为双精度,即头尾对,这导致了我的原始代码的以下修改版本:floatp:plo

/* Compute q = (a - K) / (a + K) with improved accuracy. Variant 2 */
m = a - K;
p = a + K;
r = 1.0f / p;
q = m * r;
mx = fmaxf (a, K);
mn = fminf (a, K);
plo = (mx - p) + mn;
t = fmaf (q, -p, m);
e = fmaf (q, -plo, t);
q = fmaf (r, e, q);

测试表明,这提供了 [K/2, 224*K] 中几乎正确的四舍五入结果,从而可以大幅增加到获得准确结果的区间上限。a

在低端加宽间隔需要更准确地表示 (a-K)。我们可以将其计算为双头尾对,这会导致以下代码变体:floatm:mlo

/* Compute q = (a - K) / (a + K) with improved accuracy. Variant 3 */
m = a - K;
p = a + K;
r = 1.0f / p;
q = m * r;
plo = (a < K) ? ((K - p) + a) : ((a - p) + K);
mlo = (a < K) ? (a - (K + m)) : ((a - m) - K);
t = fmaf (q, -p, m);
e = fmaf (q, -plo, t);
e = e + mlo;
q = fmaf (r, e, q);

详尽的测试如何为区间 [K/224, K*224] 提供几乎正确的四舍五入结果。不幸的是,与我问题中的代码相比,这需要花费十个额外的操作,这是一个高昂的代价,要从大约 1.625 ulps 获得最大误差,而朴素的计算下降到接近 0.5 ulp。a

就像我在问题中的原始代码一样,可以用 (a-K) 表示 (a+K),从而消除了 的尾部计算。此方法生成以下代码:pplo

/* Compute q = (a - K) / (a + K) with improved accuracy. Variant 4 */
m = a - K;
p = a + K;
r = 1.0f / p;
q = m * r;
mlo = (a < K) ? (a - (K + m)) : ((a - m) - K);
t = fmaf (q, -2.0f*K, m);
t = fmaf (q, -m, t);
e = fmaf (q - 1.0f, -mlo, t);
q = fmaf (r, e, q);

如果主要关注点是减少间隔的下限,这是有利的,这是我在问题中解释的特别关注点。对单精度情况的详尽测试表明,当 K=2n 时,对于区间 [K/2,24, 4.23*K] 的值,会产生几乎正确的四舍五入结果。总共有 14 或 15 个操作(取决于架构是支持完全预测还是仅支持条件移动),这需要比我的原始代码多 7 到 8 个操作。a

最后,可以直接基于原始变量进行残差计算,以避免计算 和 时固有的误差。这导致了以下代码,当 K = 2n 时,计算区间 [K/224, K/3] 内几乎正确的舍入结果:ampa

/* Compute q = (a - K) / (a + K) with improved accuracy. Variant 5 */
m = a - K;
p = a + K;
r = 1.0f / p;       
q = m * r;
t = fmaf (q + 1.0f, -K, a);
e = fmaf (q, -a, t);
q = fmaf (r, e, q);