Opcode mana yang lebih cepat pada level CPU? [Tutup]


19

Di setiap bahasa pemrograman ada set opcode yang direkomendasikan daripada yang lain. Saya sudah mencoba daftar mereka di sini, dalam urutan kecepatan.

  1. Bitwise
  2. Penambahan / Pengurangan Integer
  3. Penggandaan / Divisi Integer
  4. Perbandingan
  5. Mengontrol aliran
  6. Penambahan / Pengurangan Float
  7. Penggandaan / Divisi Float

Di mana Anda memerlukan kode berkinerja tinggi, C ++ dapat dioptimalkan secara tangan dalam perakitan, untuk menggunakan instruksi SIMD atau aliran kontrol yang lebih efisien, tipe data, dll. Jadi saya mencoba memahami apakah tipe data (int32 / float32 / float64) atau operasi yang digunakan ( *, +, &) mempengaruhi kinerja di tingkat CPU.

  1. Apakah satu kali lipat lebih lambat pada CPU daripada penambahan?
  2. Dalam teori MCU Anda belajar bahwa kecepatan opcodes ditentukan oleh jumlah siklus CPU yang diperlukan untuk mengeksekusi. Jadi, apakah ini berarti bahwa multiply membutuhkan 4 siklus dan add membutuhkan 2?
  3. Apa saja karakteristik kecepatan dari matematika dasar dan opcode aliran kontrol?
  4. Jika dua opcode mengambil jumlah siklus yang sama untuk dieksekusi, maka keduanya dapat digunakan secara bergantian tanpa ada untung / rugi kinerja?
  5. Detail teknis lainnya yang dapat Anda bagikan mengenai kinerja CPU x86 dihargai

17
Ini kedengarannya seperti optimasi prematur, dan ingat bahwa kompiler tidak menampilkan apa yang Anda ketikkan, dan Anda benar-benar tidak ingin menulis rakitan kecuali Anda benar-benar juga.
Roy T.

3
Perkalian dan pembagian float adalah hal yang sangat berbeda, Anda tidak harus menempatkan mereka dalam kategori yang sama. Untuk bilangan n-bit, perkalian adalah proses O (n), dan pembagian adalah proses O (nlogn). Hal ini membuat pembagian sekitar 5 kali lebih lambat daripada multiplikasi pada CPU modern.
sam hocevar

1
Satu-satunya jawaban nyata adalah "profil itu".
Tetrad

1
Memperluas jawaban Roy, perakitan yang mengoptimalkan tangan hampir selalu akan menjadi kerugian bersih kecuali Anda benar-benar luar biasa. CPU modern adalah binatang yang sangat kompleks dan kompiler pengoptimalisasi yang baik melakukan transformasi kode yang sepenuhnya tidak jelas dan tidak sepele terhadap kode dengan tangan. Bahkan untuk SSE / SIMD, selalu selalu menggunakan intrinsik dalam C / C ++, dan biarkan kompiler mengoptimalkan penggunaannya untuk Anda. Menggunakan perakitan mentah menonaktifkan optimisasi kompiler dan Anda akan rugi besar.
Sean Middleditch

Anda tidak perlu mengoptimalkan secara manual ke perakitan untuk menggunakan SIMD. SIMD sangat berguna untuk mengoptimalkan tergantung pada situasinya, tetapi ada sebagian besar standar konvensi (setidaknya bekerja pada GCC dan MSVC) untuk menggunakan SSE2. Sejauh menyangkut daftar Anda, pada prosesor multi-pipelined supserscalar modern, ketergantungan data dan tekanan register menyebabkan lebih banyak masalah daripada integer mentah dan terkadang kinerja floating point; hal yang sama berlaku untuk lokalitas data. Omong-omong, pembagian integer sama dengan perkalian pada x86 modern
OrgnlDave

Jawaban:


26

Panduan optimasi Agner Fog sangat baik. Ia memiliki panduan, tabel timing instruksi, dan dokumen tentang arsitektur mikro semua desain CPU x86 terbaru (kembali sejauh Intel Pentium). Lihat juga beberapa sumber lain yang ditautkan dari /programming//tags/x86/info

Hanya untuk bersenang-senang, saya akan menjawab beberapa pertanyaan (angka dari CPU Intel terbaru). Pilihan ops bukanlah faktor utama dalam mengoptimalkan kode (kecuali Anda dapat menghindari pembagian.)

Apakah satu kali lipat lebih lambat pada CPU daripada penambahan?

Ya (kecuali jika kekuatan 2). (3-4x latency, dengan hanya satu throughput clock pada Intel.) Namun, jangan jauh-jauh untuk menghindarinya, karena itu secepat 2 atau 3 menambahkan.

Apa saja karakteristik kecepatan dari matematika dasar dan opcode aliran kontrol?

Lihat tabel instruksi Agner Fog dan panduan arsitektur mikro jika Anda ingin tahu persis : P. Hati-hati dengan lompatan bersyarat. Lompatan tanpa syarat (seperti pemanggilan fungsi) memiliki beberapa overhead kecil, tetapi tidak banyak.

Jika dua opcode mengambil jumlah siklus yang sama untuk dieksekusi, maka keduanya dapat digunakan secara bergantian tanpa ada untung / rugi kinerja?

Tidak, mereka mungkin bersaing untuk port eksekusi yang sama dengan yang lain, atau mereka mungkin tidak. Itu tergantung pada rantai ketergantungan apa yang bisa dikerjakan CPU secara paralel. (Dalam praktiknya, biasanya tidak ada keputusan yang berguna untuk dibuat. Kadang-kadang muncul bahwa Anda dapat menggunakan pergeseran vektor atau pengocokan vektor, yang berjalan pada port yang berbeda pada CPU Intel. Tetapi pergeseran demi byte dari seluruh register ( PSLLDQdll. berjalan di unit acak.)

Detail teknis lainnya yang dapat Anda bagikan mengenai kinerja CPU x86 dihargai

Dokumen microarch Agner Fog menggambarkan jalur pipa Intel dan AMD CPU secara cukup rinci untuk menentukan dengan tepat berapa banyak siklus yang harus diambil per iterasi, dan apakah hambatannya adalah throughput, rantai dependensi, atau pertikaian untuk satu port eksekusi. Lihat beberapa jawaban saya di StackOverflow, seperti ini atau ini .

Juga, http://www.realworldtech.com/haswell-cpu/ (dan serupa untuk desain sebelumnya) adalah bacaan yang menyenangkan jika Anda menyukai desain CPU.

Inilah daftar Anda, yang diurutkan untuk CPU Haswell, berdasarkan daftar tamu terbaik saya. Ini sebenarnya bukan cara yang berguna untuk memikirkan sesuatu untuk apa pun selain menyetel loop asm. Efek prediksi cache / cabang biasanya mendominasi, jadi tuliskan kode Anda untuk memiliki pola yang baik. Angka-angka sangat bergelombang, dan mencoba untuk menghitung latensi tinggi, bahkan jika throughput tidak menjadi masalah, atau untuk menghasilkan lebih banyak uops yang menyumbat pipa untuk hal-hal lain terjadi secara paralel. Esp. nomor cache / cabang sangat dibuat-buat. Latensi penting untuk dependensi yang dibawa loop, throughput penting ketika setiap iterasi independen.

TL: DR angka-angka ini dibuat berdasarkan apa yang saya bayangkan untuk kasus penggunaan "khas", sejauh pertukaran antara latensi, hambatan pelabuhan eksekusi, dan throughput front-end (atau kios untuk hal-hal seperti kehilangan cabang) ). Tolong jangan gunakan angka-angka ini untuk segala jenis analisis perf serius .

  • 0,5 hingga 1 Bitwise / Integer Addition / Subtraction /
    shift dan rotate (comp-time const count) /
    versi vektor dari semua ini (1 hingga 4 per siklus throughput, 1 siklus latensi)
  • 1 vektor min, maks, bandingkan-sama, bandingkan-lebih besar (untuk membuat topeng)
  • 1,5 vektor mengocok. Haswell dan yang lebih baru hanya memiliki satu port shuffle, dan bagi saya itu umum perlu banyak pengocokan jika Anda memerlukannya, jadi saya menimbangnya sedikit lebih tinggi untuk mendorong berpikir tentang menggunakan shuffles yang lebih sedikit. Mereka tidak gratis, khususnya. jika Anda memerlukan topeng kontrol pshufb dari memori.
  • 1,5 load / store (hit cache L1. Throughput lebih baik daripada latensi)
  • 1.75 Penggandaan Integer (3c latency / satu per 1c tput pada Intel, 4c lat pada AMD dan hanya satu per 2c tput). Konstanta kecil bahkan lebih murah menggunakan LEA dan / atau ADD / SUB / shift . Tapi tentu saja konstanta kompilasi waktu selalu baik, dan sering dapat mengoptimalkan ke hal-hal lain. (Dan kalikan dalam satu lingkaran sering kali dapat dikurangi dengan kekuatan oleh kompiler menjadi tmp += 7dalam satu lingkaran bukan tmp = i*7)
  • 1.75 beberapa shuffle vektor 256b (latensi tambahan pada insns yang dapat memindahkan data antara 128b lajur vektor AVX). (Atau 3 hingga 7 di Ryzen di mana lintasan persimpangan perlu lebih banyak uops)
  • 2 fp add / sub (dan versi vektor yang sama) (1 atau 2 per siklus throughtput, 3 hingga 5 siklus latensi). Bisa lambat jika Anda menghambat latensi, mis. Menjumlahkan array dengan hanya 1 sumvariabel. (Saya bisa menimbang ini dan fp mul serendah 1 atau setinggi 5 tergantung pada use-case).
  • 2 vektor fp mul atau FMA. (x * y + z semurah mul atau add jika Anda mengkompilasi dengan dukungan FMA diaktifkan).
  • 2 memasukkan / mengekstrak register tujuan umum ke dalam elemen vektor ( _mm_insert_epi8, dll.)
  • 2,25 vektor int mul (elemen 16-bit atau pmaddubsw melakukan 8 * 8 -> 16-bit). Lebih murah di Skylake, dengan throughput yang lebih baik daripada skalar mul
  • 2,25 shift / rotate by count variabel (latensi 2c, satu throughput 2c pada Intel, lebih cepat pada AMD atau dengan BMI2)
  • 2.5 Perbandingan tanpa bercabang ( y = x ? a : b, atau y = x >= 0) ( test / setccatau cmov)
  • 3 int-> konversi float
  • 3 Aliran Kontrol diprediksi sempurna (cabang diprediksi, panggilan, kembali).
  • 4 vektor int mul (elemen 32-bit) (2 uops, latensi 10c pada Haswell)
  • 4 divisi integer atau %dengan konstanta waktu kompilasi (non-power of 2).
  • 7 ops horizontal vektor (misalnya PHADDmenambahkan nilai dalam vektor)
  • 11 (vektor) Divisi FP (10-13c latensi, satu per 7c throughput atau lebih buruk). (Bisa murah jika jarang digunakan, tetapi throughputnya 6 sampai 40x lebih buruk daripada FP mul)
  • 13? Control Flow (cabang yang diprediksi buruk, mungkin 75% dapat diprediksi)
  • 13 int divisi ( ya benar , ini lebih lambat dari divisi FP, dan tidak bisa membuat vektor). (perhatikan bahwa kompiler dibagi dengan konstanta menggunakan mul / shift / add dengan konstanta ajaib , dan div / mod dengan kekuatan 2 sangat murah.)
  • 16 (vektor) FP sqrt
  • 25? memuat (hit cache L3). (toko cache-miss lebih murah daripada banyak.)
  • 50? Trigasi FP / exp / log. Jika Anda membutuhkan banyak exp / log dan tidak membutuhkan akurasi penuh, Anda dapat bertukar akurasi untuk kecepatan dengan polinomial yang lebih pendek dan / atau tabel. Anda juga dapat membuat SIMD vektor.
  • 50-80? cabang selalu -dispredicted, biaya 15-20 siklus
  • 200-400? memuat / menyimpan (cache miss)
  • 3000 ??? baca halaman dari file (hit cache disk OS) (membuat angka di sini)
  • 20000 ??? halaman baca disk (OS-cache miss, SSD cepat) (nomor yang benar-benar dibuat-buat)

Saya benar-benar mengada-ada berdasarkan dugaan . Jika ada sesuatu yang salah, itu karena saya sedang memikirkan use-case yang berbeda, atau kesalahan editing.

Biaya relatif dari hal-hal pada CPU AMD akan serupa, kecuali mereka memiliki shif integer yang lebih cepat ketika shift-count adalah variabel. CPU AMD Bulldozer-family tentu saja lebih lambat pada sebagian besar kode, karena berbagai alasan. (Ryzen cukup pandai dalam banyak hal).

Ingatlah bahwa benar-benar mustahil untuk merebus berbagai hal menjadi biaya satu dimensi . Selain cache-miss dan branch mispredicts, bottleneck dalam suatu blok kode bisa latensi, total throughput uop (frontend), atau throughput port tertentu (port eksekusi).

Operasi "lambat" seperti divisi FP bisa sangat murah jika kode di sekitarnya membuat CPU sibuk dengan pekerjaan lain . (vector FP div atau sqrt masing-masing 1 uop, mereka hanya memiliki latency dan throughput yang buruk. Mereka hanya memblokir unit divide, bukan seluruh port eksekusi yang ada di dalamnya. Integer div adalah beberapa uops.) Jadi jika Anda hanya memiliki satu FP divide untuk setiap ~ 20 mul dan tambahkan, dan ada pekerjaan lain yang harus dilakukan CPU (misal pengulangan loop independen), maka "biaya" dari FP FP bisa kira-kira sama dengan mul FP. Ini mungkin adalah contoh terbaik dari sesuatu yang throughput rendah ketika semua yang Anda lakukan, tetapi dicampur dengan sangat baik dengan kode lain (ketika latensi bukan faktor), karena total rendah uops.

Perhatikan bahwa pembagian integer hampir tidak ramah terhadap kode di sekitarnya: Di Haswell, 9 uops, dengan satu per 8-11c throughput, dan latensi 22-29c. (Pembagian 64bit jauh lebih lambat, bahkan pada Skylake.) Jadi angka latensi dan throughput agak mirip dengan FP div, tetapi FP div hanya satu uop.

Untuk contoh-contoh menganalisa urutan pendek dari lns untuk throughput, latency, dan total uops, lihat beberapa jawaban SO saya:

IDK jika orang lain menulis jawaban SO termasuk analisis semacam ini. Saya memiliki waktu yang jauh lebih mudah untuk menemukan sendiri, karena saya tahu saya sering membahas detail ini, dan saya dapat mengingat apa yang saya tulis.


"Cabang prediksi" pada 4 masuk akal - apa yang seharusnya "cabang prediksi" pada 20-25 benar-benar? (Saya berpikir bahwa cabang yang salah prediksi (terdaftar sekitar 13) jauh lebih mahal daripada itu, tetapi itulah sebabnya saya ada di halaman ini, untuk mempelajari sesuatu yang lebih dekat dengan kebenaran - terima kasih untuk meja yang hebat!)
Matt

@ Matt: Saya pikir itu adalah kesalahan pengeditan dan seharusnya "salah cabang". Terima kasih telah menunjukkannya. Perhatikan bahwa 13 adalah untuk cabang yang diprediksi tidak sempurna, bukan cabang yang selalu salah duga, jadi saya mengklarifikasi hal itu. Saya melakukan handwaving ulang dan mengedit. : P
Peter Cordes

16

Tergantung pada CPU yang dimaksud, tetapi untuk CPU modern daftarnya adalah seperti ini:

  1. Bitwise, penambahan, pengurangan, perbandingan, penggandaan
  2. Divisi
  3. Alur kontrol (lihat jawaban 3)

Tergantung pada CPU mungkin ada banyak tol untuk bekerja dengan tipe data 64 bit.

Pertanyaan Anda:

  1. Tidak sama sekali atau tidak cukup pada CPU modern. Tergantung pada CPU.
  2. Informasi itu kira-kira 20 hingga 30 tahun kedaluwarsa (School sucks, sekarang Anda sudah punya bukti), CPU modern menangani sejumlah variabel instruksi per jam, berapa banyak tergantung pada apa yang penjadwal datang dengan.
  3. Divisi sedikit lebih lambat daripada yang lain, aliran kontrol sangat cepat jika prediksi cabang benar, dan sangat lambat jika salah (sekitar 20 siklus, tergantung pada CPU). Hasilnya adalah bahwa banyak kode dibatasi terutama oleh aliran kontrol. Jangan lakukan dengan ifapa yang dapat Anda lakukan secara wajar dengan aritmatika.
  4. Tidak ada angka pasti untuk berapa banyak siklus yang diambil instruksi, tetapi kadang-kadang dua instruksi berbeda dapat bekerja sama, menempatkan mereka dalam konteks lain dan mungkin tidak, menjalankannya pada CPU yang berbeda dan Anda cenderung melihat hasil ke-3.
  5. Di atas kontrol aliran pembuang waktu besar lainnya adalah cache misses, setiap kali Anda mencoba membaca data yang tidak ada dalam cache CPU harus menunggu untuk diambil dari memori. Secara umum Anda harus mencoba untuk menangani potongan data di samping satu sama lain secara bersamaan daripada mengambil data dari semua tempat.

Dan akhirnya, jika Anda membuat game, jangan terlalu khawatir tentang semua ini, lebih baik berkonsentrasi untuk membuat game yang bagus daripada memotong siklus CPU.


Saya juga ingin menunjukkan bahwa FPU sangat cepat: terutama pada Intel - jadi fixed-point hanya benar-benar diperlukan jika Anda ingin hasil yang deterministik.
Jonathan Dickinson

2
Saya hanya akan lebih menekankan pada bagian terakhir - buat game yang bagus. 3. Membantu memiliki kode yang jelas - itulah sebabnya 3. hanya berlaku ketika Anda benar-benar mengukur masalah kinerja. Itu selalu mudah untuk mengubah seandainya itu menjadi sesuatu yang lebih baik jika diperlukan. Di sisi lain, 5. lebih rumit - saya pasti setuju bahwa itu adalah kasus di mana Anda benar-benar ingin berpikir dulu, karena biasanya berarti mengubah arsitektur.
Luaan

3

Saya membuat tes tentang operasi penyihir bilangan bulat satu juta kali pada x64_64, mencapai kesimpulan singkat seperti di bawah ini,

tambahkan --- 116 mikrodetik

sub ---- 116 mikrodetik

mul ---- 1036 mikrodetik

div ---- 13037 mikrodetik

data di atas telah mengurangi overhead yang disebabkan oleh loop,


2

Manual prosesor intel dapat diunduh gratis dari situs web mereka. Mereka cukup besar tetapi secara teknis dapat menjawab pertanyaan Anda. Manual optimasi secara khusus adalah apa yang Anda cari, tetapi manual instruksi juga memiliki timing dan latency untuk sebagian besar jalur CPU utama untuk instruksi simd karena mereka bervariasi dari chip ke chip.

Secara umum, saya akan mempertimbangkan cabang penuh serta pengejaran penunjuk (lintasan daftar tautan, memanggil fungsi virtual) yang terbaik untuk perf killers, tetapi CPU x86 / x64 sangat bagus di keduanya, dibandingkan dengan arsitektur lain. Jika Anda pernah port ke platform lain, Anda akan melihat seberapa besar masalah mereka, jika Anda menulis kode kinerja tinggi.


+1, beban dependen (pengejaran pointer) adalah masalah besar. Kehilangan cache akan memblokir pemuatan di masa depan bahkan untuk memulai. Memiliki banyak beban dari memori utama dalam penerbangan sekaligus memberikan bandwidth yang jauh lebih baik daripada memiliki satu op membutuhkan sebelumnya untuk sepenuhnya selesai.
Peter Cordes
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.