在各种情况下,例如对于数学函数的参数减少,需要计算(a - K) / (a + K)
,其中a
是正变量参数,K
是常量。在许多情况下,K
是2的幂,这是与我的工作相关的用例。我正在寻找比直接划分更准确地计算这个商的有效方法。可以假设对融合乘法 - 加法(FMA)的硬件支持,因为此操作由所有主要CPU和GPU架构提供,并且可通过函数fma()
和{{1}在C / C ++中使用。 }。
为了便于探索,我正在尝试使用fmaf()
算术。由于我计划将方法移植到float
算术,因此不能使用高于参数和结果的本机精度的操作。到目前为止,我最好的解决方案是:
double
对于区间 /* 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);
中的参数a
,上面的代码计算所有输入几乎正确舍入的商(最大误差非常接近0.5 ulps),前提是[K/2, 4.23*K]
是功率为2,中间结果没有溢出或下溢。对于K
而不是2的幂,此代码仍然比基于除法的朴素算法更准确。在性能方面,这个代码可以更快比平台上的朴素方法更快,因为浮点数的倒数可以比浮点除法更快。
当K
= 2 n 时,我做了以下观察:当工作区间的上限增加到K
,8*K
时,......最大误差逐渐增加,并从下面开始慢慢逼近天真计算的最大误差。不幸的是,对于区间的下限,情况似乎并非如此。如果下限降至16*K
,则上述改进方法的最大误差等于朴素方法的最大误差。
是否有一种计算q =(a-K)/(a + K)的方法,与天真的方法相比,可以实现更小的最大误差(以 ulp 对数学结果测量)和上面的代码序列,在更宽的时间间隔内,特别是对于下限小于0.25*K
的区间?效率很重要,但是比上面代码中使用的操作要多一些可能会被容忍。
在下面的一个答案中,有人指出,我可以通过将商作为两个操作数的未评估总和,即作为头尾对0.5*K
,即类似于井 - 返回来提高准确性。已知双 - q:qlo
和双 - float
格式。在上面的代码中,这意味着将最后一行更改为double
。
这种方法当然很有用,我已经考虑过将其用于qlo = r * e
中的扩展精度对数。但它并没有从根本上帮助增加计算提供更准确的商的区间的期望扩大。在我看的特定情况下,我想使用pow()
(对于单精度)或K=2
(对于双精度)来保持主要近似间隔变窄,以及{{1大概是[0,28]。我面临的实际问题是争论< 0.25 * K改进除法的准确性并不比使用朴素方法好。
答案 0 :(得分:4)
如果a与K相比较大,那么(a-K)/(a + K)= 1-2K /(a + K)将给出良好的近似。如果a与K相比较小,则2a /(a + K)-1将给出良好的近似值。如果K /2≤a≤2K,则a-K是精确的运算,因此进行除法将得到一个合适的结果。
答案 1 :(得分: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赢了'改变任何事情。)
答案 2 :(得分:3)
我真的没有答案(适当的浮点错误分析非常繁琐),但有一些观察结果:
m
,其中K b 是低于K的2的幂(或者K本身,如果K是2的幂),并且n是有效数K中的尾随零的数量(即,如果K是幂2,然后n = 23)。div2
算法的简化形式:为了扩大范围(特别是下限),你可能需要从中加入更多的修正项(即将m
存储为2 float
的总和,或使用double
)。答案 3 :(得分:2)
由于我的目标只是扩大实现准确结果的时间间隔,而不是找到适用于a
所有可能值的解决方案,因此请使用双float
算术所有中间计算似乎都太昂贵了。
更多地思考这个问题,很明显,在我的问题的代码中,除法的剩余部分e
的计算是实现更准确结果的关键部分。在数学上,余数是(a-K)-q *(a + K)。在我的代码中,我只使用m
来表示(a-K)并将(a + k)表示为m + 2*K
,因为这可以在直接表示中提供数值优越的结果。
由于额外的计算成本相对较小,(a + K)可以表示为双 - float
,即头尾对p: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,2 24 * K]中的a
提供了几乎正确的舍入结果,允许大幅增加到区间的上限。获得了准确的结果。
加宽下端的间隔需要更准确地表示(a-K)。我们可以将其计算为双float
头尾对m: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 / 2 24 ,K * 2 24 的区间内,a
几乎可以得到几乎正确的舍入结果。不幸的是,与我的问题中的代码相比,这需要花费10个额外的操作,这是一个陡峭的代价,以便从最小的1.625 ulps获得最大误差,并将原始计算降低到接近0.5 ulp。
正如我在问题中的原始代码一样,可以用(a-K)表达(a + K),从而消除p
,plo
尾部的计算。这种方法产生以下代码:
/* 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 = 2 n 时,[{1}}的值在{K / 2 24 ,4.23 * K]。总共有14或15次操作(取决于架构是支持完全预测还是只是条件移动),这需要比原始代码多7到8个操作。
最后,可以将残差计算直接基于原始变量a
,以避免计算a
和m
时固有的误差。这导致以下代码,对于K = 2 n ,在区间[K / 2 24 ,K /中计算p
几乎正确的舍入结果3):
a
答案 4 :(得分:1)
如果您可以放松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+k
和a-k
的精度损失。
要处理这些错误,我想我需要使用双精度或bithack来使用定点。
更新测试代码以人为生成非零最低有效位 在输入
测试代码
答案 5 :(得分:1)
问题是(a + K)
中的添加。 (a + K)
中任何精度的损失都会被分裂放大。问题不在于分裂本身。
如果a
和K
的指数相同(几乎),则不会丢失任何精度,并且如果指数之间的绝对差值大于有效数字大小,那么(a + K) == a
(如果a
幅度较大)或(a + K) == K
(如果K
幅度较大)。
没有办法阻止这种情况。增加有效位数大小(例如,在80x86上使用80位“扩展双精度”)仅有助于略微扩大“精确结果范围”。要理解原因,请考虑smallest + largest
(其中smallest
是最小的正非正规,32位浮点数可以是)。在这种情况下(对于32位浮点数),您需要大约260位的有效位大小才能完全避免精度损失。执行(例如)temp = 1/(a + K); result = a * temp - K / temp;
也无济于事,因为您仍然遇到完全相同的(a + K)
问题(但它会避免(a - K)
中的类似问题)。你也不能result = anything / p + anything_error/p_error
,因为除法不能那样。
对于所有可能适合32位浮点的a
正值,我只能想到3个替代0.5 ulps的替代方法。没有可能是可以接受的。
第一个替代方案涉及为a
的每个值预先计算查找表(使用“大实数”数学),对于32位浮点,(对于一些技巧)最终约为2 GiB (对于64位浮点而言完全疯狂)。当然,如果a
的可能值范围小于“可以适合32位浮点数的任何正值”,查找表的大小将会减小。
第二种方法是在运行时使用其他东西(“大实数”)进行计算(并转换为/从32位浮点转换)。
第三种选择涉及“某事”(我不知道它叫什么,但它很昂贵)。将舍入模式设置为“舍入到正无穷大”并计算temp1 = (a + K); if(a < K) temp2 = (a - K);
然后切换到“舍入到负无穷大”并计算if(a >= K) temp2 = (a - K); lower_bound = temp2 / temp1;
。接下来执行a_lower = a
并尽可能减少a_lower
并重复“lower_bound”计算,并继续这样做,直到获得lower_bound
的不同值,然后恢复为之前的值值a_lower
。之后,您基本上执行相同的(但相反的舍入模式,并递增而不递减)来确定upper_bound
和a_upper
(从a
的原始值开始)。最后,插入,如a_range = a_upper - a_lower; result = upper_bound * (a_upper - a) / a_range + lower_bound * (a - a_lower) / a_range;
。请注意,如果它们相等,您将需要计算初始上限和下限并跳过所有这些。另外要注意的是,这一切都“理论上完全没有经过考验”,我可能会把它搞砸到某个地方。
主要是我所说的(在我看来)你应该放弃并接受你无法做到接近0.5 ulp。对不起.. :))