Zdaję sobie sprawę z wydajności osiągniętej podczas mieszania podpisanych int z floatami.
Czy jest gorsze mieszanie nieoznaczonych int z pływakami?
Czy jest jakieś trafienie podczas miksowania podpisanego / niepodpisanego bez pływaków?
Czy różne rozmiary (u32, u16, u8, i32, i16, i8) mają jakikolwiek wpływ na wydajność? Na jakich platformach?
c++
performance
Luis
źródło
źródło
Odpowiedzi:
Ogromną karą za mieszanie ints (dowolnego rodzaju) i float jest to, że są one w różnych zestawach rejestrów. Aby przejść z jednego zestawu rejestrów do drugiego, musisz zapisać wartość w pamięci i odczytać ją z powrotem, co powoduje przeciągnięcie się do sklepu .
Przechodzenie między różnymi rozmiarami lub sygnaturami ints utrzymuje wszystko w tym samym zestawie rejestrów, dzięki czemu unikasz dużej kary. Mogą obowiązywać mniejsze kary z powodu rozszerzeń znaków itp., Ale są one znacznie mniejsze niż w sklepie z ładowaniem.
źródło
Podejrzewam, że informacje o konsolach Xbox 360 i PS3 będą znajdować się za ścianami tylko dla licencjonowanych programistów, podobnie jak większość szczegółów niskiego poziomu. Możemy jednak zbudować równoważny program x86 i go zdemontować, aby uzyskać ogólny pomysł.
Najpierw zobaczmy, jakie koszty rozszerzenia bez podpisu:
Odpowiednia część rozkłada się na (za pomocą GCC 4.4.5):
Więc w zasadzie to samo - w jednym przypadku przenosimy bajt, w drugim przenosimy słowo. Kolejny:
Zamienia się w:
Koszt rozszerzenia znaku jest więc niezależnie od kosztu,
movsbl
a niemovzbl
- poziomu podinstrukcji. Zasadniczo jest to niemożliwe do oszacowania na nowoczesnych procesorach ze względu na sposób, w jaki działają nowoczesne procesory. Wszystko inne, od szybkości pamięci do buforowania do tego, co było wcześniej w potoku, zdominuje środowisko uruchomieniowe.W ciągu ~ 10 minut, które zajęło mi napisanie tych testów, mogłem łatwo znaleźć prawdziwy błąd wydajności, a gdy tylko włączę dowolny poziom optymalizacji kompilatora, kod staje się nierozpoznawalny dla tak prostych zadań.
To nie jest przepełnienie stosu, więc mam nadzieję, że nikt tutaj nie twierdzi, że mikrooptymalizacja nie ma znaczenia. Gry często działają na danych, które są bardzo duże i bardzo liczbowe, więc uważna uwaga na rozgałęzienia, rzutowania, harmonogramowanie, wyrównanie struktury itd. Może dać bardzo krytyczne ulepszenia. Każdy, kto spędził dużo czasu na optymalizacji kodu PPC, prawdopodobnie ma co najmniej jedną horror o sklepach z ładowaniem hitów. Ale w tym przypadku to naprawdę nie ma znaczenia. Rozmiar pamięci typu liczb całkowitych nie wpływa na wydajność, o ile jest wyrównany i mieści się w rejestrze.
źródło
Podpisane operacje na liczbach całkowitych mogą być droższe na prawie wszystkich architekturach. Na przykład dzielenie przez stałą jest szybsze, gdy nie jest podpisany, np .:
zostanie zoptymalizowany w celu:
Ale...
zoptymalizuje, aby:
lub w systemach, w których rozgałęzienie jest tanie,
To samo dotyczy modulo. Dotyczy to również non-potęgi-2 (ale przykład jest bardziej złożony). Jeśli w Twojej architekturze nie ma podziału sprzętowego (np. Większość ARM), niepodpisane podziały nie-stałych są również szybsze.
Mówienie kompilatorowi, że liczby ujemne nie mogą dać rezultatu, pomoże zoptymalizować wyrażenia, zwłaszcza te używane do zakończenia pętli i innych warunków warunkowych.
Jeśli chodzi o int różnej wielkości, tak, jest to niewielki wpływ, ale trzeba by to wyważyć w porównaniu z przenoszeniem mniejszej ilości pamięci. W dzisiejszych czasach zapewne zyskujesz więcej, uzyskując dostęp do mniejszej ilości pamięci niż tracisz dzięki zwiększeniu rozmiaru. W tym momencie jesteś bardzo zainteresowany mikrooptymalizacją.
źródło
Operacje z int podpisanymi lub niepodpisanymi mają taki sam koszt na obecnych procesorach (x86_64, x86, powerpc, uzbrojenie). W procesorze 32-bitowym u32, u16, u8 s32, s16, s8 powinny być takie same. Możesz mieć karę ze złym wyrównaniem.
Ale konwersja int na float lub float na int jest kosztowną operacją. Możesz łatwo znaleźć zoptymalizowane wdrożenie (SSE2, Neon ...).
Najważniejszym punktem jest prawdopodobnie dostęp do pamięci. Jeśli Twoje dane nie mieszczą się w pamięci podręcznej L1 / L2, stracisz więcej cyklu niż konwersji.
źródło
Jon Purdy mówi powyżej (nie mogę komentować), że niepodpisany może być wolniejszy, ponieważ nie może się przepełnić. Nie zgadzam się, arytmetyka bez znaku jest prostym modulo arytmetycznym moular 2 do liczby bitów w słowie. Zasadniczo podpisane operacje mogą ulec przepełnieniu, ale zwykle są wyłączone.
Czasami możesz zrobić sprytne (ale niezbyt czytelne) rzeczy, takie jak spakowanie dwóch lub więcej pozycji danych w int i uzyskanie wielu operacji na instrukcję (arytmetyka kieszeni). Ale musisz zrozumieć, co robisz. Oczywiście MMX pozwala ci to robić naturalnie. Ale czasem użycie największego rozmiaru słowa obsługiwanego przez sprzęt i ręczne pakowanie danych zapewnia najszybszą implementację.
Uważaj na wyrównanie danych. W większości wdrożeń sprzętowych niezrównane obciążenia i magazyny są wolniejsze. Naturalne wyrównanie oznacza, że dla powiedzmy słowa 4-bajtowego adres jest wielokrotnością czterech, a adresy ośmiu bajtów powinny być wielokrotnościami ośmiu bajtów. Przenosi to na SSE (128 bitów sprzyja wyrównaniu 16 bajtów). AVX wkrótce rozszerzy te rozmiary rejestrów „wektorowych” do 256 bitów, a następnie 512 bitów. A wyrównane ładunki / sklepy będą szybsze niż te niewyrównane. Dla maniaków HW, niewyrównana operacja pamięci może obejmować takie rzeczy jak linia cacheline, a nawet granice strony, o które HW musi uważać.
źródło
Nieco lepiej jest używać podpisanych liczb całkowitych do indeksów pętli, ponieważ podpisane przepełnienie jest niezdefiniowane w C, więc kompilator przyjmie, że takie pętle mają mniej przypadków narożnych. Jest to kontrolowane przez „-fstrict-overflow” gcc (domyślnie włączony) i efekt jest prawdopodobnie trudny do zauważenia bez odczytu danych wyjściowych zestawu.
Poza tym x86 działa lepiej, jeśli nie miksujesz typów, ponieważ może używać operandów pamięci. Jeśli musi konwertować typy (rozszerzenia znakowe lub zerowe), oznacza to jawne obciążenie i użycie rejestru.
Trzymaj się int dla zmiennych lokalnych, a większość z nich nastąpi domyślnie.
źródło
Jak wskazuje celion, narzut związany z konwersją między liczbami całkowitymi i zmiennoprzecinkowymi ma w dużej mierze związek z kopiowaniem i konwersją wartości między rejestrami. Jedyny narzut związany z niepodpisanymi intami sam w sobie wynika z ich gwarantowanego zachowania, które wymaga pewnej ilości kontroli przepełnienia w skompilowanym kodzie.
Zasadniczo nie ma narzutu podczas konwersji liczb całkowitych ze znakiem i bez znaku. Dostęp do różnych rozmiarów liczb całkowitych może (nieskończenie mały) być szybszy lub wolniejszy w zależności od platformy. Ogólnie mówiąc, liczba całkowita najbliższa wielkości słowa platformy będzie najszybsza , ale ogólna różnica w wydajności zależy od wielu innych czynników, w szczególności wielkości pamięci podręcznej: jeśli użyjesz,
uint64_t
gdy wszystko, czego potrzebujeszuint32_t
, może ponieważ mniej danych będzie od razu mieściło się w pamięci podręcznej, co może wiązać się z pewnym obciążeniem.Jednak myślenie o tym jest trochę przesadne. Jeśli używasz typów odpowiednich dla danych, wszystko powinno działać idealnie dobrze, a ilość mocy, jaką można uzyskać, wybierając typy oparte na architekturze, jest i tak nieistotna.
źródło