Znalazłem interesującą regresję wydajności w małym fragmencie kodu C ++, gdy włączę 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;
}
Z g ++ (GCC) 4.8.2 20131219 (wersja wstępna) i C ++ 03 otrzymuję:
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% )
Z drugiej strony przy włączonym C ++ 11 wydajność znacznie się obniża:
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% )
Czy ktoś może to wyjaśnić? Do tej pory moje doświadczenie było takie, że STL działa szybciej, włączając C ++ 11, szczególnie. dzięki semantyce ruchów.
EDYCJA: Jak sugerowano, użycie container.emplace_back();
zamiast tego wydajność jest na równi z wersją C ++ 03. W jaki sposób wersja C ++ 03 może osiągnąć to samo 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())
naemplace_back()
wersję C ++ 11?Odpowiedzi:
Mogę odtworzyć twoje wyniki na moim komputerze dzięki tym opcjom, które piszesz w swoim poście.
Jeśli jednak włączę również optymalizację czasu łącza (przekazuję
-flto
flagę do gcc 4.7.2), wyniki są identyczne:(Kompiluję twój oryginalny kod, z
container.push_back(Item());
)Jeśli chodzi o powody, należy spojrzeć na wygenerowany kod zestawu (
g++ -std=c++11 -O3 -S regr.cpp
). W trybie C ++ 11 generowany kod jest znacznie bardziej zaśmiecony niż w trybie C ++ 98, a wstawianie funkcjivoid std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
kończy się niepowodzeniem w trybie C ++ 11 z ustawieniem domyślnym
inline-limit
.To nieudane wstawianie ma efekt domina. Nie dlatego, że ta funkcja jest wywoływana (nawet nie jest wywoływana!), Ale dlatego, że musimy być przygotowani: Jeśli zostanie wywołana, funkcja argumentuje (
Item.a
iItem.b
) musi już znajdować się we właściwym miejscu. Prowadzi to do dość niechlujnego kodu.Oto odpowiednia część wygenerowanego kodu dla przypadku, w którym wstawianie się powiodło :
To jest ładne i kompaktowe dla pętli. Porównajmy teraz to z nieudaną wstawką :
Ten kod jest zaśmiecony i w pętli dzieje się o wiele więcej niż w poprzednim przypadku. Przed funkcją
call
(pokazany ostatni wiersz) argumenty muszą być odpowiednio umieszczone:Mimo że nigdy tak naprawdę nie jest to wykonywane, pętla porządkuje rzeczy wcześniej:
To prowadzi do niechlujnego kodu. Jeśli nie ma żadnej funkcji,
call
ponieważ inlinizacja się powiodła, mamy tylko 2 instrukcje ruchu w pętli i nie ma bałaganu%rsp
(wskaźnik stosu). Jeśli jednak inlining się nie powiedzie, dostaniemy 6 ruchów i bardzo zepsujemy%rsp
.Aby uzasadnić moją teorię (zwróć uwagę
-finline-limit
), oba w trybie C ++ 11:Rzeczywiście, jeśli poprosimy kompilator o nieco trudniejsze wprowadzenie tej funkcji, różnica w wydajności zniknie.
Więc jaki jest sens tej historii? Te nieudane wstawki mogą Cię dużo kosztować i powinieneś w pełni wykorzystać możliwości kompilatora: mogę jedynie zalecić optymalizację czasu łącza. Dało to znaczący wzrost wydajności moim programom (do 2,5x) i wszystko, co musiałem zrobić, to przejść
-flto
flagę. To całkiem niezła okazja! ;)Nie polecam jednak usuwania twojego kodu słowem kluczowym inline; pozwól kompilatorowi zdecydować, co robić. (Optymalizator może mimo to traktować słowo kluczowe w wierszu jako spację).
Świetne pytanie, +1!
źródło
inline
nie ma nic wspólnego z wstawianiem funkcji; oznacza „zdefiniowane wstawianie”, a nie „proszę wstawić to”. Jeśli chcesz poprosić o wstawienie, użycie__attribute__((always_inline))
lub podobne.inline
jest także żądaniem skierowanym do kompilatora, aby funkcja była wstawiana, na przykład kompilator Intel C ++ używał ostrzeżeń o wydajności, jeśli nie spełnił on twojego żądania. (Ostatnio nie sprawdzałem ICC, jeśli nadal działa). Niestety, widziałem, jak ludzie niszczyli swój kodinline
i czekali na cud. Nie użyłbym__attribute__((always_inline))
; istnieje szansa, że twórcy kompilatora lepiej wiedzą, co wstawiać, a czego nie. (Pomimo kontrprzykładu tutaj.)inline
wskazuje implementacji, że podstawienie funkcji funkcji w punkcie wywołania ma być preferowane w stosunku do zwykłego mechanizmu wywoływania funkcji”. (§7.1.2.2) Jednak implementacje nie są wymagane do wykonania tej optymalizacji, ponieważ w dużej mierze zbieg okoliczności sprawia, żeinline
funkcje często są dobrymi kandydatami do wprowadzania. Lepiej więc być wyraźnym i użyć pragmy kompilatora.