Pytanie Dlaczego GCC nie zoptymalizuje a * a * a * a * a * a do (a * a * a) * (a * a * a)?


Wykonuję optymalizację numeryczną na aplikacji naukowej. Zauważyłem tylko, że GCC zoptymalizuje połączenie pow(a,2) kompilując go a*a, ale połączenie pow(a,6) nie jest zoptymalizowany i faktycznie wywoła funkcję biblioteki pow, co znacznie spowalnia działanie. (W przeciwieństwie, Kompilator Intel C ++, plik wykonywalny icc, wyeliminuje wywołanie biblioteki pow(a,6).)

Tym, co mnie interesuje, jest to, że kiedy wymieniłem pow(a,6) z a*a*a*a*a*a używając GCC 4.5.1 i opcji "-O3 -lm -funroll-loops -msse4", wykorzystuje 5 mulsd instrukcje:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

podczas gdy ja piszę (a*a*a)*(a*a*a), będzie produkować

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

co zmniejsza liczbę mnożących się instrukcji do 3. icc ma podobne zachowanie.

Dlaczego kompilatory nie rozpoznają tej sztuczki optymalizacyjnej?


1965
2018-06-21 18:49


pochodzenie


Co oznacza "rozpoznawanie pow (a, 6)"? - Varun Madiath
Um ... wiesz, żezazazazaa i (azaa) * (aa * a) nie są takie same z liczbami zmiennoprzecinkowymi, prawda? Będziesz musiał użyć -funsafe-matematyki lub -ffast-matematyki lub czegoś takiego. - Damon
Proponuję przeczytać "Co każdy informatyk powinien wiedzieć o arytmetyki zmiennoprzecinkowej" autorstwa Davida Goldberga: download.oracle.com/docs/cd/E19957-01/806-3568/... po czym będziesz miał pełniejszą wiedzę na temat jamy smołowej, którą właśnie wszedłeś! - Phil Armstrong
Idealne pytanie. 20 lat temu zadałem to samo ogólne pytanie, a przez zmiażdżenie tego pojedynczego wąskiego gardła skróciłem czas wykonania symulacji Monte Carlo z 21 godzin do 7 godzin. Kod w wewnętrznej pętli został wykonany 13 bilionów razy w trakcie procesu, ale symulacja przekształciła się w nocne okno. (patrz odpowiedź poniżej)
Może rzucić (a*a)*(a*a)*(a*a) do miksu też. Ta sama liczba mnożeń, ale prawdopodobnie bardziej dokładna. - Rok Kralj


Odpowiedzi:


Bo Floating Point Math nie jest asocjatywny. Sposób grupowania operandów w mnożeniu zmiennoprzecinkowym wpływa na dokładność liczbową odpowiedzi.

W rezultacie większość kompilatorów jest bardzo konserwatywna, jeśli chodzi o zmianę kolejności obliczeń zmiennoprzecinkowych, chyba że można mieć pewność, że odpowiedź pozostanie taka sama, lub jeśli nie powiesz im, że nie zależy ci na dokładności liczbowej. Na przykład: -fassociative-math opcja gcc, który pozwala gcc na ponowne powiązanie operacji zmiennoprzecinkowych, a nawet -ffast-math opcja, która pozwala na jeszcze bardziej agresywną kompensację dokładności względem prędkości.


2567
2018-06-22 15:32



Tak. Dzięki -ffast-math robi taką optymalizację. Dobry pomysł! Ale ponieważ nasz kod dotyczy większej dokładności niż prędkość, lepiej byłoby go nie przekazywać. - xis
IIRC C99 pozwala kompilatorowi na takie "niebezpieczne" optymalizacje FP, ale GCC (na czymkolwiek innym niż x87) podejmuje rozsądną próbę podążania za IEEE 754 - nie jest to "granica błędu"; istnieje tylko jedna poprawna odpowiedź. - tc.
Szczegóły implementacji pow nie są tu ani tam; ta odpowiedź nawet nie wspomina pow. - Stephen Canon
@nedR: ICC domyślnie dopuszcza ponowną asocjację. Jeśli chcesz uzyskać zachowanie zgodne z normami, musisz ustawić -fp-model precise z ICC. clang i gcc domyślne do ścisłej zgodności w.r.t. reassociation. - Stephen Canon
@xis, to nie jest tak naprawdę -fassociative-mathbyłoby niedokładne; to tylko to a*a*a*a*a*a i (a*a*a)*(a*a*a) są różne. Tu nie chodzi o dokładność; chodzi o zgodność ze standardami i ściśle powtarzalne wyniki, np. takie same wyniki na każdym kompilatorze. Liczby zmiennoprzecinkowe nie są już dokładne. Rzadko się na to nie zgadza -fassociative-math. - Paul Draper


Lambdageek poprawnie wskazuje, że ponieważ asocjatywność nie obejmuje liczb zmiennoprzecinkowych, "optymalizacja" a*a*a*a*a*a do (a*a*a)*(a*a*a) może zmienić wartość. Z tego powodu jest zabronione przez C99 (chyba, że ​​jest to wyraźnie dozwolone przez użytkownika, poprzez flagę kompilatora lub pragmę). Zasadniczo zakłada się, że programista napisał przyczynę, którą zrobiła, a kompilator powinien to uszanować. Jeśli chcesz (a*a*a)*(a*a*a), napisz to.

Ale to może być ból do napisania; dlaczego kompilator nie może zrobić tego, co uważasz za słuszne, kiedy go używasz pow(a,6)? Ponieważ byłby to źle rzecz do zrobienia. Na platformie z dobrą biblioteką matematyczną, pow(a,6) jest znacznie bardziej dokładna niż a*a*a*a*a*a lub (a*a*a)*(a*a*a). Aby dostarczyć trochę danych, przeprowadziłem mały eksperyment na moim komputerze Mac Pro, mierząc najgorszy błąd w ocenie ^ 6 dla wszystkich liczb zmiennoprzecinkowych o pojedynczej precyzji pomiędzy [1,2]:

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

Za pomocą pow zamiast drzewa mnożenia zmniejsza błąd związany z a współczynnik 4. Kompilatory nie powinny (i zazwyczaj nie) tworzyć "optymalizacje", które zwiększają błąd, chyba że są licencjonowane przez użytkownika (np. -ffast-math).

Zauważ, że GCC zapewnia __builtin_powi(x,n) jako alternatywa dla pow( ), które powinno generować śródliniowe drzewo mnożenia. Użyj tego, jeśli chcesz zamienić dokładność na wydajność, ale nie chcesz włączać szybkiej matematyki.


614
2018-06-22 22:39



Zauważ też, że Visual C ++ zapewnia "ulepszoną" wersję pow (). Poprzez dzwonienie _set_SSE2_enable(<flag>) z flag=1, jeśli to możliwe, użyje SSE2. Zmniejsza to dokładność nieco, ale poprawia prędkość (w niektórych przypadkach). MSDN: _set_SSE2_enable () i pow () - TkTech
@TkTech: Każda mniejsza dokładność wynika z implementacji Microsoftu, a nie z rozmiaru używanych rejestrów. Możliwe jest dostarczenie poprawnie zaokrąglone  pow używanie tylko 32-bitowych rejestrów, jeśli autor biblioteki jest tak zmotywowany. Są oparte na SSE pow wdrożenia, które są jeszcze dokładniejsze niż większość implementacji opartych na architekturze x87, istnieją również implementacje, które ograniczają dokładność prędkości. - Stephen Canon
@TkTech: Oczywiście, chciałem tylko wyjaśnić, że zmniejszenie dokładności wynika z wyborów dokonywanych przez autorów bibliotek, a nie z samego użycia SSE. - Stephen Canon
Chciałbym wiedzieć, w jaki sposób stosowałeś tutaj "złoty standard" do obliczania względnych błędów - zwykle oczekiwałbym, że tak będzie a*a*a*a*a*a, ale najwyraźniej tak nie jest! :) - j_random_hacker
@j_random_hacker: ponieważ porównywałem wyniki pojedynczej precyzji, wystarczy podwójna precyzja dla standardu złota - błąd zzazazazaobliczone w podwójnym jest * ogromnie mniejszy niż błąd dowolnej z obliczeń pojedynczej precyzji. - Stephen Canon


Kolejny podobny przypadek: większość kompilatorów nie będzie optymalizować a + b + c + d do (a + b) + (c + d) (jest to optymalizacja, ponieważ drugie wyrażenie może być lepiej potokowane) i ocenić je jako podane (tj (((a + b) + c) + d)). To też jest z powodu narożnych przypadków:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

To wyjście 1.000000e-05 0.000000e+00


152
2018-06-23 11:44



To nie jest dokładnie to samo. Changin kolejność mnożeń / podziałów (z wyłączeniem dzielenia przez 0) jest bezpieczniejsza niż kolejność sumowania / odejmowania. W mojej skromnej opinii, kompilator powinien spróbować skojarzyć mults./divs. ponieważ to zmniejsza całkowitą liczbę operacji, a poza zyskiem wydajności również zyskuje precyzję. - GameDeveloper
@DarioOO: To nie jest bezpieczniejsze. Mnożenie i dzielenie są takie same jak dodawanie i odejmowanie wykładnika, a zmiana kolejności może spowodować tymczasowe przekroczenie możliwego zakresu wykładnika. (Nie dokładnie to samo, ponieważ wykładnik nie traci precyzji ... ale reprezentacja jest nadal dość ograniczona, a zmiana kolejności może prowadzić do niereprezentowalnych wartości) - Ben Voigt
Myślę, że brakuje ci jakiegoś rachunku różniczkowego. Multplying i dzielenie 2 liczb wprowadza taką samą ilość błędów. Podczas odejmowania / dodawania 2 liczby mogą wprowadzać większy błąd, szczególnie gdy 2 liczby są różne od wielkości, dlatego jest bezpieczniejszy re-ordgingin mul / divide niż sub / add, ponieważ wprowadza on niewielką zmianę w końcowym błędzie. - GameDeveloper
@DarioOO: ryzyko jest inne w przypadku mul / div: zmiana kolejności powoduje znikomą zmianę wyniku końcowego lub przekroczenie wykładnika w pewnym momencie (gdzie nie miałoby to miejsca wcześniej), a wynik jest znacznie inny (potencjalnie + inf lub 0). - Peter Cordes


Fortran (zaprojektowany do obliczeń naukowych) ma wbudowanego operatora mocy, i o ile wiem, kompilatory Fortranu zwykle optymalizują podnoszenie do mocy całkowitych w sposób podobny do tego, który opisujesz. C / C ++ niestety nie mają operatora mocy, tylko funkcję biblioteki pow(). Nie wyklucza to traktowania inteligentnych kompilatorów pow specjalnie i obliczenia go w szybszy sposób dla przypadków specjalnych, ale wydaje się, że robią to rzadziej ...

Kilka lat temu starałem się, aby wygodniej było obliczać moce całkowite w optymalny sposób i wymyśliłem następujące. Jest to C ++, a nie C, i nadal zależy od tego, czy kompilator jest nieco inteligentny w kwestii optymalizacji / wstawiania rzeczy. W każdym razie, miej nadzieję, że może się okazać przydatna w praktyce:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Wyjaśnienie dla ciekawskich: nie znajduje to optymalnego sposobu obliczania mocy, ale od tego czasu znalezienie optymalnego rozwiązania jest problemem NP-zupełnym i to jest warte tylko dla małych mocy i tak (w przeciwieństwie do używania pow), nie ma powodu, by mieszać z detalami.

Następnie użyj go jako power<6>(a).

Ułatwia to wpisywanie uprawnień (nie trzeba przeliterować 6) as with parens) i pozwala na taką optymalizację bez -ffast-math w przypadku, gdy masz coś zależnego od precyzji, na przykład skompensowane sumowanie (przykład, w którym kolejność operacji jest niezbędna).

Prawdopodobnie możesz też zapomnieć, że jest to C ++ i po prostu użyj go w programie C (jeśli kompiluje się z kompilatorem C ++).

Mam nadzieję, że to może być przydatne.

EDYTOWAĆ:

Oto, co otrzymuję z mojego kompilatora:

Dla a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Dla (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Dla power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

74
2018-06-23 10:07



Znalezienie optymalnego drzewa mocy może być trudne, ale ponieważ jest to interesujące tylko dla małych mocy, oczywistą odpowiedzią jest wcześniejsze obliczenie go (Knuth zapewnia tabelę do 100) i użycie tej tabeli z twardym zakodowaniem (to jest to, co gcc wewnętrznie dla powiatu) . - Marc Glisse
W nowoczesnych procesorach prędkość jest ograniczona przez opóźnienie. Na przykład wynik mnożenia może być dostępny po pięciu cyklach. W takiej sytuacji znalezienie najszybszego sposobu na stworzenie mocy może być trudniejsze. - gnasher729
Można również spróbować znaleźć drzewo mocy, które daje najniższą górną granicę dla błędu względnej zaokrąglenia lub najniższy średni błąd względnego zaokrąglenia. - gnasher729
Boost ma również wsparcie dla tego, np. boost :: math :: pow <6> (n); Myślę, że próbuje nawet zmniejszyć liczbę mnożeń przez wyodrębnienie wspólnych czynników. - gast128
Dobry pomysł ! Zrobiłem to już wcześniej w przypadku precutingu czynnikowego. - Caduchon


Ponieważ 32-bitowa liczba zmiennoprzecinkowa - na przykład 1.024 - nie jest równa 1.024. W komputerze 1.024 jest przedziałem: od (1.024-e) do (1.024 + e), gdzie "e" oznacza błąd. Niektórzy nie zdają sobie z tego sprawy, a także uważają, że * w a * a oznacza mnożenie liczb arbitralnej precyzji bez żadnych błędów związanych z tymi liczbami. Powodem, dla którego niektórzy ludzie nie zdają sobie z tego sprawy, być może są obliczenia matematyczne, które wykonywali w szkołach podstawowych: pracują tylko z idealną liczbą bez błędów i wierzą, że można po prostu zignorować "e" podczas wykonywania mnożenia. Nie widzą "e" ukrytego w "float a = 1.2", "a * a * a" i podobnych kodach C.

Jeśli większość programistów rozpozna (i będzie w stanie wykonać) koncepcję, że wyrażenie C a * a * a * a * a * a faktycznie nie działa z idealnymi liczbami, kompilator GCC będzie wtedy DARMOWY, aby zoptymalizować "a * a * a * a * a * a "to say" t = (a * a); t * t * t ", które wymaga mniejszej liczby multiplikacji. Ale niestety, kompilator GCC nie wie, czy programista piszący kod myśli, że "a" jest liczbą z błędem lub bez. I tak GCC zrobi tylko to, na co wygląda kod źródłowy - ponieważ tak właśnie GCC widzi "gołym okiem".

... gdy już wiesz, jakiego rodzaju programista ty możesz użyć przełącznika "-ffast-math", aby powiedzieć GCC, że "Hej, GCC, wiem, co robię!". Pozwoli to GCC na przekształcenie * a * a * a * a * a w inny fragment tekstu - wygląda inaczej niż * a * a * a * a * a - ale nadal oblicza liczbę w przedziale błędu a * a * a * a * a * a. To jest w porządku, ponieważ wiesz już, że pracujesz z interwałami, a nie idealnymi liczbami.


49
2018-03-29 06:51



Liczby zmiennoprzecinkowe są dokładne. Niekoniecznie są dokładnie tym, czego się spodziewałeś. Co więcej, technika z epsilon sama w sobie jest przybliżeniem do tego, jak radzić sobie z rzeczami w rzeczywistości, ponieważ prawdziwy spodziewany błąd jest w stosunku do skali mantysy, tzn. Zwykle masz do około 1 LSB, ale to może wzrosnąć z każda operacja wykonywana, jeśli nie jesteś ostrożny, więc skonsultuj się z analitykiem numerycznym, zanim zrobisz coś nie trywialnego z zmiennoprzecinkowym. Użyj odpowiedniej biblioteki, jeśli możesz. - Donal Fellows
@DonalFellows: Standard IEEE wymaga, aby obliczenia zmiennoprzecinkowe dały wynik, który najdokładniej pasuje do wyniku, gdyby operandy źródłowe były dokładnymi wartościami, ale to nie znaczy, że faktycznie przedstawiać dokładne wartości. W wielu przypadkach bardziej pomocne jest uznanie 0.1f za (1,677,722 +/- 0,5) / 16,777,216, które powinno być wyświetlane z cyfrą dziesiętną implikowaną przez tę niepewność, niż uznanie go za dokładną ilość (1,677,722 +/- 0,5) / 16,777,216 (które powinny być wyświetlane z 24 cyframi dziesiętnymi). - supercat
@supercat: IEEE-754 jest całkiem jasne w kwestii danych zmiennoprzecinkowych zrobić reprezentują dokładne wartości; punktami 3.2 - 3.4 są odpowiednie sekcje. Możesz oczywiście wybrać interpretowanie ich inaczej, tak jak możesz to zinterpretować int x = 3 co oznacza, że x wynosi 3 +/- 0,5. - Stephen Canon
@supercat: Zgadzam się całkowicie, ale to nie oznacza tego Distance nie jest dokładnie równa jego wartości liczbowej; oznacza to, że wartość liczbowa jest jedynie przybliżeniem do modelowanej wielkości fizycznej. - Stephen Canon
W przypadku analizy numerycznej twój mózg będzie ci wdzięczny, jeśli będziesz interpretował liczby zmiennoprzecinkowe nie jako interwały, ale jako dokładne wartości (które nie są dokładnie tymi, których potrzebujesz). Na przykład, jeśli x jest gdzieś w okolicy 4.5 z błędem mniejszym niż 0,1, a obliczasz (x + 1) - x, interpretacja "przedziałowa" pozostawia w przedziale od 0,8 do 1,2, podczas gdy interpretacja "dokładnej wartości" mówi wynik będzie wynosił 1 z błędem co najwyżej 2 ^ (- 50) z podwójną precyzją. - gnasher729


GCC faktycznie optymalizuje a * a * a * a * a * a do (a * a * a) * (a * a * a), gdy a jest liczbą całkowitą. Próbowałem za pomocą tego polecenia:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Istnieje wiele flag gcc, ale nic nadzwyczajnego. Mają na myśli: Czytaj ze standardowego wejścia; używać poziomu optymalizacji O2; wyjściowy listing języka asemblerowego zamiast binarnego; listing powinien używać składni języka asemblerowego; wejście jest w języku C (zwykle język jest wywnioskowany z rozszerzenia pliku wejściowego, ale nie ma rozszerzenia pliku podczas odczytu ze standardowego wejścia); i napisz do stdout.

Oto ważna część wyników. Zanotowałem to z pewnymi komentarzami wskazującymi, co się dzieje w języku asemblerowym:

    ; x is in edi to begin with.  eax will be used as a temporary register.
    mov    eax, edi     ; temp1 = x
    imul    eax, edi    ; temp2 = x * temp1
    imul    eax, edi    ; temp3 = x * temp2
    imul    eax, eax    ; temp4 = temp3 * temp3

Używam systemu GCC na Linux Mint 16 Petra, pochodnej Ubuntu. Oto wersja gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Jak zauważyły ​​inne plakaty, opcja ta nie jest możliwa w zmiennym punkcie, ponieważ arytmetyka zmiennoprzecinkowa w rzeczywistości nie jest asocjacyjna.


49
2018-06-27 21:03



Jest to dopuszczalne w przypadku mnożenia liczb całkowitych, ponieważ przepełnienie uzupełnienia dwóch jest niezdefiniowanym zachowaniem. Jeśli nastąpi przepełnienie, stanie się to gdzieś, bez względu na kolejność operacji. Tak więc wyrażenia bez przepełnienia oceniają to samo, wyrażenia, które to przepełnienie są niezdefiniowanym zachowaniem, więc kompilator może zmienić punkt, w którym następuje przepełnienie. gcc robi to z unsigned int, także. - Peter Cordes


Żadne plakaty nie wspomniały jeszcze o skróceniu wyrażeń pływających (standard ISO C, 6.5p8 i 7.12.2). Jeśli FP_CONTRACT pragma jest ustawiona na ON, kompilator może traktować wyrażenie takie jak a*a*a*a*a*a jako pojedyncza operacja, jakby oceniana dokładnie za pomocą pojedynczego zaokrąglenia. Na przykład, kompilator może zastąpić go wewnętrzną funkcją zasilania, która jest zarówno szybsza, jak i bardziej dokładna. Jest to szczególnie interesujące, ponieważ zachowanie jest częściowo kontrolowane przez programistę bezpośrednio w kodzie źródłowym, podczas gdy opcje kompilatora dostarczane przez użytkownika końcowego mogą czasami być używane niepoprawnie.

Domyślny stan pliku FP_CONTRACT Pragma jest definiowana przez implementację, więc kompilator może domyślnie wykonywać takie optymalizacje. Dlatego przenośny kod, który musi ściśle przestrzegać zasad IEEE 754, powinien jawnie go ustawić OFF.

Jeśli kompilator nie obsługuje tej pragmy, musi być konserwatywny, unikając takiej optymalizacji, na wypadek gdyby programista wybrał ustawienie OFF.

GCC nie obsługuje tej pragmy, ale z domyślnymi opcjami zakłada, że ​​tak właśnie jest ON; tak więc dla celów ze sprzętową FMA, jeśli chcemy zapobiec transformacji a*b+c do fma (a, b, c), należy podać opcję, taką jak -ffp-contract=off (Aby wyraźnie ustawić pragma OFF) lub -std=c99 (aby GCC było zgodne z niektórymi standardowymi wersjami C, tutaj C99, postępuj zgodnie z powyższym paragrafem). W przeszłości ta ostatnia opcja nie zapobiegała transformacji, co oznacza, że ​​GCC nie spełniało tego punktu: https://gc.gnu.org/bugzilla/show_bug.cgi?id=37845


27
2018-06-23 12:44



Długotrwałe popularne pytania pokazują czasami ich wiek. To pytanie zadano i udzielono odpowiedzi w 2011 r., Kiedy GCC można było usprawiedliwić za nieprzestrzeganie dokładnie tego ostatniego standardu C99. Oczywiście, teraz jest 2014, więc GCC ... ahem. - Pascal Cuoq
Czy jednak nie powinieneś odpowiadać na stosunkowo nowe pytania zmiennoprzecinkowe bez zaakceptowanej odpowiedzi? kaszel stackoverflow.com/questions/23703408 kaszel - Pascal Cuoq
Uważam, że ... przeszkadza to, że gcc nie implementuje pragmatyki zmiennoprzecinkowej C99. - David Monniaux


Jak zauważył Lambdageek, mnożenie float nie jest asocjacyjne i można uzyskać mniejszą dokładność, ale także gdy uzyskuje się większą dokładność, można argumentować przeciwko optymalizacji, ponieważ chcemy deterministycznego zastosowania. Na przykład w grze / kliencie symulacji gry, gdzie każdy klient musi symulować ten sam świat, na którym obliczenia zmiennoprzecinkowe mają być deterministyczne.


26
2018-06-21 18:52



Pływający punkt jest zawsze deterministyczny. - Alice
@Alice Wydaje się dość oczywiste, że Bjorn używa tutaj "deterministycznego" w sensie kodu dającego taki sam wynik na różnych platformach i różnych wersjach kompilatora itp. (Zmienne zewnętrzne, które mogą być poza kontrolą programisty) - w przeciwieństwie do braku rzeczywistej losowości liczbowej w czasie wykonywania. Jeśli wskażesz, że to nie jest właściwe użycie tego słowa, nie będę się z tym sprzeczał. - greggo
@Greggo Z wyjątkiem nawet twojej interpretacji tego, co mówi, wciąż jest źle; to jest cały punkt IEEE 754, aby zapewnić identyczną charakterystykę dla większości (jeśli nie wszystkich) operacji na różnych platformach. Teraz nie wspominał o platformach i wersjach kompilatorów, które byłyby ważnym problemem, gdyby każda operacja na każdym zdalnym serwerze / kliencie była identyczna ... ale nie jest to oczywiste z jego oświadczenia. Lepsze słowo może być "niezawodnie podobne" lub coś podobnego. - Alice
@Alice marnujesz czas wszystkich, także twoich, argumentując semantykę. Jego znaczenie było jasne. - Lanaru
@Lanaru Cały punkt standardów to semantyka; jego znaczenie zdecydowanie nie było jasne. - Alice


Nie spodziewałbym się, że ta sprawa zostanie w ogóle zoptymalizowana. Nie może być bardzo często, gdy wyrażenie zawiera podwyrażenia, które można przegrupować w celu usunięcia całych operacji. Spodziewam się, że twórcy kompilacji poświęcą swój czas na obszary, które z większym prawdopodobieństwem spowodują zauważalne ulepszenia, niż pokrycie rzadko spotykanego przypadku krawędzi.

Byłem zaskoczony, gdy dowiedziałem się z innych odpowiedzi, że to wyrażenie może być rzeczywiście zoptymalizowane za pomocą odpowiednich przełączników kompilatora. Zarówno optymalizacja jest trywialna, jak i skrajny przypadek znacznie powszechniejszej optymalizacji lub autorzy kompilatorów byli bardzo dokładni.

Nie ma nic złego w dostarczaniu wskazówek kompilatorowi, tak jak tutaj. Jest to normalna i oczekiwana część procesu mikro-optymalizacji, polegająca na przestawianiu instrukcji i wyrażeń, aby zobaczyć, jakie różnice przyniosą.

Chociaż kompilator może być uzasadniony w rozważaniu dwóch wyrażeń, aby dostarczyć niespójne wyniki (bez odpowiednich przełączników), nie ma potrzeby, abyś był związany tym ograniczeniem. Różnica będzie niewiarygodnie mała - tak bardzo, że jeśli różnica ma znaczenie dla ciebie, nie powinieneś używać w pierwszej kolejności standardowej arytmetyki zmiennoprzecinkowej.


26
2018-01-03 16:40



Jak zauważył inny komentator, jest to nieprawdą aż do absurdu; różnica może wynosić nawet połowę do 10% kosztów, a jeśli zostanie uruchomiona w ciasnej pętli, to przełoży się to na wiele instrukcji marnowanych, aby uzyskać niewielką ilość dodatkowej precyzji. Mówienie, że nie powinieneś używać standardowego FP, gdy robisz monte carlo, to tak, jakbyś zawsze używał samolotu do przejścia przez kraj; ignoruje wiele efektów zewnętrznych. Wreszcie NIE jest to rzadka optymalizacja; analiza martwych kodów i redukcja kodu / refaktor jest bardzo powszechna. - Alice