Bacaan latar belakang penting: pdf microarch Agner Fog , dan mungkin juga Ulrich Drepper Apa Yang Harus Ketahui Setiap Programmer Tentang Memori oleh . Lihat juga tautan lain dix86beri tag wiki, terutama manual pengoptimalan Intel, dan analisis David Kanter tentang arsitektur mikro Haswell, dengan diagram .
Tugas yang sangat keren; jauh lebih baik daripada yang saya lihat di mana siswa diminta untuk mengoptimalkan beberapa kodegcc -O0
, mempelajari banyak trik yang tidak penting dalam kode nyata. Dalam hal ini, Anda diminta untuk belajar tentang pipa CPU dan menggunakannya untuk memandu upaya de-optimasi Anda, bukan hanya menebak-nebak. Bagian yang paling menyenangkan dari ini adalah membenarkan setiap pesimisasi dengan "ketidakmampuan jahat", bukan niat jahat.
Masalah dengan kata-kata dan kode tugas :
Opsi khusus uarch untuk kode ini terbatas. Itu tidak menggunakan array, dan sebagian besar biaya adalah panggilan ke exp
/ log
fungsi perpustakaan. Tidak ada cara yang jelas untuk memiliki paralelisme tingkat instruksi yang lebih banyak atau lebih sedikit, dan rantai ketergantungan yang digerakkan loop sangat pendek.
Saya ingin melihat jawaban yang berusaha untuk memperlambat dari mengatur ulang ekspresi untuk mengubah dependensi, untuk mengurangi ILP hanya dari dependensi (bahaya). Saya belum mencobanya.
Intel Sandybridge-family CPUs adalah desain out-of-order agresif yang menghabiskan banyak transistor dan daya untuk menemukan paralelisme dan menghindari bahaya (ketergantungan) yang akan menyulitkan pipa in-order RISC klasik . Biasanya satu-satunya bahaya tradisional yang memperlambatnya adalah dependensi "benar" RAW yang menyebabkan throughput dibatasi oleh latensi.
Bahaya WAR dan WAW untuk register tidak terlalu menjadi masalah, terima kasih untuk penggantian nama register . (kecuali untukpopcnt
/lzcnt
/tzcnt
, yang memiliki ketergantungan salah tujuan mereka pada CPU Intel , meskipun itu hanya untuk menulis. yaitu WAW ditangani sebagai bahaya RAW + tulisan). Untuk pemesanan memori, CPU modern menggunakan antrean toko untuk menunda komit ke dalam cache hingga pensiun, juga menghindari bahaya WAR dan WAW .
Mengapa mulss hanya mengambil 3 siklus di Haswell, berbeda dari tabel instruksi Agner? memiliki lebih banyak tentang register penggantian nama dan menyembunyikan latensi FMA dalam loop produk FP dot.
Nama merek "i7" diperkenalkan dengan Nehalem (penerus Core2) , dan beberapa manual Intel bahkan mengatakan "Core i7" ketika mereka tampaknya berarti Nehalem, tetapi mereka mempertahankan branding "i7" untuk Sandybridge dan kemudian mikroarsitektur. SnB adalah ketika keluarga P6 berevolusi menjadi spesies baru, keluarga SnB . Dalam banyak hal, Nehalem memiliki lebih banyak kesamaan dengan Pentium III daripada dengan Sandybridge (mis. Register baca kios dan kios baca-ROB tidak terjadi pada SnB, karena itu berubah menjadi menggunakan file register fisik. Juga cache uop dan internal yang berbeda format uop). Istilah "arsitektur i7" tidak berguna, karena tidak masuk akal mengelompokkan keluarga SnB dengan Nehalem tetapi tidak dengan Core2. (Nehalem memang memperkenalkan arsitektur cache L3 inklusif bersama untuk menghubungkan beberapa core secara bersamaan, dan juga GPU terintegrasi. Jadi level chip, penamaannya lebih masuk akal.)
Ringkasan ide-ide bagus yang bisa dibenarkan oleh ketidakmampuan jahat
Bahkan tidak kompeten secara jahat tidak mungkin untuk menambahkan pekerjaan yang jelas tidak berguna atau loop tak terbatas, dan membuat kekacauan dengan kelas C ++ / Boost berada di luar ruang lingkup tugas.
- Multi-utas dengan penghitung loop bersama tunggal
std::atomic<uint64_t>
, sehingga jumlah total iterasi yang tepat terjadi. Atom uint64_t sangat buruk dengan -m32 -march=i586
. Untuk poin bonus, atur agar tidak sejajar, dan melewati batas halaman dengan pemisahan yang tidak rata (bukan 4: 4).
- Berbagi salah untuk beberapa variabel non-atom lainnya -> pipa mis-spekulasi memori-order hilang, serta kesalahan cache tambahan.
- Alih-alih menggunakan
-
pada variabel FP, XOR byte tinggi dengan 0x80 untuk membalik bit tanda, menyebabkan warung penerusan toko .
- Atur waktu setiap iterasi secara independen, dengan sesuatu yang bahkan lebih berat daripada
RDTSC
. misalnya CPUID
/ RDTSC
atau fungsi waktu yang membuat panggilan sistem. Serialisasi instruksi secara inheren pipa-tidak ramah.
- Ubah kelipatan dengan konstanta untuk membaginya dengan kebalikannya ("untuk kemudahan membaca"). div lambat dan tidak sepenuhnya pipelined.
- Membuat vektor multiply / sqrt dengan AVX (SIMD), tetapi gagal digunakan
vzeroupper
sebelum panggilan ke skalar matematika-perpustakaan exp()
dan log()
fungsi, menyebabkan AVX <-> SSE warung transisi .
- Simpan output RNG dalam daftar tertaut, atau dalam array yang Anda lewati tidak sesuai pesanan. Sama untuk hasil setiap iterasi, dan jumlah di akhir.
Juga tercakup dalam jawaban ini tetapi dikecualikan dari ringkasan: saran yang akan sama lambatnya pada CPU non-pipelined, atau yang tampaknya tidak dapat dibenarkan bahkan dengan ketidakmampuan jahat. misalnya banyak ide gimp-the-compiler yang menghasilkan jelas berbeda / lebih buruk asm.
Multi-utas buruk
Mungkin menggunakan OpenMP untuk loop multi-thread dengan iterasi yang sangat sedikit, dengan overhead yang jauh lebih tinggi daripada gain kecepatan. Kode monte-carlo Anda memiliki paralelisme yang cukup untuk benar-benar mendapatkan speedup, esp. jika kita berhasil membuat setiap iterasi lambat. (Setiap utas menghitung sebagian payoff_sum
, ditambahkan di akhir). #omp parallel
pada loop itu mungkin akan menjadi optimasi, bukan pesimisasi.
Multi-utas tetapi memaksa kedua utas untuk berbagi penghitung putaran yang sama (dengan atomic
penambahan sehingga jumlah total iterasi benar). Ini tampaknya masuk akal secara logis. Ini berarti menggunakan static
variabel sebagai penghitung lingkaran. Membenarkan ini penggunaan atomic
untuk counter lingkaran, dan menciptakan aktual cache-garis ping-ponging (asalkan benang tidak berjalan di inti fisik yang sama dengan HyperThreading; yang mungkin tidak seperti yang lambat). Bagaimanapun, ini jauh lebih lambat daripada kasus yang tidak diperebutkan lock inc
. Dan lock cmpxchg8b
untuk penambahan atom, sebuah uint64_t
sistem 32bit harus coba lagi dalam satu lingkaran daripada memiliki perangkat keras yang melakukan arbitrasi atom inc
.
Juga buat berbagi palsu , tempat banyak utas menyimpan data pribadi mereka (misalnya status RNG) dalam byte berbeda dari baris cache yang sama. (Tutorial Intel tentang hal itu, termasuk penghitung perf untuk dilihat) . Ada aspek mikroarsitektur spesifik untuk ini : Intel CPU berspekulasi tentang kesalahan memori pemesanan tidak terjadi, dan ada acara perf-order mesin-memori untuk mendeteksi ini, setidaknya pada P4 . Penalti mungkin tidak sebesar pada Haswell. Seperti yang ditunjukkan oleh tautan itu, lock
instruksi ed mengasumsikan ini akan terjadi, menghindari spekulasi yang salah. Muatan normal berspekulasi bahwa core lain tidak akan membatalkan garis cache antara ketika beban dieksekusi dan ketika itu pensiun dalam urutan program (kecuali Anda menggunakanpause
). Berbagi sejati tanpa lock
instruksi ed biasanya bug. Akan menarik untuk membandingkan penghitung loop bersama non-atomik dengan kasing atom. Untuk benar-benar pesimis, pertahankan penghitung loop atom yang dibagikan, dan menyebabkan berbagi salah dalam baris cache yang sama atau berbeda untuk beberapa variabel lain.
Gagasan khusus uarch-random:
Jika Anda dapat memperkenalkan cabang yang tidak dapat diprediksi , itu akan membuat pesimistis kode secara substansial. CPU x86 modern memiliki jaringan pipa yang cukup panjang, sehingga biaya salah duga ~ 15 siklus (saat berjalan dari cache uop).
Rantai ketergantungan:
Saya pikir ini adalah salah satu bagian tugas yang dimaksudkan.
Kalahkan kemampuan CPU untuk mengeksploitasi paralelisme tingkat instruksi dengan memilih urutan operasi yang memiliki satu rantai ketergantungan panjang alih-alih beberapa rantai ketergantungan pendek. Kompiler tidak diperbolehkan untuk mengubah urutan operasi untuk perhitungan FP kecuali Anda menggunakan -ffast-math
, karena itu dapat mengubah hasil (seperti dibahas di bawah).
Untuk benar-benar membuat ini efektif, tambah panjang rantai ketergantungan loop-carry. Tidak ada yang melompat begitu jelas, meskipun: loop seperti yang tertulis memiliki rantai ketergantungan loop-carry yang sangat pendek: hanya sebuah FP add. (3 siklus). Beberapa iterasi dapat membuat kalkulasinya dalam penerbangan sekaligus, karena mereka dapat memulai jauh sebelum payoff_sum +=
pada akhir iterasi sebelumnya. ( log()
dan exp
ikuti banyak instruksi, tetapi tidak lebih dari jendela Haswell yang tidak sesuai untuk menemukan paralelisme: ukuran ROB = 192 domain-gabungan uops, dan ukuran penjadwal = 60 domain-domain tidak digunakan. Segera setelah pelaksanaan iterasi saat ini berlangsung cukup jauh untuk memberikan ruang bagi instruksi dari iterasi berikutnya untuk diterbitkan, setiap bagian dari itu yang memiliki input siap (mis. Rantai dep / independen terpisah) dapat mulai dieksekusi ketika instruksi lama meninggalkan unit eksekusi gratis (misalnya karena mereka mengalami hambatan pada latensi, bukan throughput.).
Keadaan RNG hampir pasti akan menjadi rantai ketergantungan loop-carry yang lebih panjang daripada addps
.
Gunakan operasi FP yang lebih lambat / lebih banyak (khususnya divisi lebih banyak):
Bagilah dengan 2,0 alih-alih mengalikan dengan 0,5, dan seterusnya. Multiply FP banyak diselaraskan dalam desain Intel, dan memiliki satu throughput 0,5c pada Haswell dan yang lebih baru. FP divsd
/ divpd
hanya sebagian disalurkan melalui pipa . (Meskipun Skylake memiliki throughput yang mengesankan per 4c untuk divpd xmm
, dengan latensi 13-14c, vs tidak disalurkan sama sekali di Nehalem (7-22c)).
Itu do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);
jelas menguji jarak, jadi jelas itu akan sesuai untuk sqrt()
itu. : P ( sqrt
bahkan lebih lambat dari div
).
Seperti yang disarankan @Paul Clayton, menulis ulang ekspresi dengan asosiatif / distributif yang setara dapat memperkenalkan lebih banyak pekerjaan (selama Anda tidak menggunakan -ffast-math
untuk memungkinkan kompilator mengoptimalkan ulang). (exp(T*(r-0.5*v*v))
bisa menjadi exp(T*r - T*v*v/2.0)
. Perhatikan bahwa sementara matematika pada bilangan real adalah asosiatif, matematika floating point tidak , bahkan tanpa mempertimbangkan overflow / NaN (yang mengapa -ffast-math
tidak diaktifkan secara default). Lihat komentar Paul untukpow()
saran tersarang yang sangat berbulu .
Jika Anda dapat menurunkan skala perhitungan ke angka yang sangat kecil, maka operasi matematika FP mengambil ~ 120 siklus tambahan untuk menjebak ke kode mikro ketika operasi pada dua angka normal menghasilkan denormal . Lihat microarch pdf Agner Fog untuk angka dan detail yang tepat. Ini tidak mungkin karena Anda memiliki banyak perkalian, jadi faktor skala akan dikuadratkan dan dialirkan hingga 0,0. Saya tidak melihat cara untuk membenarkan penskalaan yang diperlukan dengan ketidakmampuan (bahkan jahat), hanya niat jahat yang disengaja.
Jika Anda dapat menggunakan intrinsik ( <immintrin.h>
)
Gunakan movnti
untuk mengusir data Anda dari cache . Diabolis: ini baru dan tidak teratur, sehingga seharusnya membiarkan CPU menjalankannya lebih cepat, bukan? Atau lihat pertanyaan terkait untuk kasus di mana seseorang dalam bahaya melakukan hal ini (untuk menulis tersebar di mana hanya beberapa lokasi yang panas). clflush
mungkin tidak mungkin tanpa kedengkian.
Gunakan shuffle integer antara operasi matematika FP untuk menyebabkan penundaan bypass.
Mencampur instruksi SSE dan AVX tanpa penggunaan yang tepat vzeroupper
menyebabkan warung besar di pra-Skylake (dan penalti berbeda di Skylake ). Bahkan tanpa itu, vektorisasi buruk dapat menjadi lebih buruk daripada skalar (lebih banyak siklus menghabiskan pengocokan data ke / dari vektor daripada disimpan dengan melakukan operasi add / sub / mul / div / sqrt untuk 4 iterasi Monte-Carlo sekaligus, dengan 256b vektor) . add / sub / mul unit pelaksanaan sepenuhnya pipelined dan lebar penuh, tetapi div dan sqrt pada 256b vektor tidak secepat pada 128b vektor (atau skalar), sehingga speedup tidak dramatis untukdouble
.
exp()
dan log()
tidak memiliki dukungan perangkat keras, sehingga bagian itu akan memerlukan mengekstraksi elemen vektor kembali ke skalar dan memanggil fungsi pustaka secara terpisah, kemudian mengocok hasilnya kembali menjadi vektor. libm biasanya dikompilasi untuk hanya menggunakan SSE2, jadi akan menggunakan encoding legacy-SSE dari instruksi matematika skalar. Jika kode Anda menggunakan 256b vektor dan panggilan exp
tanpa melakukan yang vzeroupper
pertama, maka Anda berhenti. Setelah kembali, instruksi AVX-128 ingin vmovsd
mengatur elemen vektor berikutnya sebagai argumen untuk exp
juga akan berhenti. Dan kemudian exp()
akan berhenti lagi ketika menjalankan instruksi SSE. Inilah yang terjadi dalam pertanyaan ini , menyebabkan penurunan 10x. (Terima kasih @ZBoson).
Lihat juga eksperimen Nathan Kurz dengan lib matematika vs glibc Intel untuk kode ini . Glibc masa depan akan datang dengan implementasi vektor exp()
dan sebagainya.
Jika menargetkan pre-IvB, atau esp. Nehalem, cobalah untuk membuat gcc menyebabkan warung register parsial dengan operasi 16bit atau 8bit diikuti oleh operasi 32bit atau 64bit. Dalam kebanyakan kasus, gcc akan digunakan movzx
setelah operasi 8 atau 16bit, tetapi inilah kasus di mana gcc memodifikasi ah
dan kemudian membacaax
Dengan (inline) asm:
Dengan (inline) asm, Anda dapat memecah cache uop: Potongan kode 32B yang tidak muat dalam tiga baris cache 6uop memaksa switch dari cache uop ke decoder. Ketidakmampuan ALIGN
menggunakan banyak byte tunggal nop
alih-alih pasangan panjang nop
pada target cabang di dalam lingkaran dalam mungkin melakukan trik. Atau letakkan bantalan pelurus setelah label, bukan sebelumnya. : P Ini hanya masalah jika frontend adalah bottleneck, yang tidak akan terjadi jika kita berhasil pesimisasi sisa kode.
Gunakan kode modifikasi sendiri untuk memicu pembersihan saluran (alias mesin-nuklir).
LCP warung dari instruksi 16bit dengan terlalu besar untuk muat dalam 8 bit sepertinya tidak akan berguna. Tembolok uop pada SnB dan yang lebih baru berarti Anda hanya membayar penalti decode sekali. Pada Nehalem (i7 pertama), ini mungkin bekerja untuk satu loop yang tidak sesuai dengan buffer loop 28 uop. gcc kadang-kadang akan menghasilkan instruksi seperti itu, bahkan dengan -mtune=intel
dan ketika itu bisa menggunakan instruksi 32bit.
Idiom umum untuk waktu adalah CPUID
(untuk bersambung)RDTSC
. Waktu setiap iterasi secara terpisah dengan CPUID
/ RDTSC
untuk memastikan RDTSC
tidak mengatur kembali dengan instruksi sebelumnya, yang akan memperlambat segalanya a banyak . (Dalam kehidupan nyata, cara cerdas untuk mengatur waktu adalah mengatur waktu semua iterasi bersama, alih-alih menentukan waktu masing-masing secara terpisah dan menambahkannya).
Menyebabkan banyak kesalahan cache dan perlambatan memori lainnya
Gunakan a union { double d; char a[8]; }
untuk beberapa variabel Anda. Menyebabkan warung penerusan toko dengan melakukan penyempitan sempit (atau Baca-Ubah-Tulis) hanya ke salah satu dari byte. (Artikel wiki itu juga membahas banyak hal mikroarsitektur lainnya untuk memuat / menyimpan antrian). misalnya membalikkan tanda double
menggunakan XOR 0x80 hanya pada byte tinggi , bukan -
operator. Pengembang yang tidak kompeten secara jahat mungkin pernah mendengar bahwa FP lebih lambat dari integer, dan dengan demikian mencoba untuk melakukan sebanyak mungkin menggunakan operasi integer. (Kompilator penargetan matematika FP yang sangat bagus dalam register SSE dapat mengkompilasi ini kexorps
dengan konstanta dalam register xmm lain, tetapi satu-satunya cara ini tidak buruk untuk x87 adalah jika kompiler menyadari bahwa itu meniadakan nilai dan mengganti add berikutnya dengan subtract.)
Gunakan volatile
jika Anda mengkompilasi dengan -O3
dan tidak menggunakan std::atomic
, untuk memaksa kompiler untuk benar-benar menyimpan / memuat ulang di semua tempat. Variabel global (bukan lokal) juga akan memaksa beberapa toko / memuat ulang, tetapi lemahnya pemesanan model memori C ++ tidak mengharuskan kompiler untuk menumpahkan / memuat ulang ke memori sepanjang waktu.
Ganti vars lokal dengan anggota struct besar, sehingga Anda dapat mengontrol tata letak memori.
Gunakan array dalam struct untuk padding (dan menyimpan angka acak, untuk membenarkan keberadaan mereka).
Pilih tata letak memori Anda sehingga semuanya masuk ke jalur berbeda di "set" yang sama di cache L1 . Ini hanya asosiatif 8 arah, yaitu setiap set memiliki 8 "cara". Garis cache adalah 64B.
Bahkan lebih baik, pisahkan 4096B dengan tepat, karena banyak yang memiliki ketergantungan salah pada toko ke halaman yang berbeda tetapi dengan offset yang sama dalam satu halaman . CPU out-of-order yang agresif menggunakan Memory Disambiguation untuk mencari tahu kapan memuat dan menyimpan dapat disusun ulang tanpa mengubah hasilnya , dan implementasi Intel memiliki false-positive yang mencegah beban memulai lebih awal. Mungkin mereka hanya memeriksa bit di bawah halaman offset, sehingga pemeriksaan dapat dimulai sebelum TLB menerjemahkan bit tinggi dari halaman virtual ke halaman fisik. Selain panduan Agner, lihat jawaban dari Stephen Canon , dan juga bagian di dekat akhir jawaban @Krazy Glew pada pertanyaan yang sama. (Andy Glew adalah salah satu arsitek mikroarsitektur P6 asli Intel.)
Gunakan __attribute__((packed))
untuk membiarkan Anda meluruskan variabel sehingga mereka menjangkau garis cache atau bahkan batas halaman. (Jadi satu beban double
membutuhkan data dari dua baris cache). Load yang tidak selaras tidak memiliki penalti dalam Intel i7 uarch apa pun, kecuali saat melintasi garis cache dan baris halaman. Perpecahan Cache-line masih membutuhkan siklus tambahan . Skylake secara dramatis mengurangi penalti untuk beban pemisah halaman, dari 100 hingga 5 siklus. (Bagian 2.1.3) . Mungkin terkait dengan bisa melakukan dua halaman berjalan secara paralel.
Pemisahan halaman pada atomic<uint64_t>
harus tentang kasus terburuk , esp. jika 5 byte dalam satu halaman dan 3 byte di halaman lain, atau apa pun selain 4: 4. Bahkan membagi di tengah lebih efisien untuk pemisahan cache-line dengan 16B vektor pada beberapa uarches, IIRC. Masukkan semuanya ke dalam alignas(4096) struct __attribute((packed))
(untuk menghemat ruang, tentu saja), termasuk array untuk penyimpanan untuk hasil RNG. Mencapai misalignment dengan menggunakan uint8_t
atau uint16_t
untuk sesuatu sebelum konter.
Jika Anda bisa membuat kompiler menggunakan mode pengalamatan terindeks, itu akan mengalahkan uop micro-fusion . Mungkin dengan menggunakan #define
s untuk mengganti variabel skalar sederhana dengan my_data[constant]
.
Jika Anda dapat memperkenalkan tingkat tipuan ekstra, jadi muat / simpan alamat tidak diketahui lebih awal, yang dapat menjadi pesimis lebih lanjut.
Array melintang dalam urutan yang tidak berdampingan
Saya pikir kita bisa datang dengan pembenaran tidak kompeten untuk memperkenalkan array di tempat pertama: Ini memungkinkan kita memisahkan generasi nomor acak dari penggunaan nomor acak. Hasil dari setiap iterasi juga dapat disimpan dalam sebuah array, untuk kemudian dijumlahkan (dengan ketidakmampuan lebih jahat).
Untuk "keacakan maksimum", kita dapat memiliki perulangan utas di atas array acak yang menuliskan angka acak baru ke dalamnya. Thread yang menggunakan angka acak dapat menghasilkan indeks acak untuk memuat nomor acak. (Ada beberapa perbaikan di sini, tetapi secara mikroarsitektur ini membantu untuk memuat-alamat agar diketahui lebih awal sehingga setiap latensi pemuatan yang mungkin dapat diselesaikan sebelum data yang dimuat diperlukan.) Memiliki pembaca dan penulis pada inti yang berbeda akan menyebabkan kesalahan pemesanan memori -spesifikasi menghapus saluran pipa (seperti yang dibahas sebelumnya untuk kasus berbagi-salah).
Untuk pesimisasi maksimum, lewati array Anda dengan langkah 4096 byte (yaitu 512 ganda). misalnya
for (int i=0 ; i<512; i++)
for (int j=i ; j<UPPER_BOUND ; j+=512)
monte_carlo_step(rng_array[j]);
Jadi pola aksesnya adalah 0, 4096, 8192, ...,
8, 4104, 8200, ...
16, 4112, 8208, ...
Ini adalah apa yang Anda dapatkan untuk mengakses array 2D seperti double rng_array[MAX_ROWS][512]
dalam urutan yang salah (pengulangan baris, alih-alih kolom dalam satu baris dalam pengulangan, seperti yang disarankan oleh @JesperJuhl). Jika ketidakmampuan jahat dapat membenarkan array 2D dengan dimensi seperti itu, ketidakmampuan taman dunia nyata dengan mudah membenarkan perulangan dengan pola akses yang salah. Ini terjadi dalam kode nyata dalam kehidupan nyata.
Sesuaikan batas loop jika perlu untuk menggunakan banyak halaman berbeda daripada menggunakan kembali beberapa halaman yang sama, jika array tidak terlalu besar. Pengambilan ulang perangkat keras tidak berfungsi (juga / sama sekali) di seluruh halaman. Prefetcher dapat melacak satu stream maju dan mundur dalam setiap halaman (yang terjadi di sini), tetapi hanya akan bertindak jika bandwidth memori belum jenuh dengan non-prefetch.
Ini juga akan menghasilkan banyak kesalahan TLB, kecuali jika halaman digabung ke dalam hugepage ( Linux melakukan ini secara oportunistik untuk alokasi anonim (tidak didukung file) seperti malloc
/ new
yang menggunakanmmap(MAP_ANONYMOUS)
).
Alih-alih array untuk menyimpan daftar hasil, Anda bisa menggunakan daftar tertaut . Maka setiap iterasi akan membutuhkan pointer-chasing load (bahaya ketergantungan benar RAW untuk alamat-beban dari beban berikutnya). Dengan pengalokasi yang buruk, Anda mungkin dapat menyebarkan daftar node dalam memori, mengalahkan cache. Dengan pengalokasi tidak kompeten yang jahat, itu bisa menempatkan setiap node di awal halamannya sendiri. (misalnya mengalokasikan dengan mmap(MAP_ANONYMOUS)
langsung, tanpa memecah halaman atau melacak ukuran objek untuk mendukung dengan benar free
).
Ini bukan mikroarsitektur spesifik, dan tidak ada hubungannya dengan pipeline (sebagian besar ini juga akan menjadi perlambatan pada CPU non-pipelined).
Agak di luar topik: membuat kompiler menghasilkan kode lebih buruk / melakukan lebih banyak pekerjaan:
Gunakan C ++ 11 std::atomic<int>
dan std::atomic<double>
untuk kode yang paling pesimis. Instruksi MFENCE dan lock
ed cukup lambat bahkan tanpa pertentangan dari utas lainnya.
-m32
akan membuat kode lebih lambat, karena kode x87 akan lebih buruk daripada kode SSE2. Konvensi panggilan 32-bit berbasis stack mengambil lebih banyak instruksi, dan meneruskan bahkan argumen FP pada stack ke fungsi-fungsi seperti exp()
. atomic<uint64_t>::operator++
pada -m32
membutuhkan lock cmpxchg8B
loop (i586). (Jadi gunakan itu untuk loop counter! [Evil laugh]).
-march=i386
juga akan pesimis (terima kasih @Jesper). FP dibandingkan dengan fcom
lebih lambat dari 686 fcomi
. Pra-586 tidak menyediakan penyimpanan atom 64bit, (apalagi cmpxchg), jadi semua atomic
operasi 64bit mengkompilasi panggilan fungsi libgcc (yang mungkin dikompilasi untuk i686, daripada benar-benar menggunakan kunci). Cobalah di tautan Penjelajah Kompresor Godbolt di paragraf terakhir.
Gunakan long double
/ sqrtl
/ expl
untuk presisi ekstra dan kelambatan ekstra dalam ABI di mana sizeof ( long double
) adalah 10 atau 16 (dengan padding untuk penyelarasan). (IIRC, 64bit Windows menggunakan 8byte long double
setara dengan double
. (Pokoknya, memuat / menyimpan 10byte (80bit) operan FP adalah 4/7 uops, vs. float
atau double
hanya mengambil 1 uop untuk fld m64/m32
/ fst
). Memaksa x87 dengan long double
kekalahan auto-vektorisasi bahkan untuk gcc -m64 -march=haswell -O3
.
Jika tidak menggunakan atomic<uint64_t>
penghitung lingkaran, gunakan long double
untuk semuanya, termasuk penghitung putaran.
atomic<double>
kompilasi, tetapi operasi baca-modifikasi-tulis seperti +=
tidak didukung untuk itu (bahkan pada 64bit). atomic<long double>
harus memanggil fungsi perpustakaan hanya untuk memuat / menyimpan atom. Ini mungkin sangat tidak efisien, karena x86 ISA tidak secara alami mendukung muatan / penyimpanan atom 10byte , dan satu-satunya cara yang dapat saya pikirkan tanpa mengunci ( cmpxchg16b
) memerlukan mode 64bit.
Pada -O0
, memecah ekspresi besar dengan menetapkan bagian ke vars sementara akan menyebabkan lebih banyak store / reload. Tanpa volatile
atau sesuatu, ini tidak masalah dengan pengaturan optimisasi yang akan digunakan oleh kode nyata.
Aturan aliasing memungkinkan a char
untuk alias apa pun, jadi menyimpan melalui suatu char*
memaksa kompiler untuk menyimpan / memuat kembali semuanya sebelum / sesudah byte-store, bahkan pada -O3
. (Ini adalah masalah untuk kodeuint8_t
auto-vektorisasi yang beroperasi pada array , misalnya.)
Coba uint16_t
penghitung putaran, untuk memaksa pemotongan ke 16bit, mungkin dengan menggunakan ukuran operan 16bit (kios potensial) dan / atau movzx
instruksi tambahan (aman). Signed overflow adalah perilaku yang tidak terdefinisi , jadi kecuali jika Anda menggunakan -fwrapv
atau setidaknya -fno-strict-overflow
, counter loop yang ditandatangani tidak harus diperpanjang lagi setiap iterasi , bahkan jika digunakan sebagai offset ke pointer 64bit.
Paksa konversi dari integer ke float
dan kembali lagi. Dan / atau double
<=> float
konversi. Instruksi memiliki latensi lebih dari satu, dan skalar int-> float ( cvtsi2ss
) dirancang dengan buruk untuk tidak nol sisa register xmm. (gcc menyisipkan tambahan pxor
untuk memutus ketergantungan, untuk alasan ini.)
Sering atur afinitas CPU Anda ke CPU yang berbeda (disarankan oleh @Egwor). alasan jahat: Anda tidak ingin satu inti terlalu panas dari menjalankan utas Anda untuk waktu yang lama, bukan? Mungkin bertukar ke inti lain akan membiarkan turbo inti ke kecepatan clock yang lebih tinggi. (Pada kenyataannya: mereka sangat dekat satu sama lain sehingga ini sangat tidak mungkin kecuali dalam sistem multi-socket). Sekarang hanya salah tala dan melakukannya terlalu sering. Selain waktu yang dihabiskan dalam keadaan thread penyimpanan / pemulihan OS, inti baru memiliki cache L2 / L1 dingin, cache uop, dan prediktor cabang.
Memperkenalkan panggilan sistem yang tidak perlu sering dapat memperlambat Anda, apa pun itu. Meskipun beberapa yang penting tetapi sederhana seperti gettimeofday
dapat diimplementasikan di ruang pengguna dengan, tanpa transisi ke mode kernel. (glibc di Linux melakukan ini dengan bantuan kernel, karena kernel mengekspor kode di vdso
).
Untuk lebih lanjut tentang overhead panggilan sistem (termasuk kesalahan cache / TLB setelah kembali ke ruang pengguna, bukan hanya konteksnya sendiri), kertas FlexSC memiliki beberapa analisis perf-counter yang hebat tentang situasi saat ini, serta proposal untuk sistem batching panggilan dari proses server multi-utas secara masif.
while(true){}