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 inverse
bên trong vòng lặp. (Nếu lỗi làm tròn trong inverse
có thể chấp nhận được)
Thông thường 1.0/x
sẽ không thể đại diện chính xác dưới dạng float
hoặc double
. Nó sẽ chính xác khi nào x
là 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 / y
bằ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 +
và *
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ư idiv
có 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;
float inv = 1.0 / scale;
for ()
a[i] = b[i] * inv;
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 div
thô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 () {
float p = polynomial(b[i], 1.23, -4.56, ...);
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ế. ( rcpps
itsef 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ì rcpps
bản thân + mul + FMA bổ sung thường làm cho việc chia bằng một divps
lệ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 float
vectơ, VRCP28PS
kết quả đã đủ chính xác để chỉ nhân mà không cần lặp Newton-Raphson. float
Kí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.0
hoặc 0.5
(nghĩa là float
biể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
/ pd
là 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 float
hoặcdouble
. Xem thêmx86 gắn thẻ wiki.