Czy użycie „auto” C ++ 11 może poprawić wydajność?

230

Rozumiem, dlaczego autotyp w C ++ 11 poprawia poprawność i łatwość konserwacji. Czytałem, że może to również poprawić wydajność ( Prawie zawsze auto autorstwa Herb Sutter), ale brakuje mi dobrego wyjaśnienia.

  • Jak autopoprawić wydajność?
  • Czy ktoś może podać przykład?
DaBrain
źródło
5
Zobacz herbutter.com/2013/06/13/…, który mówi o unikaniu przypadkowych niejawnych konwersji, np. Z gadżetu na widżet. To nie jest powszechny problem.
Jonathan Wakely,
42
Czy akceptujesz „zmniejsza prawdopodobieństwo nieumyślnego pesymalizowania” jako poprawę wydajności?
5gon12eder
1
Wydajność czyszczenia kodu może tylko w przyszłości
Croll,
Potrzebujemy krótkiej odpowiedzi: Nie, jeśli jesteś dobry. Może zapobiec błędom „noobish”. C ++ ma krzywą uczenia się, która zabija tych, którzy mimo wszystko tego nie robią.
Alec Teal,

Odpowiedzi:

309

automoże poprawić wydajność, unikając cichych niejawnych konwersji . Przykład, który uważam za przekonujący, jest następujący.

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Widzisz błąd? Oto my, myśląc, że elegancko bierzemy każdy element na mapie przez odniesienie do stałej i używamy nowego wyrażenia zakresu dla wyrażenia naszej intencji, ale w rzeczywistości kopiujemy każdy element. To dlatego, że std::map<Key, Val>::value_typejest std::pair<const Key, Val>, nie std::pair<Key, Val>. Zatem gdy (domyślnie) mamy:

std::pair<Key, Val> const& item = *iter;

Zamiast odwoływać się do istniejącego obiektu i pozostawić go przy tym, musimy wykonać konwersję typu. Możesz wziąć stałe odniesienie do obiektu (lub tymczasowego) innego typu, o ile dostępna jest niejawna konwersja, np .:

int const& i = 2.0; // perfectly OK

Konwersja typu jest dozwoloną niejawną konwersją z tego samego powodu, dla którego można przekonwertować a const Keyna Key, ale musimy skonstruować tymczasowy nowy typ, aby na to pozwolić. Zatem skutecznie nasza pętla:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Oczywiście, nie ma w rzeczywistości żadnego __tmpprzedmiotu, jest tylko dla ilustracji, w rzeczywistości nienazwany tymczasowy jest po prostu związany z itemjego życiem).

Zmieniam się na:

for (auto const& item : m) {
    // do stuff
}

właśnie zaoszczędziliśmy nam mnóstwo kopii - teraz typ odniesienia odpowiada typowi inicjalizującemu, więc nie jest potrzebna tymczasowa ani konwersja, możemy po prostu zrobić bezpośrednie odniesienie.

Barry
źródło
19
@Barry Czy możesz wyjaśnić, dlaczego kompilator chętnie wykonuje kopie zamiast narzekać na próbę potraktowania go std::pair<const Key, Val> const &jako std::pair<Key, Val> const &? Nowy w C ++ 11, nie jestem pewien, w jaki sposób zasięg i autogra.
Agop
@Barry Dzięki za wyjaśnienie. Właśnie tego mi brakowało - z jakiegoś powodu myślałem, że nie możesz mieć stałego odniesienia do tymczasowości. Ale oczywiście możesz - przestanie istnieć pod koniec swojego zakresu.
Agop
@barry Rozumiem, ale problem polega na tym, że nie ma odpowiedzi, która obejmowałaby wszystkie powody, dla których warto autozwiększyć wydajność. Więc napiszę to własnymi słowami poniżej.
Jak - Adam Nevraumont,
38
Nadal nie sądzę, że jest to dowód, że „ autopoprawia wydajność”. To tylko przykład, który „ autopomaga zapobiec błędom programisty, które niszczą wydajność”. Uważam, że istnieje między nimi subtelna, ale ważna różnica. Nadal +1.
Wyścigi lekkości na orbicie
70

Ponieważ autodedukuje typ wyrażenia inicjującego, konwersja typu nie jest wymagana. W połączeniu z algorytmami szablonowymi oznacza to, że możesz uzyskać bardziej bezpośrednie obliczenia, niż gdybyś sam tworzył typ - szczególnie, gdy masz do czynienia z wyrażeniami, których typu nie możesz nazwać!

Typowy przykład pochodzi z (ab) przy użyciu std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

Za pomocą cmp2i cmp3cały algorytm może wstawić wywołanie porównania, podczas gdy jeśli zbudujesz std::functionobiekt, nie tylko nie można wstawić wywołania, ale musisz również przejść przez wyszukiwanie polimorficzne w wymazanym typem wnętrzu opakowania funkcji.

Innym wariantem tego tematu jest to, że możesz powiedzieć:

auto && f = MakeAThing();

Jest to zawsze odwołanie powiązane z wartością wyrażenia wywołania funkcji i nigdy nie konstruuje żadnych dodatkowych obiektów. Jeśli nie znasz typu zwracanej wartości, możesz zostać zmuszony do skonstruowania nowego obiektu (być może tymczasowego) za pomocą czegoś podobnego T && f = MakeAThing(). (Ponadto auto &&działa nawet wtedy, gdy typ zwracany nie jest ruchomy, a zwracana wartość jest wartością wstępną.)

Kerrek SB
źródło
Jest to więc powód „unikania wymazywania typu” auto. Twój drugi wariant to „unikaj przypadkowych kopii”, ale wymaga upiększenia; dlaczego autodajesz szybkość po prostu wpisując tam typ? (Myślę, że odpowiedź brzmi: „źle wpisujesz ten typ, a on po cichu przekształca”). Co sprawia, że ​​jest to mniej dobrze wyjaśniony przykład odpowiedzi Barry'ego, prawda? Tzn. Istnieją dwa podstawowe przypadki: auto, aby uniknąć kasowania typu, i auto, aby uniknąć cichych błędów typu, które przypadkowo się przekonwertowały, a oba mają koszt działania.
Yakk - Adam Nevraumont
2
„nie tylko nie można wprowadzić połączenia” - dlaczego tak jest? Czy to znaczy, że w zasadzie coś uniemożliwia wywołanie jest devirtualized po analizę przepływu danych czy odpowiednich specjalności std::bind, std::functiona std::stable_partitionwszystkie zostały inlined? Czy po prostu, że w praktyce żaden kompilator C ++ nie będzie wystarczająco agresywny, aby rozwiązać problem?
Steve Jessop,
@SteveJessop: Przeważnie ten drugi - po przejściu przez std::functionkonstruktora przejrzenie rzeczywistego wywołania będzie bardzo skomplikowane, szczególnie przy optymalizacji małych funkcji (więc tak naprawdę nie chcesz dewirtualizacji). Oczywiście w zasadzie wszystko jest jak gdyby ...
Kerrek SB
41

Istnieją dwie kategorie.

automożna uniknąć usunięcia typu. Istnieją typy nienazwane (jak lambdas) i typy prawie nienazwane (jak wynik std::bindlub inne podobne do szablonu wyrażenia rzeczy).

Bez tego automusisz w końcu skasować dane do czegoś podobnego std::function. Usuwanie typu ma koszty.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1ma narzut kasowania typu - możliwy przydział sterty, trudność w jego wstawianiu oraz narzut wywołania tablicy funkcji wirtualnych. task2nie ma Jagnięta potrzebują automatycznego lub innego rodzaju odliczenia typu, aby przechowywać bez usuwania typu; inne typy mogą być tak złożone, że potrzebują go tylko w praktyce.

Po drugie, możesz pomylić typy. W niektórych przypadkach niewłaściwy typ będzie działał pozornie idealnie, ale spowoduje kopiowanie.

Foo const& f = expression();

skompiluje, jeśli expression()zwraca, Bar const&a Barnawet Bar&, gdzie Foomożna zbudować Bar. Tymczasowy Foozostanie utworzony, a następnie związany f, a jego żywotność zostanie przedłużona, aż fzniknie.

Programista mógł mieć na myśli Bar const& fi nie zamierzać tam tworzyć kopii, ale kopia jest tworzona niezależnie.

Najczęstszym przykładem jest typ *std::map<A,B>::const_iterator, który std::pair<A const, B> const&nie jest std::pair<A,B> const&, ale błąd jest kategorią błędów, które dyskretnie obniżają wydajność. Możesz zbudować std::pair<A, B>z std::pair<const A, B>. (Klucz na mapie to const, ponieważ edytowanie jej to zły pomysł)

Zarówno @Barry, jak i @KrekrekSB najpierw zilustrowały te dwie zasady w swoich odpowiedziach. Jest to po prostu próba podkreślenia dwóch problemów w jednej odpowiedzi, przy użyciu sformułowania, które ma na celu problem, a nie koncentrowanie się na przykładach.

Jak - Adam Nevraumont
źródło
9

Istniejące trzy odpowiedzi podają przykłady, w których użycie autopomaga „zmniejszyć prawdopodobieństwo niezamierzonego pesymalizowania” skutecznie, sprawiając, że „poprawia wydajność”.

Moneta ma drugą stronę. Używanie autoz obiektami, które mają operatory, które nie zwracają podstawowego obiektu, może spowodować niepoprawny (nadal możliwy do skompilowania i uruchomienia) kod. Na przykład to pytanie dotyczy tego, w jaki sposób użycie autodało różne (niepoprawne) wyniki przy użyciu biblioteki Eigen, tj . Następujące linie

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

spowodowało inną wydajność. Wprawdzie wynika to głównie z leniwej oceny Eigensa, ale ten kod jest / powinien być przezroczysty dla użytkownika (biblioteki).

Chociaż nie ma to większego wpływu na wydajność, użycie w autocelu uniknięcia niezamierzonej pesymizacji może zostać zaklasyfikowane jako przedwczesna optymalizacja, a przynajmniej źle;

Avi Ginsburg
źródło
1
Dodano przeciwne pytanie: stackoverflow.com/questions/38415831/…
Leon