Praktik Pengkodean yang memungkinkan kompiler / pengoptimal untuk membuat program lebih cepat

116

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.

Ini pertanyaan terkait

[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

EvilTeach
sumber
7
Bisa menjadi kandidat yang baik untuk komunitas wiki imho karena tidak ada jawaban pasti 'tunggal' untuk pertanyaan (menarik) ini ...
ChristopheD
Saya merindukannya setiap saat. terimakasih telah menunjukkan itu.
EvilTeach
Yang Anda maksud dengan 'lebih baik' adalah 'lebih cepat' atau apakah Anda memiliki kriteria keunggulan lainnya?
Kinerja Tinggi Mark
1
Cukup sulit untuk menulis pengalokasi register yang baik, terutama portabel, dan alokasi register sangat penting untuk kinerja dan ukuran kode. registersebenarnya membuat kode yang peka terhadap kinerja lebih portabel dengan memerangi kompiler yang buruk.
Potatoswatter
1
@EvilTeach: wiki komunitas tidak berarti "tidak ada jawaban pasti", ini tidak sama dengan tag subjektif. Wiki komunitas berarti Anda ingin menyerahkan postingan Anda ke komunitas agar orang lain dapat mengeditnya. Jangan merasa tertekan untuk mengajukan pertanyaan Anda jika Anda tidak menyukainya.
Juliet

Jawaban:

54

Tulis ke variabel lokal dan bukan argumen keluaran! Ini bisa sangat membantu untuk mengatasi pelambatan aliasing. Misalnya, jika kode Anda terlihat seperti

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

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:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

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).

celion
sumber
7
Ini memiliki keuntungan tambahan karena sering membuat hal-hal lebih mudah dibaca / dipahami untuk pemrogram juga, karena mereka juga tidak perlu khawatir tentang kemungkinan efek samping yang tidak jelas.
Michael Burr
Kebanyakan IDE menampilkan variabel lokal secara default, jadi ada lebih sedikit pengetikan
EvilTeach
9
Anda juga dapat mengaktifkan pengoptimalan itu dengan menggunakan petunjuk terbatas
Ben Voigt
4
@ Ben - itu benar, tapi menurut saya cara ini lebih jelas. Juga, jika input dan output tumpang tindih, saya yakin hasilnya tidak ditentukan dengan petunjuk terbatas (mungkin mendapatkan perilaku berbeda antara debug dan rilis), sedangkan cara ini setidaknya akan konsisten. Jangan salah paham, saya suka menggunakan pembatasan, tapi saya lebih suka tidak membutuhkannya lagi.
celion
Anda baru saja berharap bahwa Foo tidak memiliki operasi penyalinan yang dapat menyalin beberapa meg data ;-)
Skizz
76

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.

High Performance Mark
sumber
20
Pengembang compiier memiliki waktu terbatas, sama seperti orang lain. Tidak semua pengoptimalan akan membuatnya menjadi kompiler. Suka &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.
Dave Jarvis
22
& dan% hampir selalu dioptimalkan, bersama dengan sebagian besar trik aritmatika murah-gratis lainnya. Apa yang tidak dioptimalkan adalah kasus operan kanan menjadi variabel yang kebetulan selalu menjadi pangkat dua.
Potatoswatter
8
Untuk memperjelas, saya tampaknya telah membingungkan beberapa pembaca: saran dalam praktik pengkodean yang saya usulkan adalah pertama-tama mengembangkan kode langsung yang tidak menggunakan instruksi tata letak memori untuk menetapkan dasar kinerja. Kemudian, cobalah sesuatu satu per satu dan ukur dampaknya. Saya belum menawarkan saran apa pun tentang kinerja operasi.
Kinerja Tinggi Mark
17
Untuk power-of-two yang konstan n, gcc menggantikan % ndengan & (n-1) bahkan saat pengoptimalan dinonaktifkan . Itu tidak persis "jarang, jika pernah" ...
Porculus
12
% TIDAK DAPAT dioptimalkan sebagai & ketika tipe ditandatangani, karena aturan idiot C untuk pembagian bilangan bulat negatif (dibulatkan ke arah 0 dan memiliki sisa negatif, daripada pembulatan ke bawah dan selalu memiliki sisa positif). Dan sebagian besar waktu, pembuat kode yang bodoh menggunakan tipe yang ditandatangani ...
R .. GitHub STOP HELPING ICE
47

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:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}
vicatcu
sumber
Sebenarnya ini bukan masalah pengoptimal, tetapi masalah pengoptimalan.
EvilTeach
10
Yakin itu masalah pengoptimal. Orang-orang telah menulis makalah tentang pengoptimalan loop interchange otomatis selama beberapa dekade.
Phil Miller
20
@Potatoswatter Apa yang kamu bicarakan? Kompilator C dapat melakukan apapun yang diinginkannya selama hasil akhir yang sama diamati, dan memang GCC 4.4 memiliki -floop-interchangeyang akan membalik loop dalam dan luar jika pengoptimal menganggapnya menguntungkan.
singkat
2
Huh, ini dia. Semantik C sering dirusak oleh masalah aliasing. Saya kira nasihat sebenarnya di sini adalah mengibarkan bendera itu!
Potatoswatter
36

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 inlineatau makro

Setiap 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 #definemakro atau inlinefungsi. (Ya, saya tahu inlineini 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:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Setelah membuka gulungan:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

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 memcpyjauh lebih cepat daripada compiler memcpy. - TM

Pengurangan ifpernyataan

Prosesor 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 ifpernyataan menjadi tugas boolean. Beberapa prosesor dapat menjalankan instruksi secara kondisional tanpa bercabang:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

The arus pendek dari Logical AND operator ( &&) mencegah pelaksanaan tes jika statusini false.

Contoh:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

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:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

Efisiensi teknik ini dapat ditunjukkan secara visual. :-)

Jangan gunakan printf keluarga untuk data konstan

Data 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 chararray menggunakan beberapa sprintf, lalu gunakan fwrite. 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 menggunakan staticawalan.

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.

Thomas Matthews
sumber
2
Menarik, Anda dapat memberikan contoh di mana Anda mendapatkan kode yang lebih baik dengan beberapa pernyataan kecil, bukan yang lebih besar. Bisakah Anda menunjukkan contoh menulis ulang jika, menggunakan boolean. Secara umum, saya akan membiarkan loop membuka gulungan ke kompiler, karena mungkin memiliki perasaan yang lebih baik untuk ukuran cache. Saya agak terkejut dengan ide sprintfing, lalu fwriting. Saya akan berpikir bahwa fprintf sebenarnya melakukan itu di bawah tenda. Bisakah Anda memberikan sedikit lebih banyak detail di sini?
EvilTeach
1
Tidak ada jaminan bahwa fprintfformat ke buffer terpisah kemudian mengeluarkan buffer. Sebuah streamline (untuk penggunaan memori) fprintfakan 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.
Thomas Matthews
3
Saya pernah mendapatkan peningkatan kinerja yang signifikan dengan menggulung satu lingkaran. Kemudian saya menemukan cara untuk menggulungnya lebih erat dengan menggunakan beberapa tipuan, dan program itu terasa lebih cepat. (Pembuatan profil menunjukkan fungsi khusus ini menjadi 60-80% dari waktu proses, dan saya menguji kinerja dengan hati-hati sebelum dan sesudah.) Saya yakin peningkatan ini disebabkan oleh lokalitas yang lebih baik, tetapi saya tidak sepenuhnya yakin tentang itu.
David Thornley
16
Banyak di antaranya adalah pengoptimalan programmer daripada cara bagi programmer untuk membantu compiler melakukan pengoptimalan, yang merupakan inti dari pertanyaan awal. Misalnya, loop unrolling. Ya, Anda dapat melakukan unrolling Anda sendiri, tetapi menurut saya lebih menarik untuk mencari tahu penghalang apa yang ada pada kompiler yang membuka gulungan untuk Anda dan menghapusnya.
Adrian McCarthy
26

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.

Potatoswatter
sumber
14
Ini adalah jawaban yang bagus, tetapi perhatikan bahwa pengoptimalan seluruh program menjadi lebih populer, dan sebenarnya dapat berfungsi sebaris di seluruh unit terjemahan.
Phil Miller
1
@Novelocrat Ya - tentu saja saya sangat terkejut saat pertama kali saya melihat sesuatu dari A.cmasuk ke dalam B.c.
Jonathon Reinhart
18

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 .

Francis Boivin
sumber
4
1 untuk toko beban-hit dan tipe register yang berbeda. Saya tidak yakin seberapa besar kesepakatannya di x86, tetapi mereka melakukan devestasi pada PowerPC (misalnya Xbox360 dan Playstation3).
celion
Sebagian besar makalah tentang teknik pengoptimalan loop kompiler mengasumsikan nesting sempurna, yang berarti bahwa isi setiap loop kecuali yang paling dalam hanyalah loop lain. Makalah ini sama sekali tidak membahas langkah-langkah yang diperlukan untuk menggeneralisasi seperti itu, meskipun sangat jelas bahwa mereka dapat melakukannya. Jadi, saya berharap banyak penerapan yang tidak benar-benar mendukung generalisasi tersebut, karena upaya ekstra yang diperlukan. Dengan demikian, banyak algoritme untuk mengoptimalkan penggunaan cache dalam loop mungkin bekerja jauh lebih baik pada sarang yang sempurna daripada pada sarang yang tidak sempurna.
Phil Miller
11

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.

segera
sumber
7
Saya mengaku aneh - saya mengerjakan kode pengolah angka ilmiah besar yang terikat bandwidth memori. Untuk populasi umum program, saya setuju dengan Neil.
Kinerja Tinggi Mark
6
Benar; tetapi banyak sekali kode yang terikat I / O saat ini ditulis dalam bahasa yang secara praktis pesimizer - bahasa yang bahkan tidak memiliki kompiler. Saya menduga bahwa area di mana C dan C ++ masih digunakan akan cenderung menjadi area di mana lebih penting untuk mengoptimalkan sesuatu (penggunaan CPU, penggunaan memori, ukuran kode ...)
Porculus
3
Saya telah menghabiskan sebagian besar dari 30 tahun terakhir mengerjakan kode dengan sangat sedikit I / O. Hemat selama 2 tahun mengerjakan database. Grafik, sistem kontrol, simulasi - tidak ada yang terikat I / O. Jika I / O adalah hambatan bagi kebanyakan orang, kami tidak akan terlalu memperhatikan Intel dan AMD.
phkahler
2
Ya, saya tidak benar-benar percaya argumen ini - jika tidak, kami (di pekerjaan saya) tidak akan mencari cara untuk menghabiskan lebih banyak waktu komputasi dan juga melakukan I / O. Juga- banyak perangkat lunak terikat I / O yang saya temui telah terikat I / O karena I / O dilakukan secara sembarangan; jika seseorang mengoptimalkan pola akses (seperti halnya dengan memori), seseorang dapat memperoleh keuntungan besar dalam kinerja.
dash-tom-bang
3
Baru-baru ini saya menemukan bahwa hampir tidak ada kode yang ditulis dalam bahasa C ++ yang terikat I / O. Tentu, jika Anda memanggil fungsi OS untuk transfer disk massal, utas Anda mungkin masuk ke I / O menunggu (tetapi dengan caching, bahkan itu dipertanyakan). Tetapi fungsi pustaka I / O biasa, yang direkomendasikan semua orang karena standar dan portabel, sebenarnya sangat lambat dibandingkan dengan teknologi disk modern (bahkan barang dengan harga sedang). Kemungkinan besar, I / O adalah penghambat hanya jika Anda membuang semua jalan ke disk setelah menulis hanya beberapa byte. OTOH, UI adalah hal yang berbeda, kita manusia lambat.
Ben Voigt
11

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:

  • menggunakan daftar inisialisasi konstruktor
  • gunakan operator awalan
  • gunakan konstruktor eksplisit
  • fungsi sebaris
  • hindari benda sementara
  • perhatikan biaya fungsi virtual
  • mengembalikan objek melalui parameter referensi
  • pertimbangkan per alokasi kelas
  • pertimbangkan pengalokasi kontainer stl
  • optimasi 'anggota kosong'
  • dll
Kodok
sumber
8
Tidak banyak, jarang. Itu memang meningkatkan ketepatan sebenarnya.
Potatoswatter
5
Dalam C dan C ++ compiler tidak dapat menggunakan const untuk mengoptimalkan karena membuangnya adalah perilaku yang terdefinisi dengan baik.
dsimcha
+1: const adalah contoh bagus dari sesuatu yang akan berdampak langsung pada kode yang dikompilasi. re @ dsimcha's comment - kompilator yang baik akan menguji untuk melihat apakah ini terjadi. Tentu saja, kompiler yang baik akan "menemukan" elemen const yang tidak dideklarasikan seperti itu ...
Hogan
@dsimcha: Namun, mengubah penunjuk const dan restrict penunjuk yang memenuhi syarat tidak ditentukan. Jadi kompiler dapat mengoptimalkan secara berbeda dalam kasus seperti itu.
Dietrich Epp
6
@dsimcha pengecoran pergi constpada constreferensi atau constpointer ke non constobjek didefinisikan dengan baik. memodifikasi constobjek aktual (yaitu yang dideklarasikan sebagai constaslinya) tidak.
Stephen Lin
9

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:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

hampir selalu lebih murah daripada menguji konstanta lain.

Trik lainnya adalah menggunakan pengurangan untuk menghilangkan satu perbandingan dalam pengujian jarak.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

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:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

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.

nategoose
sumber
2
Sebenarnya tidak perlu menggunakan pengurangan saat menguji rentang, LLVM, GCC dan kompiler saya setidaknya melakukan ini secara otomatis. Hanya sedikit orang yang mungkin memahami apa yang dilakukan kode dengan pengurangan dan bahkan lebih sedikit lagi mengapa kode itu benar-benar berfungsi.
Gratian Lup
pada contoh di atas, b () tidak bisa dipanggil karena if (x <0) maka a () akan dipanggil.
EvilTeach
@EvilTeach Tidak itu tidak akan. Perbandingan yang menghasilkan panggilan ke a () adalah! X
nategoose
@tokopedia. jika x adalah -3 maka! x benar.
EvilTeach
@EvilTeach Di C 0 salah dan yang lainnya benar, jadi -3 benar, jadi! -3 salah
nategoose
9

Saya menulis kompiler C yang mengoptimalkan dan berikut beberapa hal yang sangat berguna untuk dipertimbangkan:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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).

  8. 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 .

Gratian Lup
sumber
7

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.

figurassa
sumber
Aturan praktis saya adalah jika Anda harus mengalokasikan secara dinamis, dapatkan array sehingga Anda tidak perlu melakukannya lagi. Lakukan pra-alokasi vektor.
EvilTeach
7

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.

Zan Lynx
sumber
2
Baik C maupun C ++ tidak menjamin, atau bahkan menyebutkan, meneruskan register atau lokasi tumpukan tertentu. Ini adalah ABI (misalnya Linux ELF) yang menentukan rincian parameter passing.
Emmet
5

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.

kriss
sumber
2
Ya, sekarang, LTO telah membuat paruh pertama posting ini berlebihan dan mungkin nasihat yang buruk.
underscore_d
@underscore_d: masih ada beberapa masalah (kebanyakan terkait dengan visibilitas simbol yang diekspor), tetapi dari sudut pandang kinerja mungkin tidak ada lagi.
kriss
4

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.

dsimcha
sumber
5
Menempatkan fungsi yang digunakan bersama dalam kedekatan fisik dalam sumber meningkatkan kemungkinan bahwa mereka akan berdekatan dalam file objek dan dekat satu sama lain dalam file yang dapat dieksekusi. Lokalitas instruksi yang ditingkatkan ini dapat membantu menghindari cache instruksi meleset saat berjalan.
paxos1977
Kompilator AIX memiliki sakelar kompilator untuk mendorong perilaku tersebut -qipa [= <suboptions_list>] | -qnoipa Mengaktifkan atau menyesuaikan kelas pengoptimalan yang dikenal sebagai analisis interprocedural (IPA).
EvilTeach
4
Yang terbaik adalah memiliki cara untuk berkembang yang tidak membutuhkan ini. Menggunakan fakta ini sebagai alasan untuk menulis kode un-modular secara keseluruhan hanya akan menghasilkan kode yang lambat dan memiliki masalah pemeliharaan.
Hogan
3
Saya pikir informasi ini agak ketinggalan jaman. Secara teori, fitur pengoptimalan seluruh program yang dibangun ke dalam banyak kompiler sekarang (mis. "Pengoptimalan Waktu Tautan" di gcc) memungkinkan manfaat yang sama, tetapi dengan alur kerja yang sepenuhnya standar (ditambah waktu kompilasi ulang yang lebih cepat daripada meletakkan semuanya dalam satu file !)
Ponkadoodle
@Wallacoloo Yang pasti, ini sudah usang. FWIW, saya baru saja menggunakan LTO GCC untuk pertama kalinya hari ini, dan - semuanya sama -O3- itu meledakkan 22% dari ukuran asli dari program saya. (Ini tidak terikat CPU, jadi saya tidak banyak bicara tentang kecepatan.)
underscore_d
4

Sebagian besar kompiler modern harus melakukan pekerjaan yang baik dengan mempercepat rekursi tail , karena pemanggilan fungsi dapat dioptimalkan.

Contoh:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

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.

Hogan
sumber
1
pertanyaannya adalah tentang C. C tidak menghapus rekursi-ekor, jadi ekor atau rekursi lain, tumpukan mungkin meledak jika rekursi berjalan terlalu dalam.
Kodok
1
Saya telah menghindari masalah konvensi panggilan, dengan menggunakan goto. Ada sedikit biaya tambahan seperti itu.
EvilTeach
2
@hogan: ini baru bagiku. Bisakah Anda menunjuk ke kompiler yang melakukan ini? Dan bagaimana Anda bisa yakin itu benar-benar mengoptimalkannya? Jika itu akan melakukan ini benar-benar perlu memastikan itu melakukannya. Ini bukanlah sesuatu yang Anda harap dapat dipahami oleh pengoptimal kompiler (seperti sebaris yang mungkin atau mungkin tidak berfungsi)
Toad
6
@hogan: Saya berdiri dikoreksi. Anda benar bahwa Gcc dan MSVC sama-sama melakukan pengoptimalan rekursi ekor.
Kodok
5
Contoh ini bukanlah rekursi ekor karena ini bukan panggilan rekursif yang terakhir, ini adalah perkaliannya.
Brian Young
4

Jangan lakukan pekerjaan yang sama berulang kali!

Antipattern umum yang saya lihat berjalan di sepanjang garis ini:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

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 ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

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.

dasbor-tom-bang
sumber
3
  1. Gunakan cakupan paling lokal untuk semua deklarasi variabel.

  2. Gunakan constbila memungkinkan

  3. Jangan 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.

John Knoeller
sumber
3

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.

dasbor-tom-bang
sumber
3

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:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;
Xeo
sumber
2

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.

Remy
sumber
Mengonversi rekursi menjadi tumpukan adalah pengoptimalan yang diasumsikan di ompf.org, untuk orang yang mengembangkan raytracers dan menulis algoritme rendering lainnya.
Tom
... Saya harus menambahkan ini, bahwa overhead terbesar dalam proyek raytracer pribadi saya adalah rekursi berbasis vtable melalui hierarki volume pembatas menggunakan pola Komposit. Ini benar-benar hanya sekumpulan kotak bersarang yang terstruktur sebagai pohon, tetapi menggunakan pola menyebabkan penggembungan data (penunjuk tabel virtual) dan mengurangi koherensi instruksi (apa yang bisa menjadi loop kecil / ketat sekarang menjadi rangkaian pemanggilan fungsi)
Tom
2

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

Tanda Kinerja Tinggi
sumber
2

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.

EvilTeach
sumber
x86-64 bit juga memungkinkan banyak parameter register-pass, yang dapat memiliki efek dramatis pada overhead pemanggilan fungsi.
Tom
1

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.

Ariel
sumber
3
Itu hanya berfungsi jika "antarmuka" penggandengan itu sendiri dapat menerima pengoptimalan. Antarmuka pada dasarnya bisa "lambat", misalnya dengan memaksa pencarian atau penghitungan yang berlebihan, atau memaksa akses cache yang buruk.
Tom
1

Anda mendapatkan jawaban yang bagus di sini, tetapi mereka menganggap program Anda hampir optimal untuk memulai, dan Anda berkata

Asumsikan bahwa program telah ditulis dengan benar, dikompilasi dengan optimalisasi penuh, diuji dan dimasukkan ke dalam produksi.

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.

Mike Dunlavey
sumber
1

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.

jf.
sumber
Anda hampir menjawab pertanyaan ..... hal macam apa yang Anda lakukan pada kode, untuk memungkinkan kompiler melakukan pengoptimalan semacam itu?
EvilTeach
1
Mencoba menulis lebih banyak dalam C-style (vs. dalam C ++) misalnya menghindari fungsi virtual tanpa kebutuhan mutlak, terutama jika mereka akan sering dipanggil, hindari AddRefs .. dan semua hal keren (sekali lagi kecuali jika benar-benar diperlukan). Menulis kode dengan mudah untuk membuat sebaris - lebih sedikit parameter, lebih sedikit "jika" -s. Tidak menggunakan variabel global kecuali kebutuhan mutlak. Dalam struktur data - letakkan bidang yang lebih luas terlebih dahulu (ganda, int64 sebelum int) - jadi compiler menyelaraskan struct pada ukuran alami bidang pertama - menyelaraskan baik untuk kinerja.
jf.
1
Tata letak dan akses data sangat penting untuk kinerja. Jadi setelah membuat profil - terkadang saya memecah struktur menjadi beberapa yang mengikuti lokalitas akses. Satu lagi trik umum - gunakan int atau size-t vs. char - bahkan nilai datanya kecil - hindari berbagai perf. penyimpanan hukuman untuk memuat pemblokiran, masalah dengan register parsial macet. tentu saja ini tidak berlaku ketika membutuhkan array besar dari data tersebut.
jf.
Satu lagi - hindari panggilan sistem, kecuali ada kebutuhan nyata :) - harganya SANGAT mahal
jf.
2
@jf: Saya memberi +1 pada jawaban Anda, tetapi bisakah Anda memindahkan jawaban dari komentar ke isi jawaban? Ini akan lebih mudah dibaca.
kriss
1

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.

EvilTeach
sumber
6
Saya percaya implementasi tipikal dari std :: vector tidak benar-benar membangun lebih banyak objek ketika Anda memesan () lebih banyak kapasitas. Itu hanya mengalokasikan halaman. Konstruktor dipanggil nanti, menggunakan penempatan baru, ketika Anda benar-benar menambahkan objek ke vektor - yang (mungkin) tepat sebelum Anda memanggil init (), jadi Anda tidak benar-benar memerlukan fungsi init () yang terpisah. Juga ingat bahwa meskipun konstruktor Anda "kosong" dalam kode sumber, konstruktor yang dikompilasi dapat berisi kode untuk menginisialisasi hal-hal seperti tabel virtual dan RTTI, sehingga halaman tetap disentuh pada waktu konstruksi.
Wyzard
1
Ya. Dalam kasus kami, kami menggunakan push_back untuk mengisi vektor. Objek tidak memiliki fungsi virtual apa pun, jadi ini bukan masalah. Pertama kali kami mencobanya dengan konstruktor, kami terkejut dengan banyaknya kesalahan halaman. Saya menyadari apa yang terjadi, dan kami menarik nyali dari konstruktor, dan masalah kesalahan halaman menghilang.
EvilTeach
Itu agak mengejutkan saya. Implementasi C ++ dan STL apa yang Anda gunakan?
David Thornley
3
Saya setuju dengan yang lain, ini terdengar seperti implementasi yang buruk dari std :: vector. Meskipun objek Anda memiliki vtable, objek tersebut tidak akan dibuat hingga push_back Anda. Anda harus dapat mengujinya dengan mendeklarasikan konstruktor default menjadi privat, karena semua vektor yang dibutuhkan adalah copy-constructor untuk push_back.
Tom
1
@David - Implementasinya di AIX.
EvilTeach
1

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.

David Thornley
sumber
0

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.

EvilTeach
sumber
6
Itu dulu benar, sekarang ini lagi. Justru sebaliknya yang benar. Jika Anda mendeklarasikan array Anda dengan pangkat dua, Anda kemungkinan besar akan mengalami situasi di mana Anda bekerja pada dua pointer pangkat dua terpisah dalam memori. Masalahnya adalah, bahwa cache CPU diatur seperti itu dan Anda mungkin berakhir dengan dua array yang bertengkar di sekitar satu baris cache. Anda mendapatkan kinerja yang buruk dengan cara itu. Memiliki salah satu pointer beberapa byte ke depan (misalnya non power dari dua) mencegah situasi ini.
Nils Pipenbrinck
+1 Nils, dan satu kejadian spesifik dari ini adalah "64k aliasing" pada perangkat keras Intel.
Tom
Ngomong-ngomong, ini adalah sesuatu yang mudah dibantah dengan melihat pembongkaran. Saya kagum, bertahun-tahun yang lalu, saat melihat bagaimana gcc akan mengoptimalkan semua jenis perkalian konstan dengan shift dan penjumlahan. Misalnya val * 7berubah menjadi apa yang akan terlihat seperti itu (val << 3) - val.
dash-tom-bang
0

Letakkan fungsi kecil dan / atau yang sering disebut di bagian atas file sumber. Hal ini mempermudah penyusun untuk menemukan peluang penyebarisan.

Mark Ransom
sumber
Betulkah? Dapatkah Anda mengutip alasan dan contoh untuk ini? Tidak mengatakan itu tidak benar, hanya terdengar tidak intuitif bahwa lokasi itu penting.
underscore_d
@underscore_d itu tidak bisa menyebariskan sesuatu sampai definisi fungsi diketahui. Meskipun kompiler modern mungkin membuat beberapa lintasan sehingga definisinya diketahui pada waktu pembuatan kode, saya tidak menganggapnya.
Markus Tebusan
Saya berasumsi bahwa kompiler mengerjakan grafik panggilan abstrak daripada urutan fungsi fisik, yang berarti ini tidak masalah. Tentu, saya kira tidak ada salahnya untuk ekstra hati-hati - terutama ketika, selain kinerja, IMO sepertinya lebih logis untuk mendefinisikan fungsi yang dipanggil sebelum yang memanggilnya. Saya harus menguji kinerja tetapi akan terkejut jika itu penting, tetapi sampai saat itu, saya terbuka untuk terkejut!
underscore_d