Czy wyjątki są szeroko stosowane w projektowaniu silnika gry, czy bardziej wskazane jest stosowanie czystych instrukcji if? Na przykład z wyjątkami:
try {
m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
// some info about error
}
i jeśli:
m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
// show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
// show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
// show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
// show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
// show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
// show error
}
Słyszałem, że wyjątki są nieco wolniejsze niż ifs i nie powinienem mieszać wyjątków metodą sprawdzania if. Jednak z wyjątkami mam bardziej czytelny kod niż z mnóstwem ifs po każdej metodzie initialize () lub czymś podobnym, chociaż moim zdaniem są one czasem zbyt ciężkie, by zmieścić tylko jedną metodę. Czy nadają się do tworzenia gier, czy lepiej trzymać się prostych instrukcji if?
Odpowiedzi:
Krótka odpowiedź: przeczytaj: Rozsądna obsługa błędów 1 , Rozsądna obsługa błędów 2 i Rozsądna obsługa błędów 3 autorstwa Niklas Frykholm. W rzeczywistości czytaj wszystkie artykuły na tym blogu, gdy jesteś przy nim. Nie twierdzę, że zgadzam się ze wszystkim, ale większość to złoto.
Nie używaj wyjątków. Istnieje wiele powodów. Wymienię najważniejsze.
Mogą być rzeczywiście wolniejsze, choć jest to nieco zminimalizowane w nowszych kompilatorach. Niektóre kompilatory obsługują „wyjątki narzutu zerowego” dla ścieżek kodu, które w rzeczywistości nie wyzwalają wyjątku (choć to trochę kłamstwo, ponieważ wciąż istnieją dodatkowe dane, których wymaga obsługa wyjątków, zwiększając rozmiar pliku wykonywalnego / dll). Rezultatem końcowym jest to, że tak, stosowanie wyjątków jest wolniejsze i na każdej ścieżce kodu krytycznej pod względem wydajności należy absolutnie ich unikać. W zależności od kompilatora włączenie ich w ogóle może zwiększyć obciążenie. Zawsze zawsze zwiększają rozmiar kodu, w wielu przypadkach dość znacząco, co może poważnie wpłynąć na wydajność dzisiejszego sprzętu.
Wyjątki sprawiają, że kod jest znacznie bardziej kruchy. Istnieje niesławna grafika (której niestety nie mogę teraz znaleźć), która po prostu pokazuje na wykresie trudność pisania kodu bezpiecznego dla wyjątku w porównaniu do kodu niebezpiecznego dla wyjątku, a ten pierwszy jest znacznie większym paskiem. Jest po prostu wiele małych błędów z wyjątkami i wiele sposobów pisania kodu, który wygląda na wyjątkowo bezpieczny, ale tak naprawdę nie jest. Nawet cały komitet C ++ 11 wygłupił się i zapomniał dodać ważne funkcje pomocnicze do prawidłowego używania std :: unique_ptr, a nawet przy tych funkcjach pomocniczych potrzeba więcej pisania niż ich użycie, a większość programistów wygrała ” nawet zdają sobie sprawę, co jest nie tak, jeśli nie.
W szczególności dla branży gier, kompilatory / środowiska wykonawcze dostarczone przez niektórych konsol wprost nie obsługują wyjątków w pełni, a nawet nie obsługują ich wcale. Jeśli Twój kod korzysta z wyjątków, być może nadal w tym dniu musisz przepisać fragmenty kodu, aby przenieść go na nowe platformy. (Nie jestem pewien, czy zmieni się to w ciągu 7 lat od wydania konsol; po prostu nie używamy wyjątków, wyłączamy je nawet w ustawieniach kompilatora, więc nie wiem, czy ktoś, z kim rozmawiam, ostatnio sprawdził).
Ogólna linia myślenia jest dość jasna: należy stosować wyjątki w wyjątkowych okolicznościach. Użyj ich, gdy twój program wpadnie w „Nie mam pojęcia, co robić, może mam nadzieję, że zrobi to ktoś inny, więc rzucę wyjątek i zobaczę, co się stanie”. Używaj ich, gdy żadna inna opcja nie ma sensu. Używaj ich, gdy nie przejmujesz się, że przypadkowo wycieknie trochę pamięci lub nie wyczyścisz zasobu, ponieważ pomieszałeś użycie odpowiednich inteligentnych uchwytów. We wszystkich innych przypadkach nie używaj ich.
Jeśli chodzi o kod jak twój przykład, masz kilka innych sposobów rozwiązania problemu. Jednym z bardziej niezawodnych - choć niekoniecznie najbardziej idealnych w twoim prostym przykładzie - jest skłanianie się ku typom błędów monadycznych. Oznacza to, że createText () może zwrócić niestandardowy typ uchwytu zamiast liczby całkowitej. Ten typ uchwytu ma akcesoria do aktualizacji lub kontrolowania tekstu. Jeśli uchwyt zostanie przełączony w stan błędu (ponieważ nie powiodło się wywołanie metody createText ()), dalsze wywołania uchwytu po prostu zakończą się niepowodzeniem. Możesz także wysłać zapytanie do uchwytu, aby sprawdzić, czy jest on błędny, a jeśli tak, jaki był ostatni błąd. To podejście ma więcej kosztów ogólnych niż inne opcje, ale jest dość solidne. Użyj go w przypadkach, gdy musisz wykonać długi ciąg operacji w kontekście, w którym jakakolwiek pojedyncza operacja może się nie powieść w produkcji, ale gdzie nie możesz / nie możesz / wygrałeś ”
Alternatywą dla implementacji monadycznej obsługi błędów jest zamiast używania niestandardowych obiektów uchwytów, aby metody w obiekcie kontekstowym w sposób łagodny radziły sobie z nieprawidłowymi identyfikatorami uchwytów. Na przykład, jeśli createText () zwraca -1, gdy zawiedzie, to wszelkie inne wywołania m_statistics, które przyjmują jeden z tych uchwytów, powinny z wdziękiem wyjść, jeśli -1 zostanie przekazane.
Możesz również umieścić błąd drukowania w funkcji, która faktycznie nie działa. W twoim przykładzie, createText () prawdopodobnie ma znacznie więcej informacji o tym, co poszło źle, więc będzie mógł zrzucić bardziej znaczący błąd do dziennika. W tym przypadku nie ma większych korzyści z przekazywania obsługi błędów / drukowania do dzwoniących. Zrób to, gdy dzwoniący będą musieli dostosować obsługę (lub użyć wstrzykiwania zależności). Pamiętaj, że posiadanie konsoli w grze, która może się pojawiać po zarejestrowaniu błędu, jest dobrym pomysłem i również tutaj pomaga.
Najlepszą opcją (już przedstawioną w linkowanej serii artykułów powyżej) dla wywołań, których nie spodziewasz się w żadnym zdrowym środowisku - jak zwykły akt tworzenia obiektów blob tekstowych dla systemu statystyk - jest po prostu funkcja to się nie powiodło (w twoim przykładzie createText) się przerywa. Możesz być całkiem pewny, że createText () nie zawiedzie w produkcji, chyba że coś zostanie całkowicie wyeliminowane (np. Użytkownik usunął pliki danych czcionek lub z jakiegoś powodu ma tylko 256 MB pamięci itp.). W wielu z tych przypadków nic dobrego nie można zrobić, gdy dojdzie do awarii. Brak pamięci? Możesz nawet nie być w stanie dokonać alokacji niezbędnej do utworzenia ładnego panelu GUI, aby pokazać użytkownikowi błąd OOM. Brakuje czcionek? Utrudnia wyświetlanie użytkownikowi błędów. Cokolwiek jest nie tak,
Samo zawieszanie się jest w porządku, o ile (a) logujesz błąd do pliku dziennika i (b) robisz to tylko w przypadku błędów, które nie są spowodowane zwykłymi działaniami użytkownika.
Nie powiedziałbym wcale tego samego w przypadku wielu aplikacji serwerowych, w których dostępność jest krytyczna, a monitorowanie watchdoga nie jest wystarczające, ale jest to zupełnie inne niż tworzenie klienta gry . Chciałbym również zdecydowanie unikać używania C / C ++, ponieważ narzędzia do obsługi wyjątków innych języków nie gryzą podobnie jak C ++, ponieważ są to środowiska zarządzane i nie mają wszystkich problemów związanych z bezpieczeństwem wyjątków, jakie ma C ++. Wszelkie problemy z wydajnością są również łagodzone, ponieważ serwery koncentrują się bardziej na równoległości i przepustowości niż na minimalnych opóźnieniach, takich jak klienci gier. Na przykład nawet serwery gier akcji dla strzelców i tym podobne mogą działać całkiem dobrze, gdy są napisane w języku C #, ponieważ rzadko przekraczają granice sprzętu, jak to zwykle robią klienci FPS.
źródło
warn_unused_result
funkcja w niektórych kompilatorach, co pozwala przechwycić nieobsługiwany kod błędu.Stara mądrość samego Donalda Knutha to:
Wydrukuj to jako duży plakat i powiesz go wszędzie, gdzie wykonujesz poważne programowanie.
Więc nawet jeśli try / catch jest trochę wolniejszy, użyłbym go domyślnie:
Kod musi być jak najbardziej czytelny i zrozumiały. Państwo może napisać kod raz, ale będzie ją przeczytać wiele razy więcej do debugowania tego kodu, do debugowania inny kod, dla zrozumienia, w celu poprawy ...
Poprawność w stosunku do wydajności. Prawidłowe wykonywanie czynności if / else nie jest trywialne. W twoim przykładzie jest to zrobione niepoprawnie, ponieważ nie można pokazać jednego błędu, ale wiele. Musisz użyć kaskadowo, jeśli / to / else.
Łatwiejsze w użyciu konsekwentnie: Try / catch obejmuje styl, w którym nie trzeba sprawdzać błędów w każdej linii. Z drugiej strony: brakuje jednego, jeśli / else i twój kod może zniszczyć spustoszenie. Oczywiście tylko w nie powtarzalnych okolicznościach.
Więc ostatni punkt:
Słyszałem to około 15 lat temu, ostatnio nie słyszałem o tym z żadnych wiarygodnych źródeł. Kompilatory mogły ulec poprawie lub cokolwiek innego.
Najważniejsze jest to: nie przeprowadzaj przedwczesnej optymalizacji. Zrób to tylko wtedy, gdy można udowodnić, że kod odniesienia pod ręką jest mocno wewnętrzna pętla pewnym stopniu wykorzystane kodu i że styl przełączania poprawia wydajność znacznie . To jest 3% przypadek.
źródło
Odpowiedź na to pytanie była już naprawdę świetna, ale chciałbym dodać kilka przemyśleń na temat czytelności kodu i odzyskiwania błędów w konkretnym przypadku.
Uważam, że Twój kod może wyglądać tak:
Bez wyjątków, bez ifs. Raportowanie błędów można wykonać w
createText
. IcreateText
może zwrócić domyślny identyfikator tekstury, który nie wymaga sprawdzania wartości zwracanej, dzięki czemu reszta kodu działa równie dobrze.źródło