2016-08-08 43 views
5

Rozważmy następujący kod:Czy można mieć "odniesienie" do zmiennej w czasie kompilacji?

Matrix4x4 perspective(const ViewFrustum &frustum) { 
    float l = frustum.l; 
    float r = frustum.r; 
    float b = frustum.b; 
    float t = frustum.t; 
    float n = frustum.n; 
    float f = frustum.f; 

    return { 
     { 2 * n/(r - l), 0,    (r + l)/(r - l), 0      }, 
     { 0,    2 * n/(t - b), (t + b)/(t - b), 0      }, 
     { 0,    0,    -((f + n)/(f - n)), -(2 * n * f/(f - n)) }, 
     { 0,    0,    -1,     0      } 
    }; 
} 

W celu poprawienia czytelności konstruowaniu macierzy, muszę też zrobić kopię wartości z struct ściętego lub odnośniki do nich. Jednak nie potrzebuję też kopii ani pośrednictwa.

Czy możliwe jest posiadanie jakiegoś "odniesienia", które zostanie rozwiązane w czasie kompilacji, w rodzaju dowiązania symbolicznego. Miałby taki sam efekt jak:

Matrix4x4 perspective(const ViewFrustum &frustum) { 
    #define l frustum.l; 
    #define r frustum.r; 
    #define b frustum.b; 
    #define t frustum.t; 
    #define n frustum.n; 
    #define f frustum.f; 

    return { 
     { 2 * n/(r - l), 0,    (r + l)/(r - l), 0      }, 
     { 0,    2 * n/(t - b), (t + b)/(t - b), 0      }, 
     { 0,    0,    -((f + n)/(f - n)), -(2 * n * f/(f - n)) }, 
     { 0,    0,    -1,     0      } 
    }; 

    #undef l 
    #undef r 
    #undef b 
    #undef t 
    #undef n 
    #undef f 
} 

Bez preprocesora (lub czy jest akceptowany?). Przypuszczam, że nie jest to naprawdę potrzebne lub można by tego uniknąć w tym konkretnym przypadku, czyniąc te argumenty wartości 6 funkcją bezpośrednio (chociaż byłoby to trochę irytujące, gdyby trzeba było wywołać taką funkcję - ale nawet wtedy mógłbym zrobić wbudowana funkcja proxy).

Ale zastanawiałem się, czy jest to w ogóle możliwe w ogóle? Nie mogłem znaleźć czegoś takiego. Myślę, że przydałoby się to skracając opisowe nazwy, które będą używane bardzo często, bez konieczności utraty oryginalnych nazw.

+1

Masz na myśli odniesienie? 'float & l (frustum.l)' etc? – John3136

+0

@ John3136 tak, ale rzeczywiste odniesienie nie jest potrzebne –

+5

Tak czy inaczej, na to patrzysz, jest to całkowicie mikrooptymalizacja, niezależnie od tego, czy wykonasz kopię, czy użyjesz referencji. Kompilator jest wystarczająco inteligentny, aby zoptymalizować odniesienia do tych zmiennych. Rób to, co jest najlepsze dla czytelności. –

Odpowiedz

11

Obowiązkowe wyłączenie odpowiedzialności: nie należy przedwcześnie optymalizować.

Pozwól porównać swój naiwny perspective funkcji, zawierające

float l = frustum.l; 
float r = frustum.r; 
float b = frustum.b; 
float t = frustum.t; 
float n = frustum.n; 
float f = frustum.f; 

Z define „s oraz rozwiązania @Sam Varshavchik z referencjami.

Zakładamy, że nasz kompilator optymalizuje i optymalizuje co najmniej przyzwoite.

Moc wyjściowa dla wszystkich trzech wersji: https://godbolt.org/g/G06Bx8.

Można zauważyć, że referencje i definicja wersji są dokładnie takie same - zgodnie z oczekiwaniami. Ale naiwność różni się bardzo. Najpierw ładuje wszystkie wartości z pamięci:

movss (%rdi), %xmm2   # xmm2 = mem[0],zero,zero,zero 
    movss 4(%rdi), %xmm1   # xmm1 = mem[0],zero,zero,zero 
    movss 8(%rdi), %xmm0   # xmm0 = mem[0],zero,zero,zero 
    movss %xmm0, 12(%rsp)   # 4-byte Spill 
    movss 12(%rdi), %xmm0   # xmm0 = mem[0],zero,zero,zero 
    movss %xmm0, 8(%rsp)   # 4-byte Spill 
    movss 16(%rdi), %xmm3   # xmm3 = mem[0],zero,zero,zero 
    movaps %xmm3, 16(%rsp)   # 16-byte Spill 
    movss 20(%rdi), %xmm0 

a potem nie ponownie odwołuje się do %rdi (frustrum) Pamięć. Odwoływanie i definiowanie wersji, z drugiej strony, ładuje wartości, jakie są potrzebne.

Dzieje się tak dlatego, że realizacja Vector4 konstruktora jest ukryte z optymalizatora i nie można zakładać, że konstruktor nie zmienia frustrum, więc koniecznością obciążenia INSERT, nawet gdy takie ładunki są zbędne.

Tak więc naiwna wersja może być nawet szybsza od niż "zoptymalizowana" pod pewnymi warunkami.

+0

@Byteventurer, jak dla 'clang', nie stały się takie same, ale kompilator nie ładuje ponownie wartości z pamięci za każdym razem: https: //godbolt.org/g/hQfASp. Tak więc jest lepiej i (jak sądzę) będzie tak szybki jak naiwny w czasie wykonywania. – deniss

+0

Okay, wielkie dzięki! : D (dla rekordu, gdy skasowałem komentarz - zapytałem, czy robi różnicę, jeśli macierz jest zaimplementowana jako zwykła tablica) –

+1

@Byteventurer: Mam nadzieję, że masz trywialne funkcje, takie jak te konstruktory zdefiniowane w nagłówkach, w przeciwieństwie do przykładu Denissa. Optymalizacja czasu łącza może to naprawić, ale nie jest dobrym pomysłem, aby mieć trywialne funkcje, które nie są widoczne podczas kompilacji. Kompilatory nie mogą zakładać zbyt wiele na temat funkcji, która mogłaby zostać zmieniona, gdyby nie można było zobaczyć definicji, ale jeśli są w stanie ją zobaczyć, mogą dokładnie sprawdzić, która funkcja działa, nawet jeśli nie jest włączona. –

11

Dobrze, że to, co odniesienia C++ są dla:

const float &l = frustum.l; 
const float &r = frustum.r; 
const float &b = frustum.b; 
const float &t = frustum.t; 
const float &n = frustum.n; 
const float &f = frustum.f; 

Większość nowoczesnych kompilatory zoptymalizuje się odniesienia i użyć wartości z obiektu frustum dosłownie w poniższej wypowiedzi, rozdzielając odniesienia podczas kompilacji -czas.

+1

Czy kompilator zoptymalizuje również zmienne, jeśli nie są referencjami - tylko kopiami? – Rakete1111

+0

Rozumiem, więc referencje nie będą używane? Bez pośrednictwa? –

+0

Twój kod nie będzie się kompilował, dopóki 'const' nie określi twoich referencji zmiennoprzecinkowych (' frustum' jest const). –

4

Ogólnie rzecz biorąc, można używać zwykłych odniesień, o ile jesteś w zasięgu lokalnym. Współcześni kompilatorzy "przeglądają je" i traktują je jako aliasy (zauważcie, że dotyczy to nawet wskaźników).

Jednak, gdy mamy do czynienia z rzeczami z małej strony, kopiowanie do zmiennej lokalnej, jeśli w ogóle, jest ogólnie korzystne. frustnum.r to jedna warstwa oddalenia od siebie (frustnum jest w rzeczywistości wskaźnikiem pod maską), więc uzyskanie dostępu do niego jest droższe niż się wydaje, a jeśli masz funkcje w środku swojej funkcji, kompilator może nie być w stanie udowodnić, że jego wartość się nie zmienia, więc dostęp może wymagać powtórzenia.

Zmienne lokalne są zwykle bezpośrednio na stosie (tanie) lub prosto w rejestrach (najtańsze), a co najważniejsze, z uwagi na to, że zwykle nie mają interakcji z "zewnętrzną", kompilator ma łatwiejsze rozumowanie na temat je, dzięki czemu może być bardziej agresywny dzięki optymalizacji; również, podczas faktycznego wykonywania obliczeń, te wartości i tak będą kopiowane w rejestrach i na stosie.

Więc używaj kopii, w najgorszym kompilator prawdopodobnie zrobi to samo, w najlepszym razie możesz pomóc w optymalizacji.

+0

Tak, myślałem o tym, ale nie byłem pewien, dziękuję :) –