Những opcodes nào nhanh hơn ở cấp độ CPU? [đóng cửa]


19

Trong mọi ngôn ngữ lập trình, có các bộ opcodes được khuyến nghị hơn các ngôn ngữ khác. Tôi đã cố gắng liệt kê chúng ở đây, theo thứ tự tốc độ.

  1. Bitwise
  2. Phép cộng / phép trừ
  3. Nhân / chia số nguyên
  4. So sánh
  5. Kiểm soát dòng chảy
  6. Phép cộng / phép trừ
  7. Phao nhân / chia

Khi bạn cần mã hiệu suất cao, C ++ có thể được tối ưu hóa bằng cách lắp ráp, để sử dụng các hướng dẫn SIMD hoặc luồng điều khiển, loại dữ liệu hiệu quả hơn, v.v. Vì vậy, tôi đang cố gắng hiểu loại dữ liệu (int32 / float32 / float64) hay các hoạt động được sử dụng ( *, +, &) ảnh hưởng đến hiệu suất ở cấp CPU.

  1. Là một nhân nhân chậm hơn trên CPU so với một bổ sung?
  2. Trong lý thuyết MCU, bạn học được rằng tốc độ của opcodes được xác định bởi số chu kỳ CPU cần thiết để thực hiện. Vì vậy, nó có nghĩa là nhân lên mất 4 chu kỳ và thêm mất 2?
  3. Chính xác các đặc điểm tốc độ của opcodes toán học và kiểm soát dòng cơ bản là gì?
  4. Nếu hai opcodes có cùng số chu kỳ để thực thi, thì cả hai có thể được sử dụng thay thế cho nhau mà không có bất kỳ tăng / giảm hiệu suất nào không?
  5. Bất kỳ chi tiết kỹ thuật nào khác bạn có thể chia sẻ về hiệu suất CPU x86 đều được đánh giá cao

17
Điều này nghe có vẻ giống như tối ưu hóa sớm và hãy nhớ rằng trình biên dịch không xuất ra những gì bạn nhập và bạn thực sự không muốn viết lắp ráp trừ khi bạn thực sự có quá.
Roy T.

3
Phép nhân và phép chia nổi là những thứ hoàn toàn khác nhau, bạn không nên đặt chúng trong cùng một danh mục. Đối với các số n bit, phép nhân là một quá trình O (n) và phép chia là một quá trình O (nlogn). Điều này làm cho việc phân chia chậm hơn khoảng 5 lần so với nhân trên các CPU hiện đại.
sam hocevar

1
Câu trả lời thực sự duy nhất là "hồ sơ nó".
Tetrad

1
Mở rộng câu trả lời của Roy, lắp ráp tối ưu hóa tay hầu như luôn luôn là một mất mát trừ khi bạn thực sự đặc biệt. CPU hiện đại là những con thú rất phức tạp và trình biên dịch tối ưu hóa tốt giúp loại bỏ các biến đổi mã hoàn toàn không rõ ràng và không tầm thường đối với mã bằng tay. Ngay cả đối với SSE / SIMD, luôn luôn sử dụng nội tại trong C / C ++ và để trình biên dịch tối ưu hóa việc sử dụng chúng cho bạn. Sử dụng lắp ráp thô sẽ vô hiệu hóa tối ưu hóa trình biên dịch và bạn mất lớn.
Sean Middleditch

Bạn không cần phải tối ưu hóa để lắp ráp để sử dụng SIMD. SIMD rất hữu ích để tối ưu hóa tùy thuộc vào tình huống, nhưng có một quy ước hầu hết là tiêu chuẩn (ít nhất là nó hoạt động trên GCC và MSVC) để sử dụng SSE2. Theo như danh sách của bạn, trên bộ xử lý đa đường siêu hiện đại, sự phụ thuộc dữ liệu và áp suất đăng ký gây ra nhiều vấn đề hơn so với hiệu suất nguyên và đôi khi là hiệu suất dấu phẩy động; điều tương tự cũng đúng với địa phương dữ liệu. Nhân tiện, phép chia số nguyên giống như phép nhân trên x86 hiện đại
OrgnlDave

Câu trả lời:


26

Hướng dẫn tối ưu hóa của Agner Fog là tuyệt vời. Anh ta có các hướng dẫn, bảng thời gian hướng dẫn và tài liệu về kiến ​​trúc vi mô của tất cả các thiết kế CPU x86 gần đây (quay trở lại như Intel Pentium). Xem thêm một số tài nguyên khác được liên kết từ /programming//tags/x86/info

Để giải trí, tôi sẽ trả lời một số câu hỏi (số từ các CPU Intel gần đây). Lựa chọn ops không phải là yếu tố chính trong việc tối ưu hóa mã (trừ khi bạn có thể tránh phân chia.)

Là một nhân nhân chậm hơn trên CPU so với một bổ sung?

Có (trừ khi nó có sức mạnh bằng 2). (Độ trễ gấp 3-4 lần, chỉ với một thông lượng trên mỗi đồng hồ trên Intel.) Tuy nhiên, đừng đi quá xa để tránh điều đó, vì nó nhanh như 2 hoặc 3 lần thêm.

Chính xác các đặc điểm tốc độ của opcodes toán học và kiểm soát dòng cơ bản là gì?

Xem bảng hướng dẫn của Agner Fog và hướng dẫn kiến ​​trúc vi mô nếu bạn muốn biết chính xác : P. Cẩn thận với những cú nhảy có điều kiện. Nhảy không điều kiện (như các cuộc gọi chức năng) có một số chi phí nhỏ, nhưng không nhiều.

Nếu hai opcodes có cùng số chu kỳ để thực thi, thì cả hai có thể được sử dụng thay thế cho nhau mà không có bất kỳ tăng / giảm hiệu suất nào không?

Không, họ có thể cạnh tranh cho cùng một cổng thực thi như một cái gì đó khác, hoặc họ có thể không. Nó phụ thuộc vào chuỗi phụ thuộc khác mà CPU có thể làm việc song song. (Trong thực tế, thường không có bất kỳ quyết định hữu ích nào được đưa ra. Đôi khi, bạn có thể sử dụng dịch chuyển vectơ hoặc xáo trộn vectơ chạy trên các cổng khác nhau trên CPU Intel. Nhưng thay đổi theo từng byte của toàn bộ thanh ghi ( PSLLDQv.v.) chạy trong đơn vị xáo trộn.)

Bất kỳ chi tiết kỹ thuật nào khác bạn có thể chia sẻ về hiệu suất CPU x86 đều được đánh giá cao

Các tài liệu vi mô của Agner Fog mô tả các đường ống của CPU Intel và AMD đủ chi tiết để tìm ra chính xác số vòng lặp cần thực hiện trong mỗi lần lặp và liệu nút cổ chai có thông lượng, chuỗi phụ thuộc hay tranh chấp cho một cổng thực thi hay không. Xem một số câu trả lời của tôi trên StackOverflow, như cái này hoặc cái này .

Ngoài ra, http://www.realworldtech.com/haswell-cpu/ (và tương tự cho các thiết kế trước đó) rất thú vị nếu bạn thích thiết kế CPU.

Đây là danh sách của bạn, được sắp xếp cho CPU Haswell, dựa trên các khách mời tốt nhất của tôi. Đây thực sự không phải là một cách suy nghĩ hữu ích về mọi thứ cho bất cứ điều gì ngoài việc điều chỉnh một vòng lặp asm. Hiệu ứng dự đoán bộ nhớ cache / chi nhánh thường chiếm ưu thế, vì vậy hãy viết mã của bạn để có các mẫu tốt. Các con số rất sóng và cố gắng tính toán độ trễ cao, ngay cả khi thông lượng không phải là vấn đề hoặc để tạo ra nhiều uops làm tắc nghẽn đường ống cho những điều khác xảy ra song song. Đặc biệt các số bộ đệm / chi nhánh rất được tạo ra. Các vấn đề về độ trễ đối với các phụ thuộc mang theo vòng lặp, các vấn đề thông lượng khi mỗi lần lặp là độc lập.

TL: DR những con số này được tạo thành dựa trên những gì tôi đang hình dung cho một trường hợp sử dụng "điển hình", cho đến khi đánh đổi giữa độ trễ, tắc nghẽn cổng thực thi và thông lượng mặt trước (hoặc quầy hàng cho những thứ như sai sót chi nhánh ). Vui lòng không sử dụng những con số này cho bất kỳ loại phân tích hoàn hảo nghiêm túc nào .

  • 0,5 đến 1 Bitwise / Phép cộng / Phép trừ /
    dịch chuyển và xoay (số đếm thời gian biên dịch) /
    phiên bản vectơ của tất cả các phiên bản này (1 đến 4 trên mỗi chu kỳ, độ trễ 1 chu kỳ)
  • 1 vectơ tối thiểu, tối đa, so sánh bằng, so sánh lớn hơn (để tạo mặt nạ)
  • Xáo trộn 1,5 vector. Haswell và mới hơn chỉ có một cổng xáo trộn và đối với tôi, nó thường cần nhiều sự xáo trộn nếu bạn cần, vì vậy tôi cân nhắc nó cao hơn một chút để khuyến khích suy nghĩ về việc sử dụng ít xáo trộn hơn. Chúng không miễn phí, đặc biệt. nếu bạn cần một mặt nạ kiểm soát pshufb từ bộ nhớ.
  • Tải 1,5 lần / lưu trữ (nhấn bộ đệm L1. Thông lượng tốt hơn độ trễ)
  • Phép nhân số nguyên 1,75 (độ trễ 3c / một lần truy cập 1c trên Intel, 4c lat trên AMD và chỉ một lần cho mỗi lần truy cập 2c). Các hằng số nhỏ thậm chí còn rẻ hơn khi sử dụng LEA và / hoặc ADD / SUB / shift . Nhưng tất nhiên các hằng số thời gian biên dịch luôn tốt và thường có thể tối ưu hóa thành những thứ khác. (Và nhân trong một vòng lặp thường có thể được giảm sức mạnh bởi trình biên dịch thành tmp += 7trong một vòng lặp thay vì tmp = i*7)
  • 1,75 một số xáo trộn vector 256b (độ trễ thêm vào trong phần bên trong có thể di chuyển dữ liệu giữa các làn 128b của vectơ AVX). (Hoặc 3 đến 7 trên Ryzen, nơi các làn xáo trộn làn đường cần nhiều vòng lặp hơn)
  • 2 fp add / sub (và các phiên bản vectơ giống nhau) (1 hoặc 2 cho mỗi thông lượng chu kỳ, độ trễ 3 đến 5 chu kỳ). Có thể chậm nếu bạn tắc nghẽn về độ trễ, ví dụ: tổng một mảng chỉ có 1 sumbiến. (Tôi có thể cân trọng lượng này và fp mul thấp đến 1 hoặc cao tới 5 tùy theo trường hợp sử dụng).
  • 2 vector fp mul hoặc FMA. (x * y + z rẻ như một mul hoặc add nếu bạn biên dịch với hỗ trợ FMA được kích hoạt).
  • 2 chèn / trích xuất các thanh ghi mục đích chung vào các phần tử vectơ ( _mm_insert_epi8, v.v.)
  • 2,25 vector int mul (các phần tử 16 bit hoặc pmaddubsw thực hiện 8 * 8 -> 16 bit). Rẻ hơn trên Skylake, với thông lượng tốt hơn so với mul vô hướng
  • 2,25 thay đổi / xoay theo số lượng biến (độ trễ 2c, thông lượng trên mỗi 2c trên Intel, nhanh hơn trên AMD hoặc với BMI2)
  • 2.5 So sánh mà không phân nhánh ( y = x ? a : b, hoặc y = x >= 0) ( test / setcchoặc cmov)
  • 3 int-> chuyển đổi float
  • 3 dự đoán hoàn hảo Luồng điều khiển (dự đoán nhánh, gọi, trả lại).
  • 4 vector int mul (phần tử 32 bit) (2 uops, độ trễ 10c trên Haswell)
  • 4 số nguyên hoặc % theo hằng số thời gian biên dịch (không lũy ​​thừa 2).
  • 7 vectơ ngang (vd PHADD thêm các giá trị trong một vectơ)
  • 11 (vector) Phòng FP (độ trễ 10-13c, một thông lượng trên 7c hoặc tệ hơn). (Có thể rẻ nếu được sử dụng hiếm khi, nhưng thông lượng kém hơn 6 đến 40 lần so với FP mul)
  • 13? Kiểm soát lưu lượng (chi nhánh dự đoán kém, có thể dự đoán 75%)
  • Phân chia 13 int ( vâng , thực sự , nó chậm hơn phân chia FP và không thể vector hóa). (lưu ý rằng trình biên dịch chia cho một hằng số bằng cách sử dụng mul / shift / add với hằng số ma thuật và div / mod theo lũy thừa là 2 rất rẻ.)
  • 16 (vectơ) FP sqrt
  • 25? tải (nhấn bộ đệm L3). (cửa hàng nhớ cache rẻ hơn tải.)
  • 50? FP trig / exp / log. Nếu bạn cần nhiều exp / log và không cần độ chính xác đầy đủ, bạn có thể trao đổi độ chính xác cho tốc độ với đa thức ngắn hơn và / hoặc bảng. Bạn cũng có thể vector hóa SIMD.
  • 50-80? chi nhánh luôn được dự đoán trước, chi phí 15-20 chu kỳ
  • 200-400? tải / lưu trữ (nhớ cache)
  • 3000 ??? đọc trang từ tệp (nhấn bộ đệm đĩa hệ điều hành) (tạo số ở đây)
  • 20000 ??? trang đọc đĩa (bỏ lỡ bộ đệm đĩa hệ điều hành, SSD nhanh) (số hoàn toàn tạo thành)

Tôi hoàn toàn làm điều này dựa trên phỏng đoán . Nếu có gì đó không ổn, đó là do tôi đã nghĩ đến một trường hợp sử dụng khác hoặc lỗi chỉnh sửa.

Chi phí tương đối của mọi thứ trên CPU AMD sẽ tương tự nhau, ngoại trừ chúng có bộ dịch chuyển số nguyên nhanh hơn khi số lượng ca thay đổi. Các CPU gia đình AMD Bulldozer tất nhiên chậm hơn trên hầu hết các mã, vì nhiều lý do. (Ryzen khá giỏi trong nhiều thứ).

Hãy nhớ rằng thực sự không thể đun sôi mọi thứ với chi phí một chiều . Khác với lỗi nhớ cache và sai lệch nhánh, nút cổ chai trong một khối mã có thể là độ trễ, tổng thông lượng uop (frontend) hoặc thông lượng của một cổng cụ thể (cổng thực thi).

Một hoạt động "chậm" như phân chia FP có thể rất rẻ nếu mã xung quanh khiến CPU bận rộn với công việc khác . (vectơ div div hoặc sqrt là 1 uop mỗi cái, chúng chỉ có độ trễ và thông lượng kém. Chúng chỉ chặn đơn vị phân chia, không phải toàn bộ cổng thực thi mà nó bật. Số nguyên div là một số u.) Vì vậy, nếu bạn chỉ có một phân chia FP cứ sau 20 mul và thêm, và có một công việc khác để CPU thực hiện (ví dụ: lặp vòng lặp độc lập), thì "chi phí" của div FP có thể tương đương với một mul FP. Đây có lẽ là ví dụ tốt nhất về một cái gì đó có thông lượng thấp khi tất cả những gì bạn đang làm, nhưng kết hợp rất tốt với mã khác (khi độ trễ không phải là một yếu tố), vì tổng số uops thấp.

Lưu ý rằng phân chia số nguyên gần như không thân thiện với mã xung quanh: Trên Haswell, nó là 9 uops, với thông lượng một trên 8-11c và độ trễ 22-29c. (Phân chia 64 bit chậm hơn nhiều , ngay cả trên Skylake.) Vì vậy, độ trễ và số lượng thông lượng có phần giống với div div, nhưng div div chỉ là một uop.

Để biết ví dụ về việc phân tích một chuỗi ngắn các thông tin về thông lượng, độ trễ và tổng số vòng, hãy xem một số câu trả lời SO của tôi:

IDK nếu người khác viết câu trả lời SO bao gồm loại phân tích này. Tôi có một thời gian dễ dàng hơn để tìm kiếm chính mình, bởi vì tôi biết tôi thường đi sâu vào chi tiết này và tôi có thể nhớ những gì tôi đã viết.


"Nhánh dự đoán" ở 4 có ý nghĩa - "nhánh dự đoán" ở 20-25 thực sự phải là gì? (Tôi đã nghĩ rằng chi nhánh mis-dự đoán (được liệt kê khoảng 13) là đắt hơn nhiều hơn thế, nhưng đó là chính xác lý do tại sao tôi trên trang này, để học một cái gì đó gần với sự thật - nhờ cho bảng tuyệt vời!)
Matt

@Matt: Tôi nghĩ đó là một lỗi chỉnh sửa và được cho là "chi nhánh bị dự đoán sai". Cảm ơn đã chỉ ra rằng. Lưu ý rằng 13 là dành cho một nhánh được dự đoán không hoàn hảo, không phải là một nhánh luôn bị dự đoán sai, vì vậy tôi đã làm rõ điều đó. Tôi đã làm lại việc rửa tay và thực hiện một số chỉnh sửa. : P
Peter Cordes

16

Nó phụ thuộc vào CPU được đề cập, nhưng đối với CPU hiện đại, danh sách này là như thế này:

  1. Bitwise, cộng, trừ, so sánh, nhân
  2. Bộ phận
  3. Kiểm soát luồng (xem câu trả lời 3)

Tùy thuộc vào CPU, có thể có một số lượng đáng kể để làm việc với các loại dữ liệu 64 bit.

Những câu hỏi của bạn:

  1. Hoàn toàn không hoặc không đáng kể trên một CPU hiện đại. Phụ thuộc vào CPU.
  2. Thông tin đó có vẻ như đã lỗi thời từ 20 đến 30 năm (Trường học thật tệ, giờ bạn đã có bằng chứng), các CPU hiện đại xử lý một số lượng hướng dẫn khác nhau trên mỗi đồng hồ, có bao nhiêu phụ thuộc vào những gì bộ lập lịch đưa ra.
  3. Phân chia chậm hơn một chút so với phần còn lại, luồng điều khiển rất nhanh nếu dự đoán nhánh là chính xác và rất chậm nếu sai (giống như 20 chu kỳ, phụ thuộc vào CPU). Kết quả là rất nhiều mã bị giới hạn chủ yếu bởi luồng điều khiển. Đừng làm với ifnhững gì bạn có thể làm một cách hợp lý với số học.
  4. Không có số cố định cho bao nhiêu chu kỳ mà bất kỳ lệnh nào thực hiện, nhưng đôi khi hai hướng dẫn khác nhau có thể thực hiện như nhau, đặt chúng vào ngữ cảnh khác và có thể chúng không chạy chúng trên CPU khác và bạn có thể thấy kết quả thứ 3.
  5. Trên luồng điều khiển, bộ lọc thời gian lớn khác là lỗi bộ nhớ cache, bất cứ khi nào bạn cố đọc dữ liệu không có trong bộ đệm, CPU sẽ phải đợi nó được lấy từ bộ nhớ. Nói chung, bạn nên cố gắng xử lý các phần dữ liệu cạnh nhau đồng thời thay vì chọn dữ liệu từ khắp mọi nơi.

Và cuối cùng, nếu bạn đang làm một trò chơi, đừng quá lo lắng về tất cả những điều này, hãy tập trung vào việc tạo ra một trò chơi tốt hơn là cắt các chu kỳ CPU.


Tôi cũng muốn chỉ ra rằng FPU khá nhanh chóng: đặc biệt là trên Intel - vì vậy điểm cố định chỉ thực sự cần thiết nếu bạn muốn kết quả xác định.
Jonathan Dickinson

2
Tôi chỉ nhấn mạnh hơn vào phần cuối cùng - tạo ra một trò chơi hay. Nó giúp để có mã rõ ràng - đó là lý do tại sao 3. chỉ áp dụng khi bạn thực sự đo lường một vấn đề hiệu suất. Luôn dễ dàng thay đổi những if đó thành một thứ tốt hơn nếu có nhu cầu. Mặt khác, 5. phức tạp hơn - Tôi chắc chắn đồng ý rằng đó là trường hợp bạn thực sự muốn nghĩ trước tiên, vì nó thường có nghĩa là thay đổi kiến ​​trúc.
Luaan

3

Tôi đã thực hiện một thử nghiệm về hoạt động phù thủy số nguyên được lặp một triệu lần trên x64_64, đạt được kết luận ngắn gọn như dưới đây,

thêm --- 116 micro giây

phụ ---- 116 micro giây

mul ---- 1036 micro giây

div ---- 13037 micro giây

dữ liệu trên đã giảm chi phí gây ra bởi vòng lặp,


2

Hướng dẫn sử dụng bộ xử lý intel là một bản tải xuống miễn phí từ trang web của họ. Chúng khá lớn nhưng về mặt kỹ thuật có thể trả lời câu hỏi của bạn. Hướng dẫn tối ưu hóa cụ thể là những gì bạn đang theo đuổi, nhưng hướng dẫn sử dụng cũng có thời gian và độ trễ cho hầu hết các dòng CPU chính cho hướng dẫn simd vì chúng thay đổi từ chip sang chip.

Nói chung, tôi sẽ xem xét các nhánh đầy đủ cũng như đuổi theo con trỏ (truy cập danh sách liên kết, gọi các hàm ảo) là hàng đầu cho những kẻ giết người hoàn hảo, nhưng cp x86 / x64 rất tốt cả hai, so với các kiến ​​trúc khác. Nếu bạn từng chuyển sang nền tảng khác, bạn sẽ thấy chúng có thể gây ra bao nhiêu vấn đề, nếu bạn đang viết mã hiệu suất cao.


+1, tải phụ thuộc (đuổi theo con trỏ) là một vấn đề lớn. Một lỗi bộ nhớ cache sẽ chặn tải trong tương lai ngay cả khi bắt đầu. Có nhiều tải từ bộ nhớ chính trong chuyến bay cùng một lúc sẽ cho băng thông tốt hơn nhiều so với việc một op yêu cầu trước đó phải hoàn thành đầy đủ.
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.