Saya telah memeras otak saya selama seminggu mencoba menyelesaikan tugas ini dan saya berharap seseorang di sini dapat menuntun saya ke jalan yang benar. Biarkan saya mulai dengan instruksi instruktur:
Tugas Anda adalah kebalikan dari tugas lab pertama kami, yaitu untuk mengoptimalkan program bilangan prima. Tujuan Anda dalam penugasan ini adalah untuk pesimis program, yaitu membuatnya berjalan lebih lambat. Keduanya adalah program intensif CPU. Mereka membutuhkan beberapa detik untuk berjalan di PC lab kami. Anda tidak boleh mengubah algoritme.
Untuk menonaktifkan program, gunakan pengetahuan Anda tentang cara kerja pipa Intel i7. Bayangkan cara untuk memesan kembali jalur instruksi untuk memperkenalkan WAR, RAW, dan bahaya lainnya. Pikirkan cara-cara untuk meminimalkan efektivitas cache. Tidak kompeten secara iblis.
Tugas itu memberi pilihan program Whetstone atau Monte-Carlo. Komentar efektivitas cache sebagian besar hanya berlaku untuk Whetstone, tetapi saya memilih program simulasi Monte-Carlo:
// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm> // Needed for the "max" function
#include <cmath>
#include <iostream>
// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
double x = 0.0;
double y = 0.0;
double euclid_sq = 0.0;
// Continue generating two uniform random variables
// until the square of their "euclidean distance"
// is less than unity
do {
x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
euclid_sq = x*x + y*y;
} while (euclid_sq >= 1.0);
return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}
// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
double S_adjust = S * exp(T*(r-0.5*v*v));
double S_cur = 0.0;
double payoff_sum = 0.0;
for (int i=0; i<num_sims; i++) {
double gauss_bm = gaussian_box_muller();
S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
payoff_sum += std::max(S_cur - K, 0.0);
}
return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}
// Pricing a European vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
double S_adjust = S * exp(T*(r-0.5*v*v));
double S_cur = 0.0;
double payoff_sum = 0.0;
for (int i=0; i<num_sims; i++) {
double gauss_bm = gaussian_box_muller();
S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
payoff_sum += std::max(K - S_cur, 0.0);
}
return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}
int main(int argc, char **argv) {
// First we create the parameter list
int num_sims = 10000000; // Number of simulated asset paths
double S = 100.0; // Option price
double K = 100.0; // Strike price
double r = 0.05; // Risk-free rate (5%)
double v = 0.2; // Volatility of the underlying (20%)
double T = 1.0; // One year until expiry
// Then we calculate the call/put values via Monte Carlo
double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
double put = monte_carlo_put_price(num_sims, S, K, r, v, T);
// Finally we output the parameters and prices
std::cout << "Number of Paths: " << num_sims << std::endl;
std::cout << "Underlying: " << S << std::endl;
std::cout << "Strike: " << K << std::endl;
std::cout << "Risk-Free Rate: " << r << std::endl;
std::cout << "Volatility: " << v << std::endl;
std::cout << "Maturity: " << T << std::endl;
std::cout << "Call Price: " << call << std::endl;
std::cout << "Put Price: " << put << std::endl;
return 0;
}
Perubahan yang saya buat tampaknya meningkatkan waktu berjalan kode satu detik, tetapi saya tidak sepenuhnya yakin apa yang bisa saya ubah untuk menghentikan pipa tanpa menambahkan kode. Suatu titik ke arah yang benar akan luar biasa, saya menghargai setiap tanggapan.
Pembaruan: profesor yang memberi tugas ini memposting beberapa detail
Highlights adalah:
- Ini adalah kelas arsitektur semester kedua di community college (menggunakan buku teks Hennessy dan Patterson).
- komputer lab memiliki CPU Haswell
- Para siswa telah terkena
CPUID
instruksi dan cara menentukan ukuran cache, serta intrinsik danCLFLUSH
instruksi. - setiap opsi kompiler diizinkan, dan begitu juga asm.
- Menulis algoritma akar kuadrat Anda sendiri diumumkan sebagai berada di luar batas
Komentar Cowmoogun tentang meta thread menunjukkan bahwa tidak jelas optimisasi kompiler dapat menjadi bagian dari ini, dan diasumsikan-O0
, dan bahwa 17% peningkatan run-time adalah wajar.
Jadi sepertinya tujuan dari tugas ini adalah untuk membuat siswa memesan ulang pekerjaan yang ada untuk mengurangi paralelisme tingkat pengajaran atau hal-hal seperti itu, tetapi bukan hal yang buruk bahwa orang telah menggali lebih dalam dan belajar lebih banyak.
Ingatlah bahwa ini adalah pertanyaan arsitektur komputer, bukan pertanyaan tentang cara membuat C ++ lambat secara umum.
sumber
while(true){}
Jawaban:
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 kode
gcc -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 untuk
popcnt
/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.
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).-
pada variabel FP, XOR byte tinggi dengan 0x80 untuk membalik bit tanda, menyebabkan warung penerusan toko .RDTSC
. misalnyaCPUID
/RDTSC
atau fungsi waktu yang membuat panggilan sistem. Serialisasi instruksi secara inheren pipa-tidak ramah.vzeroupper
sebelum panggilan ke skalar matematika-perpustakaanexp()
danlog()
fungsi, menyebabkan AVX <-> SSE warung transisi .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 menggunakanstatic
variabel sebagai penghitung lingkaran. Membenarkan ini penggunaanatomic
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 diperebutkanlock inc
. Danlock cmpxchg8b
untuk penambahan atom, sebuahuint64_t
sistem 32bit harus coba lagi dalam satu lingkaran daripada memiliki perangkat keras yang melakukan arbitrasi atominc
.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 tanpalock
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()
danexp
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 untukdivpd 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 untuksqrt()
itu. : P (sqrt
bahkan lebih lambat daridiv
).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 menjadiexp(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()
danlog()
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 panggilanexp
tanpa melakukan yangvzeroupper
pertama, maka Anda berhenti. Setelah kembali, instruksi AVX-128 inginvmovsd
mengatur elemen vektor berikutnya sebagai argumen untukexp
juga akan berhenti. Dan kemudianexp()
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 memodifikasiah
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 tunggalnop
alih-alih pasangan panjangnop
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 denganCPUID
/RDTSC
untuk memastikanRDTSC
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 tandadouble
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 menggunakanstd::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 bebandouble
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 dalamalignas(4096) struct __attribute((packed))
(untuk menghemat ruang, tentu saja), termasuk array untuk penyimpanan untuk hasil RNG. Mencapai misalignment dengan menggunakanuint8_t
atauuint16_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 denganmy_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
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 benarfree
).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>
danstd::atomic<double>
untuk kode yang paling pesimis. Instruksi MFENCE danlock
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 sepertiexp()
.atomic<uint64_t>::operator++
pada-m32
membutuhkanlock cmpxchg8B
loop (i586). (Jadi gunakan itu untuk loop counter! [Evil laugh]).-march=i386
juga akan pesimis (terima kasih @Jesper). FP dibandingkan denganfcom
lebih lambat dari 686fcomi
. Pra-586 tidak menyediakan penyimpanan atom 64bit, (apalagi cmpxchg), jadi semuaatomic
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 8bytelong double
setara dengandouble
. (Pokoknya, memuat / menyimpan 10byte (80bit) operan FP adalah 4/7 uops, vs.float
ataudouble
hanya mengambil 1 uop untukfld m64/m32
/fst
). Memaksa x87 denganlong double
kekalahan auto-vektorisasi bahkan untuk gcc-m64 -march=haswell -O3
.Jika tidak menggunakan
atomic<uint64_t>
penghitung lingkaran, gunakanlong 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. Tanpavolatile
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 suatuchar*
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 / ataumovzx
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 / ataudouble
<=>float
konversi. Instruksi memiliki latensi lebih dari satu, dan skalar int-> float (cvtsi2ss
) dirancang dengan buruk untuk tidak nol sisa register xmm. (gcc menyisipkan tambahanpxor
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 divdso
).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.
sumber
exp(T*(r-0.5*v*v))
menjadiexp(T*r - T*v*v/2.0)
;exp(sqrt(v*v*T)*gauss_bm)
menjadiexp(sqrt(v)*sqrt(v)*sqrt(T)*gauss_bm)
). Asosiatif (dan generalisasi) juga bisa berubahexp(T*r - T*v*v/2.0)
menjadi `pow ((pow (e_value, T), r) / pow (pow (pow (e_value, T), v), v)), - 2.0) [atau sesuatu seperti itu] Trik matematika semacam itu tidak benar-benar dianggap sebagai deoptimisasi mikroarsitektur.Beberapa hal yang dapat Anda lakukan untuk membuat segalanya berkinerja seburuk mungkin:
kompilasi kode untuk arsitektur i386. Ini akan mencegah penggunaan SSE dan instruksi yang lebih baru dan memaksa penggunaan FPU x87.
gunakan
std::atomic
variabel di mana-mana. Ini akan membuat mereka sangat mahal karena kompiler dipaksa untuk memasukkan penghalang memori di semua tempat. Dan ini adalah sesuatu yang mungkin dilakukan oleh orang yang tidak kompeten untuk "memastikan keamanan benang".pastikan untuk mengakses memori dengan cara terburuk yang dapat diprediksi oleh prefetcher (utama kolom vs utama baris).
untuk membuat variabel Anda lebih mahal, Anda bisa memastikan mereka semua memiliki 'durasi penyimpanan dinamis' (alokasi dialokasikan) dengan mengalokasikannya dengan
new
daripada membiarkan mereka memiliki 'durasi penyimpanan otomatis' (tumpukan dialokasikan).pastikan bahwa semua memori yang Anda alokasikan sangat aneh selaras dan tentu saja hindari mengalokasikan halaman besar, karena hal itu akan menjadi terlalu efisien TLB.
apa pun yang Anda lakukan, jangan buat kode Anda dengan pengoptimal kompiler diaktifkan. Dan pastikan untuk mengaktifkan simbol debug paling ekspresif yang Anda bisa (tidak akan membuat kode berjalan lebih lambat, tetapi itu akan membuang beberapa ruang disk tambahan).
Catatan: Jawaban ini pada dasarnya hanya merangkum komentar saya bahwa @Peter Cordes sudah memasukkan jawaban yang sangat bagus. Sarankan dia mendapatkan Anda upvote jika Anda hanya punya satu untuk cadangan :)
sumber
std::atomic
, atau tingkat tipuan tambahan dari alokasi dinamis. Mereka akan lambat pada Atom atau K8 juga. Masih upvoting, tapi itu sebabnya saya menolak beberapa saran Anda.movapd xmm, xmm
biasanya tidak memerlukan port eksekusi (ditangani pada tahap register-rename pada IVB dan yang lebih baru). Ini juga hampir tidak pernah diperlukan dalam kode AVX, karena semuanya kecuali FMA tidak merusak. Tapi cukup adil, Haswell menjalankannya pada port5 jika tidak dihilangkan. Saya belum melihat register-copy x87 (fld st(i)
), tetapi Anda tepat untuk Haswell / Broadwell: ini berjalan pada p01. Skylake menjalankannya di p05, SnB menjalankannya di p0, IvB menjalankannya di p5. Jadi IVB / SKL melakukan beberapa hal x87 (termasuk membandingkan) pada p5, tetapi SNB / HSW / BDW tidak menggunakan p5 sama sekali untuk x87.Anda dapat menggunakan
long double
untuk perhitungan. Pada x86 formatnya harus 80-bit. Hanya warisan, x87 FPU memiliki dukungan untuk ini.Beberapa kekurangan FPU x87:
sumber
fxch
). Dengan-ffast-math
, kompiler yang baik mungkin membuat vektor loop monte-carlo, meskipun, dan x87 akan mencegahnya.mulss
di p01, tetapifmul
hanya dip0
.addss
hanya berjalan padap1
, sama denganfadd
. Hanya ada dua port eksekusi yang menangani operasi matematika FP. (Satu-satunya pengecualian untuk ini adalah bahwa Skylake menjatuhkan add unit khusus dan berjalanaddss
di unit FMA pada p01, tetapifadd
pada p5. Jadi dengan mencampurkan beberapafadd
instruksi bersamafma...ps
, Anda secara teori dapat melakukan FLOP total yang lebih banyak / s.)long double
, artinya masih adildouble
. SysV ABI memang menggunakan 80bitlong double
. Juga, ulang: 2: pengubahan nama register memperlihatkan paralelisme dalam register tumpukan. Arsitektur berbasis stack memerlukan beberapa instruksi tambahan, sepertifxchg
, esp. saat interleaving perhitungan paralel. Jadi lebih sulit mengungkapkan paralelisme tanpa bolak-balik ingatan, daripada sulit bagi raja untuk mengeksploitasi apa yang ada. Anda tidak perlu lebih banyak konversi dari reg lain. Tidak yakin apa yang Anda maksud dengan itu.Jawaban telat tapi saya rasa kami tidak cukup menyalahgunakan daftar tertaut dan TLB.
Gunakan mmap untuk mengalokasikan node Anda, sehingga sebagian besar Anda menggunakan MSB dari alamat tersebut. Ini akan menghasilkan rantai pencarian TLB yang panjang, halaman 12 bit, menyisakan 52 bit untuk terjemahan, atau sekitar 5 level yang harus dilalui setiap kali. Dengan sedikit keberuntungan mereka harus pergi ke memori setiap kali untuk pencarian 5 level plus 1 akses memori untuk sampai ke node Anda, level teratas kemungkinan besar akan berada di cache di suatu tempat, sehingga kita dapat berharap untuk akses memori 5 *. Tempatkan simpul sehingga melangkah perbatasan terburuk sehingga membaca pointer berikutnya akan menyebabkan pencarian terjemahan 3-4 lainnya. Ini juga bisa menghancurkan cache karena jumlah pencarian terjemahan yang sangat besar. Juga ukuran tabel virtual mungkin menyebabkan sebagian besar data pengguna menjadi paging ke disk untuk waktu tambahan.
Saat membaca dari daftar tertaut tunggal, pastikan untuk membaca dari awal daftar setiap kali menyebabkan keterlambatan maksimum dalam membaca satu angka.
sumber
mmap
file atau wilayah memori bersama untuk mendapatkan beberapa alamat virtual untuk halaman fisik yang sama (dengan konten yang sama), memungkinkan lebih banyak TLB kehilangan jumlah RAM fisik yang sama. Jika daftar tertaut Andanext
hanya offset relatif , Anda bisa memiliki serangkaian pemetaan halaman yang sama dengan+4096 * 1024
sampai Anda akhirnya mendapatkan halaman fisik yang berbeda. Atau tentu saja membentang beberapa halaman untuk menghindari hit cache L1d. Ada caching PDE tingkat tinggi dalam perangkat keras berjalan-halaman, jadi ya sebarkan di ruang tambahan![reg+small_offset]
mode pengalamatan] ( Apakah ada penalti ketika base + offset berada di halaman yang berbeda dari basis? ); Anda akan mendapatkan sumber memoriadd
offset 64-bit, atau Anda akan mendapatkan beban dan mode pengalamatan yang diindeks seperti[reg+reg]
. Juga lihat Apa yang terjadi setelah miss L2 TLB? - page walk menjemput L1d cache di SnB-family.