Phép chia dấu phẩy động và phép nhân dấu phẩy động


78

Có tăng hiệu suất (không vi lượng hóa) bằng cách mã hóa không

float f1 = 200f / 2

so sánh với

float f2 = 200f * 0.5

Một giáo sư của tôi đã nói với tôi vài năm trước rằng phép chia dấu phẩy động chậm hơn phép nhân dấu phẩy động mà không giải thích lý do tại sao.

Tuyên bố này có phù hợp với kiến ​​trúc PC hiện đại không?

Cập nhật1

Đối với một nhận xét, vui lòng xem xét trường hợp này:

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
}

Cập nhật 2 Trích dẫn từ các bình luận:

[Tôi muốn] biết các yêu cầu về thuật toán / kiến ​​trúc gây ra> phép chia trong phần cứng phức tạp hơn nhiều so với phép nhân


3
Cách thực sự để tìm ra câu trả lời là thử cả hai và đo thời gian.
sharptooth

15
Hầu hết các trình biên dịch sẽ tối ưu hóa một biểu thức hằng theo nghĩa đen như thế này, vì vậy nó không có gì khác biệt.
Paul R

2
@sharptooth: Vâng, cố gắng ra bản thân mình sẽ giải quyết vấn đề cho máy dev của tôi, nhưng tôi nghĩ nếu ai đó của SO-đám đông đã có câu trả lời cho trường hợp tổng quát, ông muốn share;)
sum1stolemyname

7
@Gabe, tôi nghĩ ý của Paul là nó sẽ biến 200f / 2thành 100f.
mikerobi

10
@Paul: Có thể tối ưu hóa như vậy cho lũy thừa của 2, nhưng nói chung thì không. Ngoài lũy thừa của hai, không có số dấu phẩy động nào có nghịch đảo mà bạn có thể nhân thay cho phép chia.
R .. GitHub DỪNG TRỢ GIÚP ICE

Câu trả lời:


86

Đúng vậy, nhiều CPU có thể thực hiện phép nhân trong 1 hoặc 2 chu kỳ xung nhịp nhưng phép chia luôn mất nhiều thời gian hơn (mặc dù phép chia FP đôi khi nhanh hơn phép chia số nguyên).

Nếu bạn nhìn vào câu trả lời này, bạn sẽ thấy rằng sự phân chia có thể vượt quá 24 chu kỳ.

Tại sao phép chia mất nhiều thời gian hơn phép nhân? Nếu bạn nhớ lại trường lớp, bạn có thể nhớ lại rằng phép nhân về cơ bản có thể được thực hiện với nhiều phép cộng đồng thời. Phép chia yêu cầu phép trừ lặp đi lặp lại không thể thực hiện đồng thời nên mất nhiều thời gian hơn. Trên thực tế, một số đơn vị FP tăng tốc độ chia bằng cách thực hiện phép xấp xỉ nghịch đảo và nhân với số đó. Nó không hoàn toàn chính xác nhưng có phần nhanh hơn.


1
Tôi nghĩ OP muốn biết những yêu cầu về thuật toán / kiến ​​trúc khiến phép chia phức tạp hơn rất nhiều trong phần cứng so với phép nhân.
chrisaycock

2
Như tôi nhớ lại Cray-1 không bận tâm đến lệnh chia, nó có lệnh tương hỗ và mong bạn nhân lên sau đó. Vì lý do chính xác này.
Mark Ransom

1
Đánh dấu: Thật vậy, thuật toán chia 4 bước được mô tả trên trang 3-28 của Tài liệu tham khảo phần cứng CRAY-1: xấp xỉ tương hỗ, lặp tương hỗ, tử số * xấp xỉ, thương số nửa chính xác * hệ số hiệu chỉnh.
Gabe

2
@aaronman: Nếu số FP được lưu trữ dưới dạng x ^ y, thì nhân với x ^ -ysẽ giống như phép chia. Tuy nhiên, số FP được lưu trữ dưới dạng x * 2^y. Nhân với x * 2^-ychỉ là phép nhân.
Gabe

4
"Trường lớp" là gì?
Pharap

33

Hãy rất cẩn thận với việc phân chia, và tránh nó khi có thể. Ví dụ: nâng float inverse = 1.0f / divisor;ra khỏi vòng lặp và nhân với inversebên trong vòng lặp. (Nếu lỗi làm tròn trong inversecó thể chấp nhận được)

Thông thường 1.0/xsẽ không thể đại diện chính xác dưới dạng floathoặc double. Nó sẽ chính xác khi nào xlà lũy thừa của 2. Điều này cho phép các trình biên dịch tối ưu hóa x / 2.0fđểx * 0.5f mà không cần bất kỳ sự thay đổi trong kết quả.

Để cho phép trình biên dịch thực hiện việc tối ưu hóa này cho bạn ngay cả khi kết quả không chính xác (hoặc với một ước số biến thời gian chạy), bạn cần các tùy chọn như gcc -O3 -ffast-math. Cụ thể, -freciprocal-math(được kích hoạt bởi -funsafe-math-optimizationsđược kích hoạt bởi -ffast-math) cho phép trình biên dịch thay thế x / ybằngx * (1/y) khi điều đó hữu ích. Các trình biên dịch khác có các tùy chọn tương tự và ICC có thể bật một số tối ưu hóa "không an toàn" theo mặc định (tôi nghĩ là có, nhưng tôi quên).

-ffast-math thường rất quan trọng để cho phép tự động vectơ hóa các vòng FP, đặc biệt là các phép giảm (ví dụ: tính tổng một mảng thành một tổng vô hướng), vì phép toán FP không liên kết. Tại sao GCC không tối ưu hóa a * a * a * a * a * a thành (a * a * a) * (a * a * a)?

Cũng lưu ý rằng trình biên dịch C ++ có thể gập lại +*thành FMA trong một số trường hợp (khi biên dịch cho mục tiêu hỗ trợ nó chẳng hạn -march=haswell), nhưng chúng không thể làm điều đó với /.


Phép chia có độ trễ kém hơn phép nhân hoặc phép cộng (hoặc FMA ) theo hệ số 2 đến 4 trên các CPU x86 hiện đại và thông lượng kém hơn theo hệ số 6 đến 40 1 (đối với vòng lặp chặt chẽ chỉ thực hiện phép chia thay vì chỉ phép nhân).

Đơn vị phân chia / sqrt không được tổng hợp đầy đủ, vì các lý do được giải thích trong câu trả lời của @ NathanWhitehead . Tỷ lệ tồi tệ nhất dành cho vectơ 256b, bởi vì (không giống như các đơn vị thực thi khác) đơn vị chia thường không có chiều rộng đầy đủ, vì vậy vectơ rộng phải được thực hiện thành hai nửa. Một đơn vị thực thi không được kết hợp đầy đủ là điều bất thường khi CPU Intel cóarith.divider_active đếm hiệu suất phần cứng để giúp bạn tìm mã gây tắc nghẽn trên thông lượng bộ chia thay vì tắc nghẽn giao diện người dùng hoặc cổng thực thi thông thường. (Hoặc thường xuyên hơn, tắc nghẽn bộ nhớ hoặc chuỗi độ trễ dài hạn chế tính song song cấp lệnh khiến thông lượng lệnh nhỏ hơn ~ 4 trên mỗi đồng hồ).

Tuy nhiên, phân chia FP và sqrt trên CPU Intel và AMD (ngoài KNL) được thực hiện như một uop duy nhất, vì vậy nó không nhất thiết phải có tác động thông lượng lớn đến mã xung quanh . Trường hợp tốt nhất cho phép chia là khi việc thực hiện không theo thứ tự có thể ẩn độ trễ và khi có nhiều phép nhân và phép cộng (hoặc công việc khác) có thể xảy ra song song với phép chia.

(Phép chia số nguyên được mã vi mô dưới dạng nhiều uops trên Intel, vì vậy nó luôn có nhiều tác động hơn đến mã xung quanh mà phép nhân số nguyên. Nhu cầu về phép chia số nguyên hiệu suất cao ít hơn, vì vậy việc hỗ trợ phần cứng không được ưa thích. Liên quan: hướng dẫn vi mã như idivcó thể gây ra tắc nghẽn phía trước nhạy cảm với căn chỉnh .)

Vì vậy, ví dụ, điều này sẽ thực sự tồi tệ:

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

Tất cả những gì bạn đang làm trong vòng lặp là tải / phân chia / lưu trữ và chúng độc lập nên thông lượng là vấn đề quan trọng chứ không phải độ trễ.

Việc giảm thiểu accumulator /= b[i]sẽ gây tắc nghẽn khi chia hoặc nhân độ trễ thay vì thông lượng. Nhưng với nhiều bộ tích lũy mà bạn chia hoặc nhân ở cuối, bạn có thể ẩn độ trễ và vẫn bão hòa thông lượng. Lưu ý rằng sum += a[i] / b[i]tắc nghẽn về addđộ trễ hoặc divthông lượng, nhưng không phải divđộ trễ vì sự phân chia không nằm trên đường dẫn quan trọng (chuỗi phụ thuộc được thực hiện theo vòng lặp).


Nhưng trong một cái gì đó như thế này ( xấp xỉ một hàm như log(x)với tỷ số của hai đa thức ), phép chia có thể khá rẻ :

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;
}

Đối với log() phạm vi của phần định trị, tỷ lệ của hai đa thức bậc N có ít lỗi hơn nhiều so với một đa thức đơn lẻ có hệ số 2N và việc đánh giá 2 song song cung cấp cho bạn một số song song cấp hướng dẫn trong một phần thân vòng lặp thay vì một chuỗi dài chuỗi dep, làm cho mọi thứ trở nên dễ dàng hơn RẤT NHIỀU để thực hiện không theo thứ tự.

Trong trường hợp này, chúng tôi không tắc nghẽn về độ trễ phân chia vì thực thi không theo thứ tự có thể giữ cho nhiều lần lặp lại của vòng lặp trên các mảng trong quá trình hoạt động.

Chúng tôi không tắc nghẽn về thông lượng phép chia miễn là đa thức của chúng tôi đủ lớn để chúng tôi chỉ có một phép chia cho mỗi 10 lệnh FMA hoặc lâu hơn. (Và trong một log()trường hợp sử dụng thực tế , có rất nhiều công việc trích xuất số mũ / phần định trị và kết hợp mọi thứ lại với nhau một lần nữa, vì vậy, thậm chí còn nhiều việc phải làm giữa các phép chia).


Khi bạn cần phải chia, thường tốt nhất là chỉ chia thay vì rcpps

x86 có lệnh tương đối-tương hỗ ( rcpps), chỉ cung cấp cho bạn 12 bit chính xác. (AVX512F có 14 bit và AVX512ER có 28 bit.)

Bạn có thể sử dụng điều này để làm x / y = x * approx_recip(y)mà không cần sử dụng lệnh chia thực tế. ( rcppsitsef khá nhanh; thường chậm hơn một chút so với phép nhân. Nó sử dụng tra cứu bảng từ một bảng bên trong đến CPU. Phần cứng bộ chia có thể sử dụng cùng một bảng cho một điểm bắt đầu.)

Đối với hầu hết các mục đích, x * rcpps(y)nó quá không chính xác và cần phải lặp lại Newton-Raphson để tăng gấp đôi độ chính xác. Nhưng điều đó khiến bạn tốn 2 nhân và 2 FMA , đồng thời có độ trễ cao như một lệnh chia thực tế. Nếu tất cả những gì bạn đang làm là phân chia, thì đó có thể là một chiến thắng thông lượng. (Nhưng bạn nên tránh loại vòng lặp đó ngay từ đầu nếu có thể, có thể bằng cách thực hiện phép chia như một phần của một vòng lặp khác hoạt động khác.)

Nhưng nếu bạn đang sử dụng phép chia như một phần của một hàm phức tạp hơn, thì rcppsbản thân + mul + FMA bổ sung thường làm cho việc chia bằng một divpslệnh nhanh hơn , ngoại trừ trên các CPU códivps thông lượng .

(Ví dụ: Knight's Landing, hãy xem bên dưới. KNL hỗ trợ AVX512ER , vì vậy đối với floatvectơ, VRCP28PSkết quả đã đủ chính xác để chỉ nhân mà không cần lặp Newton-Raphson. floatKích thước phần định trị chỉ là 24 bit.)


Con số cụ thể từ các bảng của Agner Fog:

Không giống như mọi hoạt động ALU khác, độ trễ / thông lượng phân chia phụ thuộc vào dữ liệu vào một số CPU. Một lần nữa, điều này là do nó quá chậm và không được truyền tải đầy đủ. Lập lịch không theo thứ tự dễ dàng hơn với độ trễ cố định, vì nó tránh được xung đột ghi ngược (khi cùng một cổng thực thi cố gắng tạo ra 2 kết quả trong cùng một chu kỳ, ví dụ như chạy lệnh 3 chu kỳ và sau đó là hai hoạt động 1 chu kỳ) .

Nói chung, các trường hợp nhanh nhất là khi số chia là một số "tròn" như 2.0hoặc 0.5(nghĩa là floatbiểu diễn base2 có rất nhiều số 0 ở cuối trong phần định trị).

float độ trễ (chu kỳ) / thông lượng (chu kỳ trên mỗi lệnh, chỉ chạy ngược lại với các đầu vào độc lập):

                   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 độ trễ (chu kỳ) / thông lượng (chu kỳ cho mỗi lệnh):

                   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 và Broadwell cũng khác nhau, nhưng tôi muốn giữ bàn nhỏ. (Core2 (trước Nehalem) có hiệu suất bộ chia tốt hơn, nhưng tốc độ xung nhịp tối đa của nó thấp hơn.)

Atom, Silvermont và thậm chí Knight's Landing (Xeon Phi dựa trên Silvermont) có hiệu suất phân chia đặc biệt thấp , và thậm chí một vector 128b còn chậm hơn so với vô hướng. CPU Jaguar công suất thấp của AMD (được sử dụng trong một số bảng điều khiển) cũng tương tự. Một dải phân cách hiệu suất cao chiếm nhiều diện tích khuôn. Xeon Phi có công suất thấp trên mỗi lõi và việc đóng gói nhiều lõi trên một khuôn làm cho nó hạn chế diện tích khuôn chặt hơn Skylake-AVX512. Có vẻ như AVX512ER rcp28ps/ pdlà thứ mà bạn "phải" sử dụng trên KNL.

(Xem kết quả InstLatx64 này cho Skylake-AVX512 hay còn gọi là Skylake-X. Các số cho vdivps zmm: 18c / 10c, vì vậy một nửa thông lượng của ymm.)


Chuỗi độ trễ dài sẽ trở thành một vấn đề khi chúng được thực hiện theo vòng lặp hoặc khi chúng dài đến mức chúng ngừng thực hiện không theo thứ tự để tìm kiếm sự song song với công việc độc lập khác.


Chú thích chân trang 1: cách tôi tạo ra các tỷ lệ hiệu suất div so với đa:

Tỷ lệ phân chia FP so với nhiều tỷ lệ hiệu suất thậm chí còn tệ hơn trong các CPU công suất thấp như Silvermont và Jaguar, và thậm chí trong Xeon Phi (KNL, nơi bạn nên sử dụng AVX512ER).

Tỷ lệ thông lượng chia / nhân thực tế cho vô hướng (không vector hóa) double vectơ : 8 trên Ryzen và Skylake với các bộ chia tăng cường, nhưng 16-28 trên Haswell (phụ thuộc vào dữ liệu và nhiều khả năng sẽ kết thúc chu kỳ 28 trừ khi các ước của bạn làm tròn số). Những CPU hiện đại này có bộ chia rất mạnh, nhưng thông lượng nhân 2 mỗi xung nhịp của chúng đã thổi bay nó. (Thậm chí còn hơn thế nữa khi mã của bạn có thể tự động hóa vectơ với vectơ AVX 256b). Cũng lưu ý rằng với các tùy chọn trình biên dịch phù hợp, các thông lượng nhân đó cũng áp dụng cho FMA.

Các số từ http://agner.org/optimize/ bảng hướng dẫn cho Intel Haswell / Skylake và AMD Ryzen, cho vô hướng SSE (không bao gồm x87 fmul/ fdiv) và cho vectơ 256b AVX SIMD của floathoặcdouble . Xem thêm gắn thẻ wiki.


20

Phép chia vốn dĩ là một phép toán chậm hơn nhiều so với phép nhân.

Và trên thực tế, đây có thể là thứ mà trình biên dịch không thể (và bạn có thể không muốn) tối ưu hóa trong nhiều trường hợp do sự không chính xác của dấu chấm động. Hai câu sau:

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

không ngữ nghĩa giống hệt nhau - 0.1không thể được đại diện chính xác như một double, do đó, một giá trị hơi khác nhau sẽ kết thúc được sử dụng - thay thế các nhân cho bộ phận trong trường hợp này sẽ mang lại một kết quả khác nhau!


3
Với g ++, 200.f / 10 và 200.f * 0.1 phát ra chính xác cùng một mã.
Johan Kotlinski

10
@kotlinski: điều đó làm cho g ++ sai, không phải câu lệnh của tôi. Tôi cho rằng người ta có thể tranh luận rằng nếu sự khác biệt quan trọng, bạn không nên sử dụng float ngay từ đầu, nhưng đó chắc chắn là điều tôi chỉ làm ở các cấp độ tối ưu hóa cao hơn nếu tôi là tác giả trình biên dịch.
Michael Borgwardt

3
@Michael: Sai theo tiêu chuẩn nào?
Johan Kotlinski

9
Nếu bạn thử nó, theo cách hợp lý (điều đó không cho phép trình biên dịch tối ưu hóa hoặc thay thế), bạn sẽ thấy rằng 7/10 và 7 * 0,1 sử dụng độ chính xác kép không cho kết quả giống nhau. Phép nhân cho câu trả lời sai nó cho một số lớn hơn số chia. dấu chấm động là về độ chính xác, nếu ngay cả một bit bị tắt thì nó là sai. Tương tự với 7/5! = 7 / 0,2, nhưng lấy một số bạn có thể đại diện cho 7/4 và 7 * 0,25, sẽ cho kết quả tương tự. IEEE hỗ trợ nhiều chế độ làm tròn để bạn có thể khắc phục một số vấn đề này (nếu bạn biết câu trả lời trước thời hạn).
old_timer

6
Ngẫu nhiên, trong trường hợp này, nhân và chia đều nhanh như nhau - chúng được tính trong thời gian biên dịch.
Johan Kotlinski

9

Đúng. Mỗi FPU mà tôi biết đều thực hiện các phép nhân nhanh hơn nhiều so với phép chia.

Tuy nhiên, PC hiện đại rất nhanh. Chúng cũng chứa các kiến ​​trúc đường ống có thể làm cho sự khác biệt có thể bị bỏ qua trong nhiều trường hợp. Đầu tiên, bất kỳ trình biên dịch tốt nào sẽ thực hiện thao tác phân chia mà bạn đã hiển thị tại thời điểm biên dịch với tính năng tối ưu hóa được bật. Đối với ví dụ đã cập nhật của bạn, bất kỳ trình biên dịch tốt nào sẽ tự thực hiện chuyển đổi đó.

Vì vậy, nói chung bạn nên lo lắng về việc làm cho mã của bạn có thể đọc được và hãy để trình biên dịch lo lắng về việc làm cho nó nhanh. Chỉ khi bạn gặp vấn đề về tốc độ đo với dòng đó, bạn mới nên lo lắng về việc sửa đổi mã của mình vì lợi ích của tốc độ. Các trình biên dịch nhận thức rõ ràng về những gì nhanh hơn những gì trên CPU của họ và nói chung là những trình tối ưu hóa tốt hơn nhiều so với những gì bạn có thể hy vọng.


4
Làm cho mã có thể đọc được là không đủ. Đôi khi có những yêu cầu để tối ưu hóa thứ gì đó và điều đó nói chung sẽ làm cho mã khó hiểu. Nhà phát triển giỏi trước tiên sẽ viết các bài kiểm tra đơn vị tốt, và sau đó tối ưu hóa mã. Khả năng đọc là tốt, nhưng không phải lúc nào cũng đạt được mục tiêu.
BЈовић

@VJo - Bạn bỏ sót câu thứ hai đến câu cuối cùng của tôi hoặc bạn không đồng ý với các ưu tiên của tôi. Nếu là cái sau, tôi e rằng chúng ta sẽ không đồng ý.
TED

14
Trình biên dịch không thể tối ưu hóa điều này cho bạn. Chúng không được phép làm như vậy vì kết quả sẽ khác và không tuân theo (wrt IEEE-754). gcc cung cấp một -ffast-mathtùy chọn cho mục đích này, nhưng nó phá vỡ nhiều thứ và không thể được sử dụng nói chung.
R .. GitHub NGỪNG TRỢ GIÚP ICE

2
Tôi cho là có một chút hoại tử, nhưng sự phân chia thường không được kết nối. Vì vậy, nó thực sự có thể tạo ra một vết lõm lớn trong hiệu suất. Nếu bất cứ điều gì, pipelining làm cho sự khác biệt về hiệu suất của phép nhân và phép chia thậm chí còn lớn hơn, bởi vì một trong những pipelined nhưng cái kia thì không.
harold

11
Các trình biên dịch C được phép tối ưu hóa điều này vì cả phép chia cho 2,0 và phép nhân cho 0,5 đều chính xác khi sử dụng số học nhị phân, do đó kết quả là như nhau. Xem phần F.8.2 của tiêu chuẩn ISO C99, phần này cho thấy chính xác trường hợp này là một sự chuyển đổi cho phép khi sử dụng các liên kết IEEE-754.
njuffa

8

Hãy suy nghĩ về những gì cần thiết cho phép nhân hai số n bit. Với phương pháp đơn giản nhất, bạn lấy một số x và liên tục dịch chuyển và có điều kiện thêm nó vào bộ tích lũy (dựa trên một bit trong số kia y). Sau khi bổ sung n, bạn đã hoàn thành. Kết quả của bạn vừa với 2n bit.

Đối với phép chia, bạn bắt đầu với x gồm 2n bit và y là n bit, bạn muốn tính x / y. Phương pháp đơn giản nhất là chia dài, nhưng ở dạng nhị phân. Ở mỗi giai đoạn, bạn thực hiện phép so sánh và phép trừ để nhận thêm một bit của thương. Bạn cần thực hiện n bước.

Một số điểm khác biệt: mỗi bước của phép nhân chỉ cần nhìn vào 1 bit; mỗi giai đoạn của phép chia cần xem xét n bit trong quá trình so sánh. Mỗi giai đoạn của phép nhân là độc lập với tất cả các giai đoạn khác (không quan trọng thứ tự bạn thêm các sản phẩm từng phần); để phân chia mỗi bước phụ thuộc vào bước trước đó. Đây là một vấn đề lớn trong phần cứng. Nếu mọi thứ có thể được thực hiện độc lập thì chúng có thể xảy ra cùng một lúc trong một chu kỳ đồng hồ.


Các CPU Intel gần đây (kể từ Broadwell) sử dụng bộ chia cơ số 1024 để phân chia được thực hiện trong ít bước hơn. Không giống như hầu hết mọi thứ khác, đơn vị phân chia không hoàn toàn liên kết (vì như bạn nói, thiếu tính độc lập / song song là một vấn đề lớn trong phần cứng). ví dụ như Skylake đóng gói phép chia độ chính xác kép ( vdivpd ymm) có thông lượng kém hơn 16 lần so với phép nhân ( vmulpd ymm) và nó tệ hơn trong các CPU cũ hơn với phần cứng chia ít mạnh hơn. agner.org/optimize
Peter Cordes

2

Newton rhapson giải phép chia số nguyên theo độ phức tạp O (M (n)) thông qua phép gần đúng đại số tuyến tính. Nhanh hơn Độ phức tạp O (n * n) khác.

Trong mã Phương thức chứa 10mults 9adds 2bitwiseshifts.

Điều này giải thích tại sao một phép chia có số cpu gần gấp 12 lần một phép nhân.


1

Câu trả lời phụ thuộc vào nền tảng mà bạn đang lập trình.

Ví dụ: thực hiện nhiều phép nhân trên một mảng trên x86 sẽ nhanh hơn nhiều sau đó thực hiện phép chia, vì trình biên dịch sẽ tạo mã trình hợp dịch sử dụng lệnh SIMD. Vì không có phép chia trong hướng dẫn SIMD, nên bạn sẽ thấy những cải tiến lớn khi sử dụng phép nhân rồi phép chia.


Nhưng các câu trả lời khác cũng tốt. Phép chia thường chậm hơn hoặc bằng phép nhân, nhưng nó phụ thuộc vào nền tảng.
BЈовић


divpslà một phần của SSE1 gốc, được giới thiệu trong PentiumIII. Không có lệnh chia số nguyên SIMD , nhưng phép chia FPD SIMD thực sự tồn tại. Đơn vị phân chia đôi khi có thông lượng / độ trễ thậm chí còn tệ hơn đối với các vectơ rộng (đặc biệt là 256b AVX) so với các vectơ vô hướng hoặc 128b. Ngay cả Intel Skylake (với phân chia FP nhanh hơn đáng kể so với Haswell / Broadwell) cũng có divps xmm(4 phao đóng gói): độ trễ 11c, thông lượng một trên 3c. divps ymm(8 phao đóng gói): độ trễ 11c, một trên 5c thông lượng. (hoặc đối với đồ đôi được đóng gói: một trên 4c hoặc một trên 8c) Xem wiki thẻ x86 để biết liên kết hoàn thiện.
Peter Cordes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.