Dlaczego kod miałby aktywnie zapobiegać optymalizacji wywołań końcowych?

81

Tytuł pytania może być trochę dziwny, ale chodzi o to, że o ile wiem, nic nie przemawia przeciwko optymalizacji wywołań ogonowych. Jednak podczas przeglądania projektów open source natknąłem się już na kilka funkcji, które aktywnie próbują powstrzymać kompilator przed optymalizacją wywołań ogonowych, na przykład implementację CFRunLoopRef, która jest pełna takich hacków . Na przykład:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (func) {
        func(observer, activity, info);
    }
    getpid(); // thwart tail-call optimization
}

Bardzo chciałbym wiedzieć, dlaczego wydaje się to tak ważne, i czy są jakieś przypadki, w których ja jako normalny programista też powinienem pamiętać? Na przykład. czy są typowe pułapki związane z optymalizacją wywołań ogonowych?

JustSid
źródło
10
Jedną z możliwych pułapek może być to, że aplikacja działa płynnie na kilku platformach, a następnie nagle przestaje działać po skompilowaniu za pomocą kompilatora, który nie obsługuje optymalizacji wywołań końcowych. Pamiętaj, że ta optymalizacja może nie tylko zwiększyć wydajność, ale także zapobiec błędom w czasie wykonywania (przepełnieniom stosu).
Niklas B.
5
@NiklasB. Ale czy to nie jest powód, aby nie próbować go wyłączać?
JustSid
4
Wywołanie systemowe może być pewnym sposobem na obniżenie całkowitego kosztu posiadania, ale także dość kosztownym.
Fred Foo
39
To świetny moment, w którym można się nauczyć właściwego komentowania. +1 za częściowe wyjaśnienie, dlaczego ta linia istnieje (aby zapobiec optymalizacji wywołań ogonowych), -100 za niewyjaśnienie, dlaczego optymalizacja wywołań końcowych musiała zostać wyłączona w pierwszej kolejności ...
Mark Sowul
16
Ponieważ wartość of getpid()nie jest używana, czy nie można jej usunąć przez poinformowany optymalizator (ponieważ getpidjest to funkcja, o której wiadomo, że nie ma skutków ubocznych), a zatem zezwala kompilatorowi na wykonanie optymalizacji wywołań końcowych? Wydaje się, że to naprawdę delikatny mechanizm.
luiscubal

Odpowiedzi:

83

Domyślam się, że ma to na celu zapewnienie, że __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__znajduje się w śladzie stosu do celów debugowania. To ma__attribute__((no inline)) co potwierdza ten pomysł.

Jeśli zauważysz, ta funkcja po prostu przechodzi i odbija się od innej funkcji, więc jest to forma trampoliny, o której myślę, że istnieje z tak rozwlekłą nazwą, aby pomóc w debugowaniu. Byłoby to szczególnie pomocne, biorąc pod uwagę, że funkcja wywołuje wskaźnik funkcji, który został zarejestrowany z innego miejsca i dlatego ta funkcja może nie mieć dostępnych symboli debugowania.

Zwróć także uwagę na inne podobnie nazwane funkcje, które robią podobne rzeczy - naprawdę wygląda na to, że pomaga w zobaczeniu, co się stało ze śladu wstecznego. Należy pamiętać, że jest to podstawowy kod systemu Mac OS X, który będzie widoczny w raportach o awariach i przykładowych raportach dotyczących procesów.

mattjgalloway
źródło
Tak, jest to zgodne z __attribute__((noinline)). Myślę, że jesteś tutaj na miejscu.
Niklas B.
Tak, rzeczywiście ma sens. Ale jeśli spojrzysz, skąd te funkcje są wywoływane, zobaczysz, że są one zawsze wywoływane tylko z jednej funkcji, na przykład moja przykładowa funkcja jest wywoływana tylko z __CFRunLoopDoObserversktórej na pewno pojawia się w śladzie stosu ...
JustSid
1
Jasne, ale wydaje mi się, że jest to kolejny znacznik dokładnie określający, gdzie jest uruchamiane wywołanie zwrotne obserwatora / blok / itp.
mattjgalloway
2
Myślę, że to najlepsza odpowiedź. +1
R .. GitHub ZATRZYMAJ SIĘ NA LODZIE
@R .. Mogę zaakceptować tylko jedną odpowiedź, a Andrew White wymienił również inne przypadki, w których optymalizacja wywołań ogonowych może nie być pożądana. Pamiętaj, nie zapytałem, dlaczego funkcja to zrobiła, ale dlaczego może nie być w ogóle pożądana i podałem funkcję jako przykład ze świata rzeczywistego.
JustSid
34

To tylko przypuszczenie, ale być może w celu uniknięcia nieskończonej pętli w porównaniu z bombardowaniem z błędem przepełnienia stosu.

Ponieważ omawiana metoda nie umieszcza niczego na stosie, wydaje się możliwe, że optymalizacja rekurencji wywołań ogonowych wygeneruje kod, który wprowadziłby nieskończoną pętlę w przeciwieństwie do niezoptymalizowanego kodu, który umieściłby adres zwrotny na stosie które w końcu uległyby przepełnieniu w przypadku niewłaściwego użycia.

Jedyna inna myśl, jaką mam, jest związana z zachowaniem wywołań na stosie do debugowania i drukowania śledzenia stosu.

Andrew White
źródło
8
Myślę, że wyjaśnienie śledzenia stosu / debugowania jest znacznie bardziej prawdopodobne (i miałem zamiar to opublikować). Nieskończona pętla nie jest gorsza niż awaria, ponieważ użytkownik może wymusić zamknięcie aplikacji. To również wyjaśniałoby noinline.
ughoavgfhw
3
@ughoavgfhw: być może, ale kiedy zaczynasz zajmować się wątkami i tym podobnymi, nieskończone pętle są naprawdę trudne do wyśledzenia. Zawsze uważałem, że niewłaściwe użycie powinno wywołać wyjątek. Ponieważ nigdy nie musiałem tego robić, to wciąż tylko przypuszczenie.
Andrew White
1
synchroniczność, coś w rodzaju ... Właśnie napotkałem zły błąd, który sprawiał, że aplikacja otwierała nowe okna. To sprawia, że ​​myślę, że gdyby aplikacja uległa awarii przed próbą nasycenia "sterty" (mojej pamięci) i dławienia X, nie musiałbym przełączać się na terminal, aby nagle zabić szaloną aplikację (ponieważ X zaczął się wkrótce stawać nie odpowiada). Więc może byłby to powód, aby preferować podejście „szybkie niepowodzenie”, które może wiązać się z przepełnieniem stosu i brakiem optymalizacji…? a może to po prostu inna sprawa ...!
ShinTakezou
2
@AndrewWhite Hmm, całkowicie uwielbiam nieskończone pętle - nie mogę wymyślić ani jednej rzeczy, która byłaby łatwiejsza do debugowania, mam na myśli, że możesz po prostu podłączyć debuger i uzyskać dokładną pozycję i stan problemu bez zgadywania. Ale jeśli chcesz uzyskać ślady stosu od użytkowników, zgadzam się, że nieskończona pętla jest problematyczna, więc wydaje się to logiczne - w twoim dzienniku pojawi się błąd, nieskończona pętla nie.
Voo
1
Zakłada się, że funkcja jest rekursywna w pierwszej kolejności - ale tak nie jest; ani bezpośrednio, ani pośrednio (patrząc na kontekst, z którego pochodzi funkcja). Na początku poczyniłem to samo błędne założenie.
Konrad Rudolph
21

Jednym z potencjalnych powodów jest ułatwienie debugowania i profilowania (przy całkowitym koszcie posiadania, nadrzędna ramka stosu znika, co utrudnia zrozumienie śladów stosu).

NPE
źródło
2
ułatwianie profilowania kosztem spowolnienia programu jest jednak trochę dziwne. Ma to tyle sensu, co rozcieńczenie oleju przed pomiarem, jak daleko może jechać Twój samochód: x
Matthieu M.
1
@MatthieuM .: Taka rzecz nie miałaby sensu, gdyby dodane wywołanie zostało wykonane miliony razy w pętli, ale jeśli jest wykonywane kilkaset razy na sekundę lub mniej, lepiej zostawić je w rzeczywistym systemie i być w stanie zbadać, jak zachowuje się prawdziwy system, niż go wyjąć i ryzykować, że takie usunięcie spowoduje subtelną, ale ważną zmianę w zachowaniu systemu.
supercat
@MatthieuM. Jeśli rozcieńczenie oleju jest w ogóle warunkiem wstępnym jakiegokolwiek pomiaru, to w rzeczywistości ma to sens.
Dmitrij Grigoriew
@DmitryGrigoryev: Nie, tak nie jest. Żadna miara nie jest irytująca, ale niewłaściwa miara jest od bezużytecznej do niebezpiecznej (w zależności od tego, jak bardzo jej ufasz). Kontynuując analogię z olejem: jeśli spowalnia cię, możesz otrzymać pomiary wskazujące, że waga jest ważniejsza niż aerodynamika, a tym samym usunąć wagę i pogorszyć aerodynamikę, aby zoptymalizować to, co zmierzyłeś ... jednak w przypadku prawdziwego oleju, kiedy jedziesz szybciej, okazuje się, że ważniejsza była aerodynamika, a twoja „poprawa” jest gorsza niż nic nie robienie!
Matthieu M.
@MatthieuM. Czy znasz zasadę nieoznaczoności? Każdy pomiar jest do pewnego stopnia błędny, ponieważ nie ma możliwości zmierzenia czegokolwiek bez interakcji z mierzonym obiektem. Tak więc, nawet jeśli nie zmienisz oleju w swoim przykładzie, oprzyrządowanie samochodu i tak zmieni aerodynamikę.
Dmitrij Grigoriew