Saya telah menemukan regresi kinerja yang menarik dalam cuplikan C ++ kecil, ketika saya mengaktifkan C ++ 11:
#include <vector>
struct Item
{
int a;
int b;
};
int main()
{
const std::size_t num_items = 10000000;
std::vector<Item> container;
container.reserve(num_items);
for (std::size_t i = 0; i < num_items; ++i) {
container.push_back(Item());
}
return 0;
}
Dengan g ++ (GCC) 4.8.2 20131219 (prerelease) dan C ++ 03 saya mendapatkan:
milian:/tmp$ g++ -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
35.206824 task-clock # 0.988 CPUs utilized ( +- 1.23% )
4 context-switches # 0.116 K/sec ( +- 4.38% )
0 cpu-migrations # 0.006 K/sec ( +- 66.67% )
849 page-faults # 0.024 M/sec ( +- 6.02% )
95,693,808 cycles # 2.718 GHz ( +- 1.14% ) [49.72%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
95,282,359 instructions # 1.00 insns per cycle ( +- 0.65% ) [75.27%]
30,104,021 branches # 855.062 M/sec ( +- 0.87% ) [77.46%]
6,038 branch-misses # 0.02% of all branches ( +- 25.73% ) [75.53%]
0.035648729 seconds time elapsed ( +- 1.22% )
Dengan C ++ 11 diaktifkan di sisi lain, kinerja menurun secara signifikan:
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
86.485313 task-clock # 0.994 CPUs utilized ( +- 0.50% )
9 context-switches # 0.104 K/sec ( +- 1.66% )
2 cpu-migrations # 0.017 K/sec ( +- 26.76% )
798 page-faults # 0.009 M/sec ( +- 8.54% )
237,982,690 cycles # 2.752 GHz ( +- 0.41% ) [51.32%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
135,730,319 instructions # 0.57 insns per cycle ( +- 0.32% ) [75.77%]
30,880,156 branches # 357.057 M/sec ( +- 0.25% ) [75.76%]
4,188 branch-misses # 0.01% of all branches ( +- 7.59% ) [74.08%]
0.087016724 seconds time elapsed ( +- 0.50% )
Adakah yang bisa menjelaskan hal ini? Sejauh pengalaman saya adalah bahwa STL menjadi lebih cepat dengan mengaktifkan C ++ 11, esp. terima kasih untuk memindahkan semantik.
EDIT: Seperti yang disarankan, container.emplace_back();
sebagai gantinya menggunakan kinerja setara dengan versi C ++ 03. Bagaimana versi C ++ 03 dapat mencapai hal yang sama push_back
?
milian:/tmp$ g++ -std=c++11 -O3 main.cpp && perf stat -r 10 ./a.out
Performance counter stats for './a.out' (10 runs):
36.229348 task-clock # 0.988 CPUs utilized ( +- 0.81% )
4 context-switches # 0.116 K/sec ( +- 3.17% )
1 cpu-migrations # 0.017 K/sec ( +- 36.85% )
798 page-faults # 0.022 M/sec ( +- 8.54% )
94,488,818 cycles # 2.608 GHz ( +- 1.11% ) [50.44%]
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
94,851,411 instructions # 1.00 insns per cycle ( +- 0.98% ) [75.22%]
30,468,562 branches # 840.991 M/sec ( +- 1.07% ) [76.71%]
2,723 branch-misses # 0.01% of all branches ( +- 9.84% ) [74.81%]
0.036678068 seconds time elapsed ( +- 0.80% )
push_back(Item())
keemplace_back()
dalam C ++ 11 versi?Jawaban:
Saya dapat mereproduksi hasil Anda di komputer saya dengan opsi-opsi yang Anda tulis di posting Anda.
Namun, jika saya juga mengaktifkan optimasi waktu tautan (saya juga meneruskan
-flto
flag ke gcc 4.7.2), hasilnya sama:(Saya sedang menyusun kode asli Anda, dengan
container.push_back(Item());
)Adapun alasannya, orang perlu melihat kode perakitan yang dihasilkan (
g++ -std=c++11 -O3 -S regr.cpp
). Dalam mode C ++ 11 kode yang dihasilkan secara signifikan lebih berantakan daripada untuk mode C ++ 98 dan inlining fungsivoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
gagal dalam mode C ++ 11 dengan default
inline-limit
.Inline yang gagal ini memiliki efek domino. Bukan karena fungsi ini dipanggil (bahkan tidak disebut!) Tetapi karena kita harus siap: Jika dipanggil, argumen fungsi (
Item.a
danItem.b
) harus sudah ada di tempat yang tepat. Ini mengarah pada kode yang cukup berantakan.Berikut adalah bagian yang relevan dari kode yang dihasilkan untuk kasus di mana inlining berhasil :
Ini bagus dan ringkas untuk loop. Sekarang, mari kita bandingkan ini dengan kasus inline yang gagal :
Kode ini berantakan dan ada banyak hal yang terjadi dalam loop daripada dalam kasus sebelumnya. Sebelum fungsi
call
(baris terakhir ditampilkan), argumen harus ditempatkan dengan tepat:Meskipun ini tidak pernah benar-benar dieksekusi, loop mengatur hal-hal sebelumnya:
Ini mengarah pada kode yang berantakan. Jika tidak ada fungsi
call
karena inlining berhasil, kami hanya memiliki 2 instruksi pindahkan dalam loop dan tidak ada messing dengan%rsp
(stack pointer). Namun, jika inlining gagal, kami mendapatkan 6 gerakan dan kami banyak mengacaukannya%rsp
.Hanya untuk mendukung teori saya (perhatikan
-finline-limit
), keduanya dalam mode C ++ 11:Memang, jika kita meminta kompiler untuk mencoba sedikit lebih keras untuk menyejajarkan fungsi itu, perbedaan kinerja hilang.
Jadi apa yang bisa diambil dari cerita ini? Inline yang gagal dapat menghabiskan banyak biaya dan Anda harus menggunakan sepenuhnya kemampuan kompiler: Saya hanya dapat merekomendasikan optimasi waktu tautan. Ini memberikan peningkatan kinerja yang signifikan untuk program saya (hingga 2,5x) dan semua yang perlu saya lakukan adalah melewati
-flto
bendera. Itu cukup bagus! ;)Namun, saya tidak merekomendasikan mencemari kode Anda dengan kata kunci sebaris; biarkan kompiler memutuskan apa yang harus dilakukan. (Pengoptimal diizinkan untuk memperlakukan kata kunci sebaris sebagai ruang kosong.)
Pertanyaan bagus, +1!
sumber
inline
tidak ada hubungannya dengan fungsi inlining; itu berarti "didefinisikan inline" bukan "tolong sebaris ini". Jika Anda ingin meminta inlining, gunakan__attribute__((always_inline))
atau yang serupa.inline
juga merupakan permintaan terhadap kompiler yang Anda ingin fungsi untuk diuraikan dan misalnya Intel C ++ Compiler digunakan untuk memberikan peringatan kinerja jika tidak memenuhi permintaan Anda. (Saya belum memeriksa icc baru-baru ini jika masih ada.) Sayangnya, saya telah melihat orang-orang yang membobol kode mereka denganinline
dan menunggu keajaiban terjadi. Saya tidak akan menggunakan__attribute__((always_inline))
; kemungkinan pengembang kompiler lebih tahu apa yang harus inline dan apa yang tidak. (Terlepas dari contoh tandingan di sini.)inline
Specifier menunjukkan pada implementasi bahwa substitusi inline dari fungsi fungsi pada titik panggilan akan lebih disukai daripada mekanisme fungsi panggilan yang biasa." (§7.1.2.2) Namun, implementasi tidak diperlukan untuk melakukan optimasi itu, karena sebagian besar kebetulan bahwainline
fungsi sering terjadi menjadi kandidat yang baik untuk digariskan. Jadi lebih baik untuk eksplisit dan menggunakan pragma kompiler.