[英]Is it better to perform n additions of a floating-point number or one integer multiplication?
考慮以下兩種情況:
// Case 1
double val { initial_value };
for (int i { 0 }; i < n; ++i) {
val += step;
foo(val);
}
// Case 2
for (int i { 0 }; i < n; ++i) {
double val = initial_value + i * step;
foo(val);
}
其中n
是步數, initial_value
是類型的一些給定值double
, step
是類型的某些預定值double
和val
在后續呼叫的功能中使用的變量foo
。 哪種情況會產生較少的浮點錯誤? 我的猜測是第二個,因為只有一個加法和乘法,而第一種情況會導致所有n
加法的浮點表示錯誤。 我問這個問題是因為我不知道要搜索什么。 對於此類案例,是否存在一些好的參考資料?
實際上,變量val
將用於這兩種情況的循環中。 我沒有包含任何示例,因為我只對浮點錯誤感興趣。
選項 2 的誤差明顯較低。
多少? 好吧,為了簡單起見,讓我們首先假設initial_value
為0
。 您有 53 個有效位,您看到舍入錯誤的速度取決於我們在加法過程中設法將它們移出遠端的速度。
因此,讓我們選擇step
,使有效位理想地全為 1: 0.999999999999999999999999
。
現在舍入誤差是每次加法過程中距離step
遠端的log2(val/step)
位。 在第一次迭代期間並不多,但錯誤很快就會變得明顯。
選擇一個巨大的initial_value
並且錯誤會變得非常極端。 對於initial_value >= pow(2, 53) * step
,您的第一個循環甚至在迭代之間根本無法更改val
。
您的第二個循環仍然可以正確處理。
關鍵是在許多情況下,人們可能需要在指定的起點和終點之間均勻間隔的一系列值。 使用第二種方法將產生的值在起點和接近所需值的結束值之間盡可能均勻地間隔,但可能不完全匹配。
還有一個由Bathsheba 寫的:
兩者都有缺陷。 您應該計算開始和結束,然后將每個值計算為這些值的函數。 第二種方法的問題是你一步一步地乘以錯誤。 前者累積錯誤。
我建議幾個選擇。
從 C++20 開始,標准庫提供std::lerp ,其中std::lerp(a, b, t)
返回“參數 t 的 a 和 b 之間的線性插值(或外推,當 t 超出范圍時 [ 0,1])”。
像value = (a * (n - i) + b * i) / n;
這樣的公式value = (a * (n - i) + b * i) / n;
可能會導致更均勻的中間值1分布。
(1)在這里,我嘗試針對不同的極端情況和樣本點數量測試所有這些方法。 該程序比較每個算法在以相反方向(首先從左到右,然后從右到左)應用時生成的值。 它顯示了中間點值之間絕對差之和的平均值和方差。
其他指標可能會產生不同的結果。
考慮一個極端情況。 假設initial_value
遠大於step
。 很多很多。 由於浮點表示的限制, initial_value + step == initial_value
如此之大。 但是,我們不希望這種“極端”情況變得過於極端。 給initial_value
一個上限,比如說讓它足夠小,以便有initial_value + (2*step) != initial_value
。 (有些人可能將這個放置step
稱為介於某個 epsilon 和該 epsilon 的一半之間,但我會將術語混淆。)現在運行您的代碼。
在第一個循環中, val
每次迭代都將等於initial_value
因為沒有執行會更改其值的操作。 相反,如果有足夠的迭代,第二個循環最終將具有不同的val
值。 因此,在這種極端情況下,第二個選項,即計算initial_value + i * step
的選項更准確。
我們還應該看看相反的極端。 假設initial_value
相對於step
小到initial_value + step == step
。 在這種情況下, initial_value
也可能為零,問題簡化為詢問是否有比將i
和step
相乘更准確的方法來計算i*step
step
。 (如果有,我可能想要一個新的編譯器。)因此,在這種極端情況下,第二個選項並不比第一個更差。
極端案例分析不是結論性的,但它往往能揭示趨勢。 我把計算推到了相反的極端,第二個選項從絕對更好到絕對不差。 我願意得出結論,第二個選項產生的錯誤更少。
警告:可能是錯誤的大小可以忽略不計,不值得編碼。 此外,該問題的范圍有限,忽略了其他考慮因素(例如step
從何而來;如果是除以n
的結果,可能還有更好的選擇)。 盡管如此,在問題提出的狹窄場景中,每次迭代計算initial_value + i*step
看起來像是獲得最小數值誤差的方法。
包括<cmath>
並使用std::fma(i, step, initial_value)
將始終產生最佳結果,假設i
不是太大以至於將其轉換為浮點類型會出現舍入錯誤。 這是因為fma
被指定為產生一個結果,相當於計算i
• step
+ initial_value
的實數,然后將其四舍五入到最接近的可表示值。 它在乘法之后和加法之前沒有內部舍入,因此它產生了可在浮點類型中表示的最佳結果。
在乘法和加法之間,一般優選乘法。 加法可以產生更好的結果。 假設 IEEE-754 雙精度二進制,一個例子很容易構造為initial_value = -1./3
, i = 3
和step = 1./3
。 然后在initial_value + step + step + step
, initial_value + step
產生恰好為零(因此沒有舍入誤差),添加step
沒有錯誤,第二個 add 只是將step
加倍,這也沒有錯誤。 所以加法會產生一個沒有錯誤的最終結果。 相比之下,在initial_value + 3*step
, 3*step
有一個舍入誤差,它在加法過程中一直存在。
然而,除了故意構造的例子,乘法通常會產生比加法更好的結果,因為它使用的運算更少,在大多數情況下更少。 通常,重復添加中的舍入誤差會像隨機游走一樣,有時會增加累積誤差,有時會減少累積誤差。 隨機游走有時可以返回原點,但很少這樣做。 因此,與具有一次乘法和一次加法的表達式相比,具有許多加法的序列具有更接近原點的累積誤差(零誤差)是很少見的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.