簡體   English   中英

浮點除法與浮點乘法

[英]Floating point division vs floating point multiplication

通過編碼是否有任何(非微優化)性能增益

float f1 = 200f / 2

在比較中

float f2 = 200f * 0.5

幾年前我的一位教授告訴我,浮點除法比浮點乘法慢,但沒有詳細說明原因。

這句話適用於現代PC架構嗎?

UPDATE1

關於評論,請同時考慮這個案例:

float f1;
float f2 = 2
float f3 = 3;
for( i =0 ; i < 1e8; i++)
{
  f1 = (i * f2 + i / f3) * 0.5; //or divide by 2.0f, respectively
}

更新2從評論中引用:

[我想]知道什么是算法/架構要求導致>除法在硬件上比復制要復雜得多

是的,許多CPU可以在1或2個時鍾周期內執行乘法,但除法總是需要更長時間(盡管FP除法有時比整數除法更快)。

如果你看一下這個答案,你會看到這個分區可以超過24個周期。

為什么除法比乘法要長得多? 如果你還記得回到小學,你可能會記得,乘法基本上可以通過多次同時添加來執行。 除法需要不能同時執行的迭代減法,因此需要更長的時間。 實際上,一些FP單元通過執行倒數近似並乘以它來加速除法。 它不是那么准確但有點快。

除了乘法之外,除法本質上是一個慢得多的運算。

事實上,由於浮點不准確,編譯器在許多情況下無法 (並且您可能不希望)進行優化。 這兩個陳述:

double d1 = 7 / 10.;
double d2 = 7 * 0.1;

在語義上相同 - 0.1不能精確地表示為double ,因此最終會使用稍微不同的值 - 在這種情況下用乘法代替除法會產生不同的結果!

分裂要非常小心,盡可能避免使用。 例如,hoist float inverse = 1.0f / divisor; 離開循環並在循環內乘以inverse (如果inverse的舍入誤差是可以接受的)

通常1.0/x不能完全表示為floatdouble x是2的冪時,它將是精確的。這使得編譯器可以將x / 2.0f優化為x * 0.5f而不會對結果進行任何更改。

為了讓編譯器為你做優化,即使結果不准確(或使用運行時變量除數),你需要像gcc -O3 -ffast-math這樣的選項。 具體而言, -freciprocal-math (由啟用-funsafe-math-optimizations通過啟用-ffast-math )讓編譯器代替x / yx * (1/y)時這是有用的。 其他編譯器有類似的選項,ICC可能會默認啟用一些“不安全”的優化(我認為它確實如此,但我忘了)。

-ffast-math通常很重要,允許FP循環的自動矢量化,特別是減少(例如將數組總和為一個標量),因為FP數學不是關聯的。 為什么GCC不優化a * a * a * a * a * a到(a * a * a)*(a * a * a)?

還要注意C ++編譯器在某些情況下可以將+*折疊成FMA(當編譯支持它的目標時,比如-march=haswell ),但是他們不能用/來做。


在現代x86 CPU上,除法的延遲比乘法或加法(或FMA )差2到4倍,吞吐量更差6到40 1 (對於僅進行除法而不是乘法的緊密循環)。

除了@ NathanWhitehead的回答中解釋的原因,除法/ sqrt單元沒有完全流水線化。 最差的比率是256b向量,因為(與其他執行單元不同)除法單元通常不是全寬度,因此寬向量必須分成兩半。 未完全流水線化的執行單元非常不尋常,以至於英特爾CPU具有arith.divider_active硬件性能計數器,可幫助您找到導致分頻器吞吐量出現瓶頸的代碼,而不是通常的前端或執行端口瓶頸。 (或者更常見的是,內存瓶頸或長延遲鏈限制了指令級並行性,導致指令吞吐量小於每個時鍾約4個)。

但是,Intel和AMD CPU(KNL除外)上的FP划分和sqrt是作為單個uop實現的,因此它不一定會對周圍的代碼產生大的吞吐量影響 划分的最佳情況是,無序執行可以隱藏延遲,並且當存在大量的乘法和增加(或其他工作)時,可以與除法並行發生。

(整數除法在英特爾上被編碼為多個uop,因此它總是對整數乘法的周圍代碼產生更大的影響。對高性能整數除法的需求較少,因此硬件支持並不那么花哨。相關: idiv這樣的微編碼指令會導致對齊敏感的前端瓶頸 。)

所以,例如,這將是非常糟糕的:

for ()
    a[i] = b[i] / scale;  // division throughput bottleneck

// Instead, use this:
float inv = 1.0 / scale;
for ()
    a[i] = b[i] * inv;  // multiply (or store) throughput bottleneck

你在循環中所做的只是加載/分割/存儲,它們是獨立的,因此它的吞吐量很重要,而不是延遲。

accumulator /= b[i]類的減少會阻礙除法或乘法延遲,而不是吞吐量。 但是,如果您在末尾划分或乘以多個累加器,則可以隱藏延遲並仍然使吞吐量飽和。 請注意, add等待時間或div吞吐量的sum += a[i] / b[i]瓶頸,但不是div延遲,因為除法不在關鍵路徑上(循環攜帶的依賴鏈)。


但是在這樣的事情中( 近似像log(x)這樣的函數,兩個多項式的比率 ),除法可以相當便宜

for () {
    // (not shown: extracting the exponent / mantissa)
    float p = polynomial(b[i], 1.23, -4.56, ...);  // FMA chain for a polynomial
    float q = polynomial(b[i], 3.21, -6.54, ...);
    a[i] = p/q;
}

對於尾數范圍內的log() ,N階的兩個多項式的比率比具有2N個系數的單個多項式具有更小的誤差,並且並行地評估2在單個循環體內給出一些指令級並行性而不是一個大規模的dep鏈,使得亂序執行變得更容易。

在這種情況下,我們不會對分頻延遲產生瓶頸,因為亂序執行可以在循環中保持循環的多次迭代。

只要我們的多項式足夠大,每10條FMA指令只有一個除法,我們就不會對除法吞吐量產生瓶頸。 (在一個真實的log()用例中,有一堆工作提取指數/尾數並將事物重新組合在一起,所以在除法之間還有更多的工作要做。)


當你需要划分時,通常最好只划分而不是rcpps

x86有一個近似倒數指令( rcpps ),它只能提供12位精度。 (AVX512F有14位,AVX512ER有28位。)

您可以使用它來執行x / y = x * approx_recip(y)而不使用實際除法指令。 rcpps itsef相當快;通常比乘法慢一點。它使用來自CPU內部表的表查找。分頻器硬件可以使用相同的表作為起始點。)

在大多數情況下, x * rcpps(y)太不准確,需要使用Newton-Raphson迭代來加倍精度。 但是這需要花費2倍和2個FMA ,並且具有與實際除法指令一樣高的延遲。 如果正在做的是分裂,那么它可以是一個吞吐量勝利。 (但是如果可以的話,你應該首先避免使用這種循環,可能是通過將除法作為另一個循環的一部分來完成其他工作。)

但是如果你使用除法作為一個更復雜的函數的一部分, rcpps本身+額外的mul + FMA通常會更快地除以divps指令,除非在具有非常低的divps吞吐量的CPU上。

(例如Knight's Landing,見下文.KLL支持AVX512ER ,因此對於float向量, VRCP28PS結果已經足夠准確,只需乘以Newton-Raphson迭代。 float尾數只有24位。)


Agner Fog表中的具體數字:

與其他每個ALU操作不同,除法延遲/吞吐量取決於某些CPU的數據。 同樣,這是因為它太慢而且沒有完全流水線化。 固定延遲會使亂序調度更容易,因為它避免了回寫沖突(當同一執行端口嘗試在同一周期產生2個結果時,例如運行3周期指令然后執行兩個1周期操作) 。

通常,最快的情況是除數是一個“圓”數,如2.00.5 (即base2 float表示在尾數中有許多尾隨零)。

float 延遲 (周期) /吞吐量 (每條指令的周期,僅使用獨立輸入背靠背運行):

                   scalar & 128b vector        256b AVX vector
                   divss      |  mulss
                   divps xmm  |  mulps           vdivps ymm | vmulps ymm

Nehalem          7-14 /  7-14 | 5 / 1           (No AVX)
Sandybridge     10-14 / 10-14 | 5 / 1        21-29 / 20-28 (3 uops) | 5 / 1
Haswell         10-13 / 7     | 5 / 0.5       18-21 /   14 (3 uops) | 5 / 0.5
Skylake            11 / 3     | 4 / 0.5          11 /    5 (1 uop)  | 4 / 0.5

Piledriver       9-24 / 5-10  | 5-6 / 0.5      9-24 / 9-20 (2 uops) | 5-6 / 1 (2 uops)
Ryzen              10 / 3     | 3 / 0.5         10  /    6 (2 uops) | 3 / 1 (2 uops)

 Low-power CPUs:
Jaguar(scalar)     14 / 14    | 2 / 1
Jaguar             19 / 19    | 2 / 1            38 /   38 (2 uops) | 2 / 2 (2 uops)

Silvermont(scalar)    19 / 17    | 4 / 1
Silvermont      39 / 39 (6 uops) | 5 / 2            (No AVX)

KNL(scalar)     27 / 17 (3 uops) | 6 / 0.5
KNL             32 / 20 (18uops) | 6 / 0.5        32 / 32 (18 uops) | 6 / 0.5  (AVX and AVX512)

double 延遲 (周期) /吞吐量 (每條指令的周期):

                   scalar & 128b vector        256b AVX vector
                   divsd      |  mulsd
                   divpd xmm  |  mulpd           vdivpd ymm | vmulpd ymm

Nehalem         7-22 /  7-22 | 5 / 1        (No AVX)
Sandybridge    10-22 / 10-22 | 5 / 1        21-45 / 20-44 (3 uops) | 5 / 1
Haswell        10-20 /  8-14 | 5 / 0.5      19-35 / 16-28 (3 uops) | 5 / 0.5
Skylake        13-14 /     4 | 4 / 0.5      13-14 /     8 (1 uop)  | 4 / 0.5

Piledriver      9-27 /  5-10 | 5-6 / 1       9-27 / 9-18 (2 uops)  | 5-6 / 1 (2 uops)
Ryzen           8-13 /  4-5  | 4 / 0.5       8-13 /  8-9 (2 uops)  | 4 / 1 (2 uops)

  Low power CPUs:
Jaguar            19 /   19  | 4 / 2            38 /  38 (2 uops)  | 4 / 2 (2 uops)

Silvermont(scalar) 34 / 32    | 5 / 2
Silvermont         69 / 69 (6 uops) | 5 / 2           (No AVX)

KNL(scalar)      42 / 42 (3 uops) | 6 / 0.5   (Yes, Agner really lists scalar as slower than packed, but fewer uops)
KNL              32 / 20 (18uops) | 6 / 0.5        32 / 32 (18 uops) | 6 / 0.5  (AVX and AVX512)

Ivybridge和Broadwell也不同,但我想保持桌子小。 (Core2(在Nehalem之前)具有更好的分頻器性能,但其最大時鍾速度較低。)

Atom,Silvermont, 甚至Knight's Landing(基於Silvermont的Xeon Phi)都具有極低的除法性能 ,甚至128b向量也比標量慢。 AMD的低功耗Jaguar CPU(在某些游戲機中使用)類似。 高性能分頻器需要大量的芯片面積。 Xeon Phi具有低功耗的每核 ,並且在芯片上封裝大量內核使其具有比Skylake-AVX512更嚴格的芯片面積限制。 似乎AVX512ER rcp28ps / pd是你在KNL上“應該”使用的東西。

(參見Skylake-AVX512又名Skylake-X的InstLatx64結果 vdivps zmm數字:18c / 10c,因此ymm的吞吐量的ymm 。)


長延遲鏈在循環傳輸時會成為一個問題,或者當它們長時間以至於它們阻止無序執行與其他獨立工作找到並行性時就會成為問題。


腳注1:我如何制作這些div與mul的表現比率:

FP鴻溝與多種性能比甚至比Silvermont和Jaguar等低功耗CPU甚至Xeon Phi(KNL,你應該使用AVX512ER)更差。

標量(非矢量化) double實際分數/乘數吞吐量比率 :Ryzen和Skylake的加權分數為8,而Haswell為16-28(數據依賴,除非你的除數是除數,否則更有可能達到28周期結束)圓形數字)。 這些現代CPU具有非常強大的分頻器,但它們每時每秒2倍的吞吐量將其擊敗。 (當您的代碼可以使用256b AVX向量自動向量化時更是如此)。 另請注意,使用正確的編譯器選項,這些乘法吞吐量也適用於FMA。

來自英特爾Haswell / Skylake和AMD Ryzen的http://agner.org/optimize/指令表中的數字,用於SSE標量(不包括x87 fmul / fdiv )和floatdouble float 256b AVX SIMD向量。 另請參閱標記wiki。

是。 我所知道的每個FPU都比分割快得多。

然而,現代PC 非常快。 它們還包含流水線架構,可以在許多情況下使差異變得可以忽略不計。 最重要的是,任何體面的編譯器都會執行您在編譯時顯示的除法操作,並啟用優化。 對於您更新的示例,任何體面的編譯器都會執行該轉換。

因此,通常您應該擔心使代碼可讀 ,並讓編譯器擔心使其快速。 只有當您測量到該行的速度問題時,您才會擔心為了速度而變換代碼。 編譯器非常清楚什么比它們的CPU更快,並且通常比你希望的好得多。

想想兩個n位數的乘法需要什么。 使用最簡單的方法,您可以取一個數字x並重復移位並有條件地將其添加到累加器(基於另一個數字y中的位)。 添加n后,你就完成了。 你的結果適合2n位。

對於除法,從x的2n位和n位的y開始,你想要計算x / y。 最簡單的方法是長除法,但是二進制。 在每個階段,你進行比較和減法以獲得一個商的一個位。 這需要你的步驟。

一些差異:乘法的每一步只需要看1位; 除法的每個階段都需要在比較期間查看n位。 乘法的每個階段都獨立於所有其他階段(與您添加部分產品的順序無關); 對於除法,每一步取決於前一步驟。 這在硬件方面是一個大問題。 如果事情可以獨立完成,那么它們可以在一個時鍾周期內同時發生。

Newton rhapson通過線性代數近似求解O(M(n))復雜度中的整數除法。 比其他O(n * n)復雜度更快。

在代碼中該方法包含10個結果9adds 2bitwiseshifts。

這就解釋了為什么一個除法與乘法相差12倍的cpu。

答案取決於您編程的平台。

例如,在x86上對數組進行大量乘法應該比分割快得多,因為編譯器應該創建使用SIMD指令的匯編代碼。 由於SIMD指令中沒有除法,因此使用乘法除法可以看到很大的改進。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM