Bertahun-tahun yang lalu, compiler C tidak terlalu pintar. Sebagai solusinya, K&R menemukan kata kunci register , untuk memberi petunjuk kepada kompiler, bahwa mungkin merupakan ide yang baik untuk menyimpan variabel ini dalam register internal. Mereka juga menjadikan operator tersier untuk membantu menghasilkan kode yang lebih baik.
Seiring waktu berlalu, kompiler semakin matang. Mereka menjadi sangat cerdas karena analisis aliran memungkinkan mereka membuat keputusan yang lebih baik tentang nilai apa yang harus dipegang dalam register daripada yang dapat Anda lakukan. Kata kunci register menjadi tidak penting.
FORTRAN bisa lebih cepat dari C untuk beberapa jenis operasi, karena masalah alias . Dalam teori dengan pengkodean yang cermat, seseorang dapat mengatasi batasan ini untuk memungkinkan pengoptimal menghasilkan kode yang lebih cepat.
Praktik pengkodean apa yang tersedia yang memungkinkan kompilator / pengoptimal menghasilkan kode lebih cepat?
- Mengidentifikasi platform dan kompiler yang Anda gunakan, akan sangat dihargai.
- Mengapa teknik ini tampaknya berhasil?
- Kode sampel dianjurkan.
[Sunting] Pertanyaan ini bukan tentang keseluruhan proses untuk membuat profil, dan mengoptimalkan. Asumsikan bahwa program telah ditulis dengan benar, dikompilasi dengan optimalisasi penuh, diuji dan dimasukkan ke dalam produksi. Mungkin ada konstruksi dalam kode Anda yang melarang pengoptimal melakukan tugas terbaiknya. Apa yang dapat Anda lakukan untuk refactor yang akan menghapus larangan ini, dan memungkinkan pengoptimal menghasilkan kode yang lebih cepat?
[Sunting] Tautan terkait offset
sumber
register
sebenarnya membuat kode yang peka terhadap kinerja lebih portabel dengan memerangi kompiler yang buruk.Jawaban:
Tulis ke variabel lokal dan bukan argumen keluaran! Ini bisa sangat membantu untuk mengatasi pelambatan aliasing. Misalnya, jika kode Anda terlihat seperti
kompilator tidak mengetahui bahwa foo1! = barOut, dan karenanya harus memuat ulang foo1 setiap kali melalui loop. Ia juga tidak bisa membaca foo2 [i] sampai tulis ke barOut selesai. Anda bisa mulai mengotak-atik petunjuk terbatas, tetapi itu sama efektifnya (dan jauh lebih jelas) untuk melakukan ini:
Kedengarannya konyol, tetapi kompilator bisa lebih pintar menangani variabel lokal, karena tidak mungkin tumpang tindih dalam memori dengan argumen mana pun. Ini dapat membantu Anda menghindari penyimpanan-hit-penyimpanan yang ditakuti (disebutkan oleh Francis Boivin di utas ini).
sumber
Berikut adalah praktik pengkodean untuk membantu kompilator membuat kode dengan cepat — bahasa apa pun, platform apa pun, kompiler apa pun, masalah apa pun:
Jangan tidak menggunakan trik pintar yang berlaku, atau bahkan mendorong, compiler untuk meletakkan variabel dalam memori (termasuk cache dan register) seperti yang Anda pikirkan terbaik. Pertama, tulis program yang benar dan dapat dipelihara.
Selanjutnya, buat profil kode Anda.
Kemudian, dan hanya setelah itu, Anda mungkin ingin mulai menyelidiki efek memberi tahu kompilator cara menggunakan memori. Buat 1 perubahan pada satu waktu dan ukur dampaknya.
Berharap untuk kecewa dan harus bekerja sangat keras untuk peningkatan kinerja kecil. Kompiler modern untuk bahasa dewasa seperti Fortran dan C sangat, sangat bagus. Jika Anda membaca akun dari 'trik' untuk mendapatkan kinerja yang lebih baik dari kode, ingatlah bahwa penulis kompiler juga telah membacanya dan, jika itu layak dilakukan, mungkin mengimplementasikannya. Mereka mungkin menulis apa yang Anda baca di tempat pertama.
sumber
&
vs.%
untuk kekuatan dua (jarang, jika pernah, dioptimalkan, tetapi dapat memiliki dampak kinerja yang signifikan). Jika Anda membaca trik untuk kinerja, satu-satunya cara untuk mengetahui apakah itu berhasil adalah dengan membuat perubahan dan mengukur dampaknya. Jangan pernah berasumsi bahwa kompilator akan mengoptimalkan sesuatu untuk Anda.n
, gcc menggantikan% n
dengan& (n-1)
bahkan saat pengoptimalan dinonaktifkan . Itu tidak persis "jarang, jika pernah" ...Urutan yang Anda lintasi memori dapat berdampak besar pada kinerja dan penyusun tidak terlalu pandai mencari tahu dan memperbaikinya. Anda harus berhati-hati dengan masalah lokalitas cache saat menulis kode jika Anda peduli dengan kinerja. Misalnya array dua dimensi dalam C dialokasikan dalam format baris-mayor. Melintasi array dalam format utama kolom akan cenderung membuat Anda memiliki lebih banyak cache yang terlewat dan membuat program Anda lebih terikat memori daripada yang terikat prosesor:
sumber
-floop-interchange
yang akan membalik loop dalam dan luar jika pengoptimal menganggapnya menguntungkan.Optimasi Generik
Di sini sebagai beberapa pengoptimalan favorit saya. Saya sebenarnya telah meningkatkan waktu eksekusi dan mengurangi ukuran program dengan menggunakan ini.
Deklarasikan fungsi kecil sebagai
inline
atau makroSetiap panggilan ke suatu fungsi (atau metode) menimbulkan overhead, seperti mendorong variabel ke tumpukan. Beberapa fungsi juga dapat menimbulkan biaya tambahan. Fungsi atau metode yang tidak efisien memiliki lebih sedikit pernyataan dalam isinya daripada overhead gabungan. Ini adalah kandidat yang baik untuk penyebarisan, baik sebagai
#define
makro atauinline
fungsi. (Ya, saya tahuinline
ini hanya saran, tetapi dalam hal ini saya menganggapnya sebagai pengingat bagi kompiler.)Hapus kode yang mati dan berlebihan
Jika kode tidak digunakan atau tidak berkontribusi pada hasil program, singkirkan.
Sederhanakan desain algoritme
Saya pernah menghapus banyak kode assembly dan waktu eksekusi dari program dengan menuliskan persamaan aljabar yang dihitungnya dan kemudian menyederhanakan ekspresi aljabar. Implementasi ekspresi aljabar yang disederhanakan memakan lebih sedikit ruang dan waktu daripada fungsi aslinya.
Ulangi Membuka gulungan
Setiap loop memiliki overhead pemeriksaan incrementing dan terminasi. Untuk mendapatkan perkiraan faktor kinerja, hitung jumlah instruksi di overhead (minimal 3: kenaikan, periksa, goto start of loop) dan bagi dengan jumlah pernyataan di dalam loop. Semakin rendah angkanya semakin baik.
Edit: berikan contoh loop unrolling Before:
Setelah membuka gulungan:
Dalam keuntungan ini, keuntungan kedua diperoleh: lebih banyak pernyataan dieksekusi sebelum prosesor harus memuat ulang cache instruksi.
Saya mendapatkan hasil yang luar biasa ketika saya membuka loop ke 32 pernyataan. Ini adalah salah satu hambatan karena program harus menghitung checksum pada file 2GB. Pengoptimalan ini dikombinasikan dengan pembacaan blok meningkatkan kinerja dari 1 jam menjadi 5 menit. Loop unrolling memberikan kinerja yang sangat baik dalam bahasa assembly juga, my
memcpy
jauh lebih cepat daripada compilermemcpy
. - TMPengurangan
if
pernyataanProsesor membenci cabang, atau lompatan, karena memaksa prosesor untuk memuat ulang antrian instruksinya.
Aritmatika Boolean ( Diedit: format kode yang diterapkan ke fragmen kode, contoh tambahan)
Ubah
if
pernyataan menjadi tugas boolean. Beberapa prosesor dapat menjalankan instruksi secara kondisional tanpa bercabang:The arus pendek dari Logical AND operator (
&&
) mencegah pelaksanaan tes jikastatus
inifalse
.Contoh:
Faktor Alokasi Variabel di luar loop
Jika variabel dibuat dengan cepat di dalam loop, pindahkan pembuatan / alokasi ke before loop. Dalam kebanyakan kasus, variabel tidak perlu dialokasikan selama setiap iterasi.
Faktorkan ekspresi konstan di luar loop
Jika nilai kalkulasi atau variabel tidak bergantung pada indeks loop, pindahkan ke luar (sebelum) loop.
I / O dalam blok
Membaca dan menulis data dalam potongan besar (blok). Lebih besar lebih baik. Misalnya, membaca satu oktek dalam satu waktu kurang efisien dibandingkan membaca 1024 oktet dengan sekali pembacaan.
Contoh:
Efisiensi teknik ini dapat ditunjukkan secara visual. :-)
Jangan gunakan
printf
keluarga untuk data konstanData konstan dapat dikeluarkan dengan menggunakan penulisan blok. Penulisan berformat akan membuang waktu memindai teks untuk memformat karakter atau memproses perintah pemformatan. Lihat contoh kode di atas.
Format ke memori, lalu tulis
Format ke
char
array menggunakan beberapasprintf
, lalu gunakanfwrite
. Ini juga memungkinkan tata letak data untuk dipecah menjadi "bagian konstan" dan bagian variabel. Pikirkan gabungan surat .Deklarasikan teks konstan (string literal) sebagai
static const
Ketika variabel dideklarasikan tanpa
static
, beberapa kompiler mungkin mengalokasikan ruang pada stack dan menyalin data dari ROM. Ini adalah dua operasi yang tidak perlu. Ini bisa diperbaiki dengan menggunakanstatic
awalan.Terakhir, Kode seperti kompilator
Terkadang, kompilator dapat mengoptimalkan beberapa pernyataan kecil dengan lebih baik daripada satu versi rumit. Selain itu, menulis kode untuk membantu pengoptimalan kompilator juga membantu. Jika saya ingin kompilator menggunakan instruksi transfer blok khusus, saya akan menulis kode yang sepertinya harus menggunakan instruksi khusus.
sumber
fprintf
format ke buffer terpisah kemudian mengeluarkan buffer. Sebuah streamline (untuk penggunaan memori)fprintf
akan mengeluarkan semua teks yang tidak diformat, kemudian memformat dan mengeluarkan, dan mengulang sampai seluruh format string diproses, sehingga membuat 1 panggilan keluaran untuk setiap jenis keluaran (diformat vs tidak diformat). Implementasi lain perlu mengalokasikan memori secara dinamis untuk setiap panggilan untuk menampung seluruh string baru (yang buruk dalam lingkungan sistem tertanam). Saran saya mengurangi jumlah keluaran.Pengoptimal tidak benar-benar mengontrol kinerja program Anda, Anda yang mengontrol. Gunakan algoritma dan struktur yang sesuai dan profil, profil, profil.
Yang mengatakan, Anda tidak boleh melakukan loop dalam pada fungsi kecil dari satu file di file lain, karena itu menghentikannya dari sebaris.
Hindari mengambil alamat variabel jika memungkinkan. Meminta pointer tidaklah "gratis" karena itu berarti variabel perlu disimpan dalam memori. Bahkan sebuah array dapat disimpan dalam register jika Anda menghindari pointer - ini penting untuk melakukan vektorisasi.
Yang mengarah ke poin berikutnya, baca manual ^ # $ @ ! GCC dapat memvektorisasi kode C biasa jika Anda menaburkannya
__restrict__
di__attribute__( __aligned__ )
sana sini. Jika Anda menginginkan sesuatu yang sangat spesifik dari pengoptimal, Anda mungkin harus spesifik.sumber
A.c
masuk ke dalamB.c
.Pada kebanyakan prosesor modern, hambatan terbesar adalah memori.
Aliasing: Load-Hit-Store bisa menghancurkan dalam loop yang ketat. Jika Anda membaca satu lokasi memori dan menulis ke yang lain dan mengetahui bahwa mereka terputus-putus, meletakkan kata kunci alias dengan hati-hati pada parameter fungsi benar-benar dapat membantu kompilator menghasilkan kode yang lebih cepat. Namun jika wilayah memori tumpang tindih dan Anda menggunakan 'alias', Anda berada dalam sesi debugging yang baik untuk perilaku tidak terdefinisi!
Cache-miss: Tidak begitu yakin bagaimana Anda dapat membantu kompiler karena sebagian besar bersifat algoritmik, tetapi ada intrinsik untuk mengambil memori terlebih dahulu.
Juga jangan mencoba untuk mengubah nilai floating point menjadi int dan sebaliknya terlalu banyak karena mereka menggunakan register yang berbeda dan mengkonversi dari satu jenis ke yang lain berarti memanggil instruksi konversi yang sebenarnya, menulis nilai ke memori dan membacanya kembali dalam set register yang tepat .
sumber
Sebagian besar kode yang ditulis orang akan terikat I / O (saya yakin semua kode yang saya tulis untuk uang dalam 30 tahun terakhir telah sangat terikat), jadi aktivitas pengoptimalan bagi kebanyakan orang akan bersifat akademis.
Namun, saya akan mengingatkan orang-orang bahwa agar kode dapat dioptimalkan Anda harus memberi tahu kompiler untuk mengoptimalkannya - banyak orang (termasuk saya ketika saya lupa) memposting benchmark C ++ di sini yang tidak ada artinya tanpa pengoptimalan diaktifkan.
sumber
gunakan kebenaran const sebanyak mungkin dalam kode Anda. Ini memungkinkan kompiler untuk mengoptimalkan jauh lebih baik.
Dalam dokumen ini banyak tips pengoptimalan lainnya: pengoptimalan CPP (dokumen yang agak lama sekalipun)
highlight:
sumber
const
danrestrict
penunjuk yang memenuhi syarat tidak ditentukan. Jadi kompiler dapat mengoptimalkan secara berbeda dalam kasus seperti itu.const
padaconst
referensi atauconst
pointer ke nonconst
objek didefinisikan dengan baik. memodifikasiconst
objek aktual (yaitu yang dideklarasikan sebagaiconst
aslinya) tidak.Cobalah memprogram menggunakan tugas tunggal statis sebanyak mungkin. SSA persis sama dengan apa yang Anda dapatkan di sebagian besar bahasa pemrograman fungsional, dan itulah yang sebagian besar kompiler mengonversi kode Anda untuk melakukan pengoptimalan mereka karena lebih mudah untuk dikerjakan. Dengan melakukan ini, tempat-tempat di mana penyusun mungkin bingung akan terungkap. Itu juga membuat semua kecuali pengalokasi register terburuk bekerja sebaik pengalokasi register terbaik, dan memungkinkan Anda untuk men-debug dengan lebih mudah karena Anda hampir tidak perlu bertanya-tanya dari mana variabel mendapatkan nilainya karena hanya ada satu tempat yang ditugaskan.
Hindari variabel global.
Saat bekerja dengan data dengan referensi atau penunjuk, tarik itu ke dalam variabel lokal, lakukan pekerjaan Anda, lalu salin kembali. (kecuali Anda memiliki alasan kuat untuk tidak melakukannya)
Manfaatkan perbandingan yang hampir gratis dengan 0 yang diberikan sebagian besar prosesor saat melakukan operasi matematika atau logika. Anda hampir selalu mendapatkan bendera untuk == 0 dan <0, yang darinya Anda dapat dengan mudah mendapatkan 3 kondisi:
hampir selalu lebih murah daripada menguji konstanta lain.
Trik lainnya adalah menggunakan pengurangan untuk menghilangkan satu perbandingan dalam pengujian jarak.
Hal ini sering kali dapat menghindari lompatan dalam bahasa yang melakukan hubungan singkat pada ekspresi boolean dan menghindari compiler harus mencoba mencari cara untuk menangani hasil dari perbandingan pertama saat melakukan perbandingan kedua dan kemudian menggabungkannya. Ini mungkin terlihat seperti berpotensi untuk menggunakan register tambahan, tetapi hampir tidak pernah melakukannya. Seringkali Anda tidak membutuhkan foo lagi, dan jika Anda melakukannya rc belum digunakan sehingga bisa pergi ke sana.
Saat menggunakan fungsi string di c (strcpy, memcpy, ...) ingat apa yang mereka kembalikan - tujuannya! Anda sering kali bisa mendapatkan kode yang lebih baik dengan 'melupakan' salinan penunjuk ke tujuan dan mengambilnya kembali dari kembalinya fungsi ini.
Jangan pernah mengabaikan peluang untuk mengembalikan hal yang persis sama dengan fungsi terakhir yang Anda panggil dikembalikan. Penyusun tidak begitu pandai mengambilnya sehingga:
Tentu saja, Anda bisa membalikkan logika jika dan hanya memiliki satu titik balik.
(trik yang saya ingat nanti)
Mendeklarasikan fungsi sebagai statis bila Anda bisa selalu merupakan ide yang bagus. Jika compiler dapat membuktikan pada dirinya sendiri bahwa ia telah memperhitungkan setiap pemanggil dari fungsi tertentu, maka compiler dapat merusak konvensi pemanggilan untuk fungsi tersebut atas nama pengoptimalan. Penyusun sering kali dapat menghindari pemindahan parameter ke register atau posisi tumpukan yang disebut fungsi biasanya mengharapkan parameternya masuk (harus menyimpang baik dalam fungsi yang dipanggil maupun lokasi semua pemanggil untuk melakukan ini). Kompiler juga sering mengambil keuntungan dari mengetahui memori dan register apa yang dibutuhkan fungsi yang dipanggil dan menghindari pembuatan kode untuk mempertahankan nilai variabel yang ada di register atau lokasi memori yang tidak diganggu oleh fungsi yang dipanggil. Ini bekerja sangat baik ketika hanya ada sedikit panggilan ke suatu fungsi.
sumber
Saya menulis kompiler C yang mengoptimalkan dan berikut beberapa hal yang sangat berguna untuk dipertimbangkan:
Jadikan sebagian besar fungsi statis. Hal ini memungkinkan propagasi konstan antarprocedural dan analisis alias untuk melakukan tugasnya, jika tidak, compiler perlu menganggap bahwa fungsi tersebut dapat dipanggil dari luar unit terjemahan dengan nilai parameter yang sama sekali tidak diketahui. Jika Anda melihat perpustakaan open-source terkenal, mereka semua menandai fungsi statis kecuali yang benar-benar perlu bersifat eksternal.
Jika variabel global digunakan, tandai sebagai statis dan konstan jika memungkinkan. Jika mereka diinisialisasi sekali (read-only), lebih baik menggunakan daftar penginisialisasi seperti static const int VAL [] = {1,2,3,4}, jika tidak, compiler mungkin tidak menemukan bahwa variabel sebenarnya adalah konstanta yang diinisialisasi dan akan gagal mengganti beban dari variabel dengan konstanta.
JANGAN PERNAH menggunakan goto ke bagian dalam loop, loop tidak akan dikenali lagi oleh sebagian besar kompiler dan tidak ada pengoptimalan terpenting yang akan diterapkan.
Gunakan parameter penunjuk hanya jika perlu, dan tandai sebagai batas jika memungkinkan. Ini sangat membantu analisis alias karena programmer menjamin tidak ada alias (analisis alias antarprocedural biasanya sangat primitif). Objek struct yang sangat kecil harus diteruskan dengan nilai, bukan dengan referensi.
Gunakan array sebagai pengganti pointer bila memungkinkan, terutama di dalam loop (a [i]). Sebuah array biasanya menawarkan lebih banyak informasi untuk analisis alias dan setelah beberapa pengoptimalan, kode yang sama akan tetap dibuat (cari pengurangan kekuatan loop jika penasaran). Ini juga meningkatkan peluang untuk menerapkan gerakan kode loop-invarian.
Cobalah untuk mengangkat panggilan di luar loop ke fungsi besar atau fungsi eksternal yang tidak memiliki efek samping (tidak bergantung pada iterasi loop saat ini). Fungsi kecil dalam banyak kasus menjadi inline atau diubah menjadi intrinsik yang mudah untuk diangkat, tetapi fungsi besar mungkin tampak bagi penyusun untuk memiliki efek samping padahal sebenarnya tidak. Efek samping untuk fungsi eksternal sama sekali tidak diketahui, dengan pengecualian beberapa fungsi dari pustaka standar yang terkadang dimodelkan oleh beberapa kompiler, memungkinkan gerakan kode loop-invarian.
Saat menulis tes dengan beberapa kondisi, tempatkan yang paling mungkin terlebih dahulu. if (a || b || c) harus if (b || a || c) if b lebih cenderung benar daripada yang lain. Kompiler biasanya tidak tahu apa-apa tentang nilai yang mungkin dari kondisi dan cabang mana yang diambil lebih banyak (mereka dapat diketahui dengan menggunakan informasi profil, tetapi sedikit pemrogram yang menggunakannya).
Menggunakan sakelar lebih cepat daripada melakukan pengujian seperti if (a || b || ... || z). Periksa terlebih dahulu apakah kompiler Anda melakukan ini secara otomatis, beberapa melakukannya dan lebih mudah dibaca untuk memiliki jika .
sumber
Dalam kasus sistem tertanam dan kode yang ditulis dalam C / C ++, saya mencoba dan menghindari alokasi memori dinamis sebanyak mungkin. Alasan utama saya melakukan ini belum tentu kinerja tetapi aturan praktis ini memiliki implikasi kinerja.
Algoritme yang digunakan untuk mengelola heap sangat lambat di beberapa platform (misalnya, vxworks). Lebih buruk lagi, waktu yang diperlukan untuk kembali dari panggilan ke malloc sangat bergantung pada status heap saat ini. Oleh karena itu, fungsi apa pun yang memanggil malloc akan mengalami penurunan kinerja yang tidak dapat dengan mudah diperhitungkan. Kinerja yang dicapai tersebut mungkin minimal jika heap masih bersih, tetapi setelah itu perangkat berjalan beberapa saat, heap dapat menjadi terfragmentasi. Panggilan akan memakan waktu lebih lama dan Anda tidak dapat dengan mudah menghitung bagaimana kinerja akan menurun seiring waktu. Anda tidak dapat benar-benar menghasilkan perkiraan kasus yang lebih buruk. Pengoptimal juga tidak dapat memberikan bantuan apa pun kepada Anda dalam kasus ini. Lebih buruk lagi, jika heap menjadi terlalu terfragmentasi, panggilan akan mulai gagal sama sekali. Solusinya adalah dengan menggunakan kumpulan memori (misalnya,glib slices ), bukan heap. Panggilan alokasi akan menjadi lebih cepat dan deterministik jika Anda melakukannya dengan benar.
sumber
Tip kecil yang bodoh, tetapi yang akan menghemat beberapa kecepatan dan kode mikroskopis.
Selalu berikan argumen fungsi dengan urutan yang sama.
Jika Anda memiliki f_1 (x, y, z) yang memanggil f_2, nyatakan f_2 sebagai f_2 (x, y, z). Jangan mendeklarasikannya sebagai f_2 (x, z, y).
Alasannya adalah karena C / C ++ platform ABI (konvensi pemanggilan AKA) berjanji untuk meneruskan argumen di register dan lokasi stack tertentu. Ketika argumen sudah dalam register yang benar, maka argumen itu tidak harus memindahkannya.
Saat membaca kode yang dibongkar, saya telah melihat beberapa pengacakan daftar yang konyol karena orang tidak mengikuti aturan ini.
sumber
Dua teknik pengkodean yang tidak saya lihat dalam daftar di atas:
Bypass linker dengan menulis kode sebagai sumber unik
Meskipun kompilasi terpisah sangat bagus untuk waktu kompilasi, sangat buruk jika Anda berbicara tentang pengoptimalan. Pada dasarnya kompiler tidak dapat mengoptimalkan di luar unit kompilasi, yaitu domain khusus linker.
Tetapi jika Anda mendesain dengan baik program Anda, Anda juga dapat mengkompilasinya melalui sumber umum yang unik. Itu bukan mengkompilasi unit1.c dan unit2.c lalu tautkan kedua objek, kompilasi all.c yang hanya #include unit1.c dan unit2.c. Dengan demikian, Anda akan mendapatkan keuntungan dari semua pengoptimalan compiler.
Ini sangat mirip dengan menulis program hanya header di C ++ (dan bahkan lebih mudah dilakukan di C).
Teknik ini cukup mudah jika Anda menulis program Anda untuk mengaktifkannya dari awal, tetapi Anda juga harus menyadari itu mengubah bagian dari semantik C dan Anda dapat menemui beberapa masalah seperti variabel statis atau benturan makro. Untuk kebanyakan program, cukup mudah untuk mengatasi masalah kecil yang terjadi. Perlu diketahui juga bahwa mengompilasi sebagai sumber unik jauh lebih lambat dan mungkin membutuhkan banyak memori (biasanya bukan masalah dengan sistem modern).
Dengan menggunakan teknik sederhana ini, saya kebetulan membuat beberapa program yang saya tulis sepuluh kali lebih cepat!
Seperti kata kunci register, trik ini juga bisa segera menjadi usang. Optimalisasi melalui linker mulai didukung oleh compiler gcc: Link time optimization .
Pisahkan tugas atom dalam loop
Yang ini lebih rumit. Ini tentang interaksi antara desain algoritma dan cara pengoptimal mengelola cache dan alokasi register. Cukup sering program harus mengulang beberapa struktur data dan untuk setiap item melakukan beberapa tindakan. Cukup sering tindakan yang dilakukan dapat dibagi menjadi dua tugas yang independen secara logis. Jika itu kasusnya, Anda dapat menulis program yang persis sama dengan dua loop pada batas yang sama yang melakukan tepat satu tugas. Dalam beberapa kasus menulis dengan cara ini bisa lebih cepat daripada loop unik (detailnya lebih kompleks, tetapi penjelasannya bisa jadi dengan kasus tugas sederhana semua variabel dapat disimpan dalam register prosesor dan dengan yang lebih kompleks itu tidak mungkin dan beberapa register harus ditulis ke memori dan dibaca kembali nanti dan biayanya lebih tinggi daripada kontrol aliran tambahan).
Hati-hati dengan yang satu ini (penampilan profil menggunakan trik ini atau tidak) karena seperti menggunakan register mungkin juga memberikan kinerja yang lebih rendah daripada yang ditingkatkan.
sumber
Saya sebenarnya telah melihat ini dilakukan di SQLite dan mereka mengklaim itu menghasilkan peningkatan kinerja ~ 5%: Letakkan semua kode Anda dalam satu file atau gunakan preprocessor untuk melakukan hal yang setara dengan ini. Dengan cara ini pengoptimal akan memiliki akses ke seluruh program dan dapat melakukan lebih banyak pengoptimalan antarprocedural.
sumber
-O3
- itu meledakkan 22% dari ukuran asli dari program saya. (Ini tidak terikat CPU, jadi saya tidak banyak bicara tentang kecepatan.)Sebagian besar kompiler modern harus melakukan pekerjaan yang baik dengan mempercepat rekursi tail , karena pemanggilan fungsi dapat dioptimalkan.
Contoh:
Tentu saja contoh ini tidak memiliki pemeriksaan batas.
Edit Terlambat
Sementara saya tidak memiliki pengetahuan langsung tentang kode tersebut; terlihat jelas bahwa persyaratan penggunaan CTE di SQL Server dirancang khusus agar dapat dioptimalkan melalui rekursi tail-end.
sumber
Jangan lakukan pekerjaan yang sama berulang kali!
Antipattern umum yang saya lihat berjalan di sepanjang garis ini:
Kompilator sebenarnya harus memanggil semua fungsi itu sepanjang waktu. Dengan asumsi Anda, programmer, tahu bahwa objek gabungan tidak berubah selama panggilan ini, karena cinta semua yang suci ...
Dalam kasus pengambil tunggal, panggilan tersebut mungkin tidak terlalu mahal, tetapi tentu saja merupakan biaya (biasanya, "periksa untuk melihat apakah objek telah dibuat, jika belum, buat, lalu kembalikan). semakin rumit rantai pengambil ini, semakin banyak waktu terbuang yang kita miliki.
sumber
Gunakan cakupan paling lokal untuk semua deklarasi variabel.
Gunakan
const
bila memungkinkanJangan gunakan register kecuali Anda berencana untuk membuat profil dengan dan tanpa itu
2 yang pertama, terutama # 1 yang membantu pengoptimal menganalisis kode. Ini akan sangat membantunya untuk membuat pilihan yang baik tentang variabel apa yang harus disimpan dalam register.
Secara membabi buta menggunakan kata kunci register kemungkinan besar akan membantu seperti merugikan pengoptimalan Anda, Terlalu sulit untuk mengetahui apa yang akan menjadi masalah sampai Anda melihat keluaran atau profil perakitan.
Ada hal lain yang penting untuk mendapatkan kinerja yang baik dari kode; merancang struktur data Anda untuk memaksimalkan koherensi cache misalnya. Tapi pertanyaannya adalah tentang pengoptimal.
sumber
Sejajarkan data Anda dengan batas asli / alami.
sumber
Saya teringat akan sesuatu yang pernah saya temui, di mana gejalanya hanya karena kami kehabisan memori, tetapi hasilnya adalah peningkatan kinerja yang substansial (serta pengurangan besar dalam jejak memori).
Masalah dalam kasus ini adalah perangkat lunak yang kami gunakan membuat banyak alokasi kecil. Seperti, mengalokasikan empat byte di sini, enam byte di sana, dll. Banyak objek kecil juga, berjalan dalam kisaran 8-12 byte. Masalahnya bukan pada program yang membutuhkan banyak hal kecil, tetapi program itu mengalokasikan banyak hal kecil secara individual, yang membengkak setiap alokasi menjadi (pada platform khusus ini) 32 byte.
Bagian dari solusinya adalah mengumpulkan kumpulan objek kecil bergaya Alexandrescu, tetapi memperluasnya sehingga saya dapat mengalokasikan array objek kecil serta item individual. Ini sangat membantu dalam kinerja juga karena lebih banyak item masuk ke dalam cache pada satu waktu.
Bagian lain dari solusi ini adalah mengganti penggunaan yang merajalela dari anggota char * yang dikelola secara manual dengan string SSO (pengoptimalan string kecil). Alokasi minimum 32 byte, saya membangun kelas string yang memiliki buffer 28 karakter tertanam di belakang char *, jadi 95% string kami tidak perlu melakukan alokasi tambahan (dan kemudian saya secara manual mengganti hampir setiap tampilan char * di perpustakaan ini dengan kelas baru ini, menyenangkan atau tidak). Ini juga membantu banyak dengan fragmentasi memori, yang kemudian meningkatkan lokalitas referensi untuk objek rujukan lainnya, dan demikian pula, ada peningkatan kinerja.
sumber
Teknik rapi yang saya pelajari dari @MSalters mengomentari jawaban ini memungkinkan kompiler untuk melakukan penghapusan salinan bahkan ketika mengembalikan objek yang berbeda sesuai dengan beberapa kondisi:
sumber
Jika Anda memiliki fungsi kecil yang Anda panggil berulang kali, saya di masa lalu mendapat keuntungan besar dengan menempatkannya di header sebagai "sebaris statis". Panggilan fungsi pada ix86 ternyata sangat mahal.
Menerapkan kembali fungsi rekursif dengan cara non-rekursif menggunakan tumpukan eksplisit juga bisa mendapatkan banyak keuntungan, tetapi kemudian Anda benar-benar berada di ranah waktu pengembangan vs keuntungan.
sumber
Inilah saran pengoptimalan kedua saya. Seperti saran pertama saya, ini adalah tujuan umum, bukan khusus bahasa atau prosesor.
Baca manual kompilator secara menyeluruh dan pahami apa yang diberitahukannya kepada Anda. Gunakan kompilator secara maksimal.
Saya setuju dengan satu atau dua responden lain yang telah mengidentifikasi pemilihan algoritme yang tepat sebagai hal yang penting untuk memeras kinerja dari suatu program. Di luar itu, tingkat pengembalian (diukur dalam peningkatan eksekusi kode) pada waktu Anda berinvestasi dalam menggunakan compiler jauh lebih tinggi daripada tingkat pengembalian dalam mengubah kode.
Ya, penulis kompilator bukan dari ras raksasa pengkodean dan kompiler mengandung kesalahan dan apa yang seharusnya, menurut manual dan menurut teori kompilator, membuat segalanya lebih cepat terkadang membuat segalanya lebih lambat. Itulah mengapa Anda harus mengambil satu langkah pada satu waktu dan mengukur kinerja sebelum dan sesudah penyesuaian.
Dan ya, pada akhirnya, Anda mungkin dihadapkan pada ledakan kombinatorial tanda kompilator sehingga Anda perlu memiliki satu atau dua skrip untuk menjalankan make dengan berbagai tanda kompilator, memasukkan tugas ke antrean di cluster besar dan mengumpulkan statistik waktu proses. Jika hanya Anda dan Visual Studio di PC, Anda akan kehabisan minat lama sebelum Anda mencoba kombinasi yang cukup dari cukup flag compiler.
Salam
Menandai
Ketika saya pertama kali mengambil sepotong kode, saya biasanya bisa mendapatkan faktor kinerja 1,4 - 2,0 kali lebih banyak (yaitu versi baru kode berjalan dalam 1 / 1.4 atau 1/2 dari waktu versi lama) dalam a satu atau dua hari dengan mengutak-atik flag compiler. Memang, itu mungkin komentar tentang kurangnya pemahaman kompiler di antara ilmuwan yang membuat sebagian besar kode yang saya kerjakan, bukan gejala keunggulan saya. Setelah mengatur flag compiler ke max (dan ini jarang hanya -O3) dibutuhkan kerja keras berbulan-bulan untuk mendapatkan faktor lain dari 1.05 atau 1.1
sumber
Ketika DEC keluar dengan prosesor alfa-nya, ada rekomendasi untuk menyimpan jumlah argumen ke fungsi di bawah 7, karena kompilator akan selalu mencoba memasukkan hingga 6 argumen dalam register secara otomatis.
sumber
Untuk performa, pertama-tama fokuslah pada penulisan kode yang dapat dipertahankan - terkomponen, digabungkan secara longgar, dll., Jadi ketika Anda harus memisahkan suatu bagian untuk menulis ulang, mengoptimalkan, atau hanya membuat profil, Anda dapat melakukannya tanpa banyak usaha.
Pengoptimal akan sedikit membantu kinerja program Anda.
sumber
Anda mendapatkan jawaban yang bagus di sini, tetapi mereka menganggap program Anda hampir optimal untuk memulai, dan Anda berkata
Menurut pengalaman saya, sebuah program dapat ditulis dengan benar, tetapi itu tidak berarti program itu hampir optimal. Dibutuhkan kerja ekstra untuk mencapai titik itu.
Jika saya dapat memberikan contoh, jawaban ini menunjukkan bagaimana program yang tampak sangat masuk akal dibuat 40 kali lebih cepat dengan pengoptimalan makro . Percepatan besar tidak dapat dilakukan di setiap program seperti yang ditulis pertama kali, tetapi di banyak program (kecuali untuk program yang sangat kecil), menurut pengalaman saya.
Setelah itu selesai, optimasi mikro (dari hot-spot) dapat memberi Anda hasil yang bagus.
sumber
saya menggunakan kompiler intel. di Windows dan Linux.
ketika kurang lebih selesai saya membuat profil kode. kemudian bertahan di hotspot dan mencoba mengubah kode untuk memungkinkan compiler membuat pekerjaan yang lebih baik.
jika kode adalah kode komputasi dan berisi banyak loop - laporan vektorisasi di kompiler intel sangat membantu - cari 'vec-report' di bantuan.
jadi ide utama - memoles kode kritis kinerja. selebihnya - prioritas untuk diperbaiki dan dipelihara - fungsi singkat, kode yang jelas yang dapat dipahami 1 tahun kemudian.
sumber
Satu pengoptimalan yang saya gunakan di C ++ adalah membuat konstruktor yang tidak melakukan apa pun. Seseorang harus secara manual memanggil init () untuk menempatkan objek ke status kerja.
Ini bermanfaat dalam kasus di mana saya membutuhkan vektor besar dari kelas-kelas ini.
Saya memanggil reserve () untuk mengalokasikan ruang untuk vektor, tetapi konstruktor tidak benar-benar menyentuh halaman memori tempat objek berada. Jadi saya telah menghabiskan beberapa ruang alamat, tetapi sebenarnya tidak menghabiskan banyak memori fisik. Saya menghindari kesalahan halaman yang terkait dengan biaya konstruksi terkait.
Saat saya membuat objek untuk mengisi vektor, saya mengaturnya menggunakan init (). Ini membatasi kesalahan halaman total saya, dan menghindari kebutuhan untuk mengubah ukuran () vektor saat mengisinya.
sumber
Satu hal yang telah saya lakukan adalah mencoba menyimpan tindakan mahal ke tempat-tempat di mana pengguna mungkin mengharapkan program untuk sedikit tertunda. Kinerja keseluruhan terkait dengan daya tanggap, tetapi tidak persis sama, dan untuk banyak hal daya tanggap adalah bagian yang lebih penting dari kinerja.
Terakhir kali saya benar-benar harus melakukan peningkatan kinerja secara keseluruhan, saya mengawasi algoritme yang kurang optimal, dan mencari tempat-tempat yang kemungkinan memiliki masalah cache. Saya membuat profil dan mengukur kinerja terlebih dahulu, dan sekali lagi setelah setiap perubahan. Kemudian perusahaan itu bangkrut, tetapi itu tetap merupakan pekerjaan yang menarik dan instruktif.
sumber
Saya sudah lama curiga, tetapi tidak pernah membuktikan bahwa mendeklarasikan array sehingga mereka memiliki kekuatan 2, sebagai jumlah elemen, memungkinkan pengoptimal melakukan pengurangan kekuatan dengan mengganti perkalian dengan pergeseran sejumlah bit, ketika melihat ke atas elemen individu.
sumber
val * 7
berubah menjadi apa yang akan terlihat seperti itu(val << 3) - val
.Letakkan fungsi kecil dan / atau yang sering disebut di bagian atas file sumber. Hal ini mempermudah penyusun untuk menemukan peluang penyebarisan.
sumber