Saya bekerja pada proyek STAPL yang merupakan perpustakaan C ++ yang sangat templated. Sekali-sekali, kita harus meninjau kembali semua teknik untuk mengurangi waktu kompilasi. Di sini, saya telah merangkum teknik yang kami gunakan. Beberapa teknik ini sudah tercantum di atas:
Menemukan bagian yang paling memakan waktu
Meskipun tidak ada korelasi yang terbukti antara panjang simbol dan waktu kompilasi, kami telah mengamati bahwa ukuran simbol rata-rata yang lebih kecil dapat meningkatkan waktu kompilasi pada semua kompiler. Jadi, tujuan pertama Anda adalah menemukan simbol terbesar dalam kode Anda.
Metode 1 - Urutkan simbol berdasarkan ukuran
Anda dapat menggunakan nm
perintah untuk membuat daftar simbol berdasarkan ukurannya:
nm --print-size --size-sort --radix=d YOUR_BINARY
Dalam perintah ini, --radix=d
Anda dapat melihat ukuran dalam angka desimal (standarnya adalah hex). Sekarang dengan melihat simbol terbesar, kenali apakah Anda dapat memecah kelas yang sesuai dan mencoba mendesain ulang dengan memfaktorkan bagian-bagian yang tidak templated di kelas dasar, atau dengan membagi kelas menjadi beberapa kelas.
Metode 2 - Urutkan simbol berdasarkan panjangnya
Anda dapat menjalankan nm
perintah biasa dan menyalurkannya ke skrip favorit Anda ( AWK , Python , dll.) Untuk mengurutkan simbol berdasarkan panjangnya . Berdasarkan pengalaman kami, metode ini mengidentifikasi masalah terbesar yang membuat kandidat lebih baik daripada metode 1.
Metode 3 - Gunakan Templight
" Templight adalah alat berbasis dentang untuk profil waktu dan memori konsumsi instantiasi template dan untuk melakukan sesi debugging interaktif untuk mendapatkan introspeksi ke dalam proses instantiasi template".
Anda dapat menginstal Templight dengan memeriksa LLVM dan Dentang ( instruksi ) dan menerapkan templight patch di atasnya. Pengaturan default untuk LLVM dan Dentang adalah pada debug dan pernyataan, dan ini dapat memengaruhi waktu kompilasi Anda secara signifikan. Sepertinya Templight membutuhkan keduanya, jadi Anda harus menggunakan pengaturan default. Proses menginstal LLVM dan Dentang harus memakan waktu sekitar satu jam atau lebih.
Setelah menerapkan tambalan Anda dapat menggunakan yang templight++
terletak di folder build yang Anda tentukan saat instalasi untuk mengkompilasi kode Anda.
Pastikan itu templight++
ada di PATH Anda. Sekarang untuk mengkompilasi, tambahkan sakelar berikut ke CXXFLAGS
dalam Makefile Anda atau ke opsi baris perintah Anda:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Atau
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Setelah kompilasi selesai, Anda akan memiliki .trace.memory.pbf dan .trace.pbf yang dihasilkan di folder yang sama. Untuk memvisualisasikan jejak ini, Anda bisa menggunakan Templight Tools yang bisa mengonversikannya ke format lain. Ikuti instruksi ini untuk menginstal konversi cahaya. Kami biasanya menggunakan output callgrind. Anda juga dapat menggunakan output GraphViz jika proyek Anda kecil:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
File callgrind yang dihasilkan dapat dibuka menggunakan kcachegrind di mana Anda dapat melacak instantiasi yang paling memakan waktu / memori.
Mengurangi jumlah contoh template
Meskipun tidak ada solusi tepat untuk mengurangi jumlah contoh template, ada beberapa panduan yang dapat membantu:
Kelas refactor dengan lebih dari satu argumen templat
Misalnya, jika Anda memiliki kelas,
template <typename T, typename U>
struct foo { };
dan keduanya T
dan U
dapat memiliki 10 opsi yang berbeda, Anda telah meningkatkan kemungkinan contoh template kelas ini menjadi 100. Salah satu cara untuk menyelesaikan ini adalah dengan abstrak bagian umum dari kode ke kelas yang berbeda. Metode lain adalah dengan menggunakan inversi warisan (membalikkan hierarki kelas), tetapi pastikan bahwa tujuan desain Anda tidak terganggu sebelum menggunakan teknik ini.
Refactor kode non-templated ke unit terjemahan individual
Dengan menggunakan teknik ini, Anda dapat mengkompilasi bagian umum satu kali dan menautkannya dengan TU lainnya (unit terjemahan) nanti.
Gunakan instantiasi templat eksternal (sejak C ++ 11)
Jika Anda tahu semua instantiasi yang mungkin dari suatu kelas, Anda dapat menggunakan teknik ini untuk mengkompilasi semua kasus di unit terjemahan yang berbeda.
Misalnya, di:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Kita tahu bahwa kelas ini dapat memiliki tiga kemungkinan instance:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Masukkan yang di atas dalam unit terjemahan dan gunakan kata kunci extern dalam file header Anda, di bawah definisi kelas:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Teknik ini dapat menghemat waktu Anda jika Anda menyusun tes yang berbeda dengan serangkaian instantiasi yang umum.
CATATAN: MPICH2 mengabaikan instantiasi eksplisit pada titik ini dan selalu mengkompilasi kelas instantiated di semua unit kompilasi.
Gunakan bangunan kesatuan
Seluruh ide di balik pembangunan kesatuan adalah untuk memasukkan semua file .cc yang Anda gunakan dalam satu file dan mengkompilasi file itu hanya sekali. Dengan menggunakan metode ini, Anda dapat menghindari memulihkan bagian umum dari file yang berbeda dan jika proyek Anda menyertakan banyak file umum, Anda mungkin akan menghemat akses disk juga.
Sebagai contoh, mari kita asumsikan Anda memiliki tiga file foo1.cc
, foo2.cc
, foo3.cc
dan mereka semua termasuk tuple
dari STL . Anda dapat membuat foo-all.cc
yang terlihat seperti:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Anda mengkompilasi file ini hanya sekali dan berpotensi mengurangi instantiations umum di antara tiga file. Sulit untuk memprediksi secara umum apakah peningkatan itu signifikan atau tidak. Tetapi satu fakta yang jelas adalah bahwa Anda akan kehilangan paralelisme di build Anda (Anda tidak bisa lagi mengkompilasi ketiga file pada saat yang sama).
Lebih lanjut, jika ada dari file-file ini yang mengambil banyak memori, Anda mungkin benar-benar kehabisan memori sebelum kompilasi selesai. Pada beberapa kompiler, seperti GCC , ini mungkin ICE (Internal Compiler Error) kompiler Anda karena kekurangan memori. Jadi jangan gunakan teknik ini kecuali Anda tahu semua pro dan kontra.
Header yang dikompilasi sebelumnya
Precompiled header (PCHs) dapat menghemat banyak waktu dalam kompilasi dengan mengkompilasi file header Anda ke representasi perantara yang dikenali oleh kompiler. Untuk menghasilkan file header yang sudah dikompilasi, Anda hanya perlu mengkompilasi file header Anda dengan perintah kompilasi biasa. Misalnya, di GCC:
$ g++ YOUR_HEADER.hpp
Ini akan menghasilkan YOUR_HEADER.hpp.gch file
( .gch
adalah ekstensi untuk file PCH di GCC) di folder yang sama. Ini berarti bahwa jika Anda memasukkan YOUR_HEADER.hpp
dalam beberapa file lain, kompiler akan menggunakan Anda YOUR_HEADER.hpp.gch
daripada YOUR_HEADER.hpp
di folder yang sama sebelumnya.
Ada dua masalah dengan teknik ini:
- Anda harus memastikan bahwa file header yang dikompilasi stabil dan tidak akan berubah ( Anda selalu dapat mengubah makefile Anda )
- Anda hanya dapat memasukkan satu PCH per unit kompilasi (pada sebagian besar kompiler). Ini berarti bahwa jika Anda memiliki lebih dari satu file header untuk dikompilasi, Anda harus memasukkannya dalam satu file (misalnya,
all-my-headers.hpp
). Tetapi itu berarti Anda harus memasukkan file baru di semua tempat. Untungnya, GCC punya solusi untuk masalah ini. Gunakan -include
dan berikan file header yang baru. Anda dapat koma memisahkan file yang berbeda menggunakan teknik ini.
Sebagai contoh:
g++ foo.cc -include all-my-headers.hpp
Gunakan ruang nama yang tidak disebutkan namanya atau anonim
Ruang nama tanpa nama (alias ruang nama anonim) dapat mengurangi ukuran biner yang dihasilkan secara signifikan. Ruang nama yang tidak disebutkan namanya menggunakan tautan internal, artinya simbol yang dihasilkan dalam ruang nama tersebut tidak akan terlihat oleh TU lain (unit terjemahan atau kompilasi). Compiler biasanya menghasilkan nama unik untuk ruang nama yang tidak disebutkan namanya. Ini berarti bahwa jika Anda memiliki file foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
Dan Anda kebetulan memasukkan file ini dalam dua TU (dua file .cc dan kompilasi secara terpisah). Dua contoh templat foo tidak akan sama. Ini melanggar Aturan Satu Definisi (ODR). Untuk alasan yang sama, menggunakan ruang nama tanpa nama tidak disarankan dalam file header. Jangan ragu untuk menggunakannya di .cc
file Anda untuk menghindari simbol muncul di file biner Anda. Dalam beberapa kasus, mengubah semua detail internal untuk .cc
file menunjukkan pengurangan 10% dalam ukuran biner yang dihasilkan.
Mengubah opsi visibilitas
Dalam kompiler yang lebih baru Anda dapat memilih simbol Anda untuk terlihat atau tidak terlihat dalam Dynamic Shared Objects (DSOs). Idealnya, mengubah visibilitas dapat meningkatkan kinerja kompiler, tautan waktu optimasi (LTO), dan ukuran biner yang dihasilkan. Jika Anda melihat file header STL di GCC Anda dapat melihat bahwa itu digunakan secara luas. Untuk mengaktifkan pilihan visibilitas, Anda perlu mengubah kode Anda per fungsi, per kelas, per variabel dan yang lebih penting per kompiler.
Dengan bantuan visibilitas, Anda dapat menyembunyikan simbol yang Anda anggap pribadi dari objek yang dibagikan yang dihasilkan. Pada GCC Anda dapat mengontrol visibilitas simbol dengan melewatkan default atau disembunyikan ke -visibility
opsi kompiler Anda. Ini dalam beberapa hal mirip dengan namespace yang tidak disebutkan namanya tetapi dengan cara yang lebih rumit dan mengganggu.
Jika Anda ingin menentukan visibilitas per kasus, Anda harus menambahkan atribut berikut ke fungsi, variabel, dan kelas Anda:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Visibilitas default di GCC adalah default (publik), yang berarti bahwa jika Anda mengkompilasi metode di atas sebagai shared library ( -shared
), foo2
dan kelas foo3
tidak akan terlihat di TU lain ( foo1
dan foo4
akan terlihat). Jika Anda mengompilasinya -visibility=hidden
maka hanya foo1
akan terlihat. Bahkan foo4
akan disembunyikan.
Anda dapat membaca lebih lanjut tentang visibilitas di wiki GCC .