Czy inicjowanie zmiennej składowej i brak odwoływania się do niej / używania jej dalej zajmuje pamięć RAM w czasie wykonywania, czy też kompilator po prostu ignoruje tę zmienną?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
W powyższym przykładzie element członkowski „var1” pobiera wartość, która jest następnie wyświetlana w konsoli. „Var2” nie jest jednak w ogóle używany. Dlatego zapisywanie go w pamięci podczas działania byłoby stratą zasobów. Czy kompilator bierze pod uwagę tego typu sytuacje i po prostu ignoruje nieużywane zmienne, czy też obiekt Foo jest zawsze tej samej wielkości, niezależnie od tego, czy używane są jego składowe?
var2
nie.sizeof(Foo)
nie może zmniejszać się z definicji - jeśli drukujeszsizeof(Foo)
, musi dać8
(na popularnych platformach). Kompilatory mogą zoptymalizować przestrzeń używaną przezvar2
(bez względu na to, czy przez,new
czy na stosie, czy w wywołaniach funkcji ...) w dowolnym kontekście, który uznają za rozsądny, nawet bez LTO lub optymalizacji całego programu. Tam, gdzie nie jest to możliwe, nie zrobią tego, jak w przypadku każdej innej optymalizacji. Uważam, że zmiana zaakceptowanej odpowiedzi znacznie zmniejsza prawdopodobieństwo wprowadzenia jej w błąd.Odpowiedzi:
Złota reguła „as-if” 1 w C ++ stwierdza, że jeśli obserwowalne zachowanie programu nie zależy od istnienia nieużywanego elementu danych, kompilator może go zoptymalizować .
Nie (jeśli jest „naprawdę” nieużywany).
Teraz przychodzą na myśl dwa pytania:
Zacznijmy od przykładu.
Przykład
#include <iostream> struct Foo1 { int var1 = 5; Foo1() { std::cout << var1; } }; struct Foo2 { int var1 = 5; int var2; Foo2() { std::cout << var1; } }; void f1() { (void) Foo1{}; } void f2() { (void) Foo2{}; }
Jeśli poprosimy gcc o skompilowanie tej jednostki tłumaczeniowej , wyświetli:
f1(): mov esi, 5 mov edi, OFFSET FLAT:_ZSt4cout jmp std::basic_ostream<char, std::char_traits<char> >::operator<<(int) f2(): jmp f1()
f2
jest tym samym, cof1
i żadna pamięć nie jest nigdy używana do przechowywania rzeczywistegoFoo2::var2
. ( Clang robi coś podobnego ).Dyskusja
Niektórzy mogą powiedzieć, że jest inaczej z dwóch powodów:
Cóż, dobry program to sprytny i złożony zestaw prostych rzeczy, a nie proste zestawienie złożonych rzeczy. W rzeczywistości piszesz mnóstwo prostych funkcji przy użyciu prostych struktur, które kompilator optymalizuje. Na przykład:
bool insert(std::set<int>& set, int value) { return set.insert(value).second; }
To jest prawdziwy przykład
std::pair<std::set<int>::iterator, bool>::first
nieużywanego elementu danych (tutaj ). Zgadnij co? Jest zoptymalizowany ( prostszy przykład z zestawem atrap, jeśli ten zestaw powoduje, że płaczesz).Teraz byłby idealny czas na przeczytanie doskonałej odpowiedzi Maxa Langhofa (proszę o głos za mną). Wyjaśnia, dlaczego ostatecznie koncepcja struktury nie ma sensu na poziomie montażu kompilatora.
„Ale jeśli zrobię X, fakt, że nieużywany członek jest zoptymalizowany, jest problemem!”
Pojawiło się wiele komentarzy, które argumentowały, że ta odpowiedź musi być błędna, ponieważ jakaś operacja (taka jak
assert(sizeof(Foo2) == 2*sizeof(int))
) mogłaby coś zepsuć.Jeśli X jest częścią obserwowalnego zachowania programu 2 , kompilator nie może zoptymalizować rzeczy. Istnieje wiele operacji na obiekcie zawierającym „nieużywany” element danych, które miałyby zauważalny wpływ na program. Jeśli taka operacja jest wykonywana lub kompilator nie może udowodnić, że żadna nie została wykonana, ten „nieużywany” element członkowski danych jest częścią obserwowalnego zachowania programu i nie może zostać zoptymalizowany .
Operacje wpływające na obserwowalne zachowanie obejmują między innymi:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Podobnie jak w przypadku stwierdzenia, że przejdzie lub upadnie.
źródło
assert(sizeof(…)…)
rzeczywistości nie ogranicza kompilatora - musi zapewnićsizeof
kod, który pozwala kodowi używać rzeczy takich jakmemcpy
działanie, ale to nie znaczy, że kompilator jest w jakiś sposób zobowiązany do użycia tak wielu bajtów, chyba że mogą być narażone na takie,memcpy
które mogą i tak nie przepisuj, aby uzyskać poprawną wartość.Ważne jest, aby zdać sobie sprawę, że kod, który tworzy kompilator, nie ma rzeczywistej wiedzy o twoich strukturach danych (ponieważ coś takiego nie istnieje na poziomie zespołu), podobnie jak optymalizator. Kompilator tworzy tylko kod dla każdej funkcji , a nie struktury danych .
Ok, zapisuje też stałe sekcje danych i takie.
Na tej podstawie możemy już powiedzieć, że optymalizator nie „usunie” ani „wyeliminuje” elementów, ponieważ nie wyprowadza struktur danych. Wyprowadza kod , który może lub nie może korzystać z elementów członkowskich, a jednym z jego celów jest oszczędzanie pamięci lub cykli poprzez eliminację bezcelowych zastosowań (tj. Zapisywanie / odczytywanie) elementów członkowskich.
Istota tego polega na tym, że „jeśli kompilator może udowodnić w zakresie funkcji (w tym funkcji, które zostały do niej wstawione), że nieużywany element członkowski nie ma znaczenia dla sposobu działania funkcji (i tego, co zwraca), to są duże szanse, że obecność członka nie powoduje kosztów ogólnych ”.
Gdy sprawisz, że interakcje funkcji ze światem zewnętrznym będą bardziej skomplikowane / niejasne dla kompilatora (pobierz / zwróć bardziej złożone struktury danych, np. A
std::vector<Foo>
, ukryj definicję funkcji w innej jednostce kompilacji, zabroń / zniechęcaj do wstawiania itp.) , staje się coraz bardziej prawdopodobne, że kompilator nie może udowodnić, że nieużywany element członkowski nie ma żadnego efektu.Nie ma tutaj sztywnych reguł, ponieważ wszystko zależy od optymalizacji dokonanych przez kompilator, ale dopóki robisz trywialne rzeczy (takie jak pokazane w odpowiedzi YSC), jest bardzo prawdopodobne, że nie będzie żadnego narzutu, podczas gdy robienie skomplikowanych rzeczy (np. a
std::vector<Foo>
z funkcji zbyt dużej do umieszczenia w tekście) prawdopodobnie spowoduje koszty ogólne.Aby zilustrować ten punkt, rozważ następujący przykład :
struct Foo { int var1 = 3; int var2 = 4; int var3 = 5; }; int test() { Foo foo; std::array<char, sizeof(Foo)> arr; std::memcpy(&arr, &foo, sizeof(Foo)); return arr[0] + arr[4]; }
Robimy tutaj nietrywialne rzeczy (pobieramy adresy, sprawdzamy i dodajemy bajty z reprezentacji bajtów ), a mimo to optymalizator może dowiedzieć się, że wynik jest zawsze taki sam na tej platformie:
test(): # @test() mov eax, 7 ret
Członkowie nie tylko
Foo
nie zajmowali pamięci, aFoo
nawet nie zaistnieli! Jeśli istnieją inne zastosowania, których nie można zoptymalizować, np.sizeof(Foo)
Może to mieć znaczenie - ale tylko dla tego segmentu kodu! Gdyby wszystkie zastosowania można było zoptymalizować w ten sposób, wówczas istnienie np.var3
Nie wpływa na generowany kod. Ale nawet jeśli zostanie użyty w innym miejscu,test()
pozostanie zoptymalizowany!W skrócie: każde użycie
Foo
jest optymalizowane niezależnie. Niektórzy mogą zużywać więcej pamięci z powodu niepotrzebnego członka, inni mogą nie. Więcej informacji znajdziesz w instrukcji kompilatora.źródło
Kompilator zoptymalizuje nieużywaną zmienną składową (szczególnie publiczną) tylko wtedy, gdy może udowodnić, że usunięcie zmiennej nie ma skutków ubocznych i że żadna część programu nie zależy od jej rozmiaru
Foo
.Nie sądzę, aby jakikolwiek obecny kompilator przeprowadzał takie optymalizacje, chyba że struktura w rzeczywistości nie jest w ogóle używana. Niektóre kompilatory mogą przynajmniej ostrzegać przed nieużywanymi zmiennymi prywatnymi, ale zazwyczaj nie w przypadku publicznych.
źródło
Ogólnie musisz założyć, że otrzymujesz to, o co prosiłeś, na przykład „nieużywane” zmienne składowe są tam.
Ponieważ w twoim przykładzie oba elementy członkowskie są
public
, kompilator nie może wiedzieć, czy jakiś kod (szczególnie z innych jednostek tłumaczeniowych = inne pliki * .cpp, które są oddzielnie kompilowane, a następnie łączone) uzyska dostęp do „nieużywanego” elementu członkowskiego.Odpowiedź YSC daje bardzo prosty przykład, w którym typ klasy jest używany tylko jako zmienna automatycznego czasu trwania przechowywania i gdzie nie jest brany wskaźnik do tej zmiennej. Tam kompilator może wstawić cały kod, a następnie może wyeliminować cały martwy kod.
Jeśli masz interfejsy między funkcjami zdefiniowanymi w różnych jednostkach tłumaczeniowych, zwykle kompilator nic nie wie. Interfejsy są zwykle zgodne z pewnymi predefiniowanymi ABI (takimi jak ten ), tak że różne pliki obiektowe mogą być połączone ze sobą bez żadnych problemów. Zazwyczaj ABI nie mają znaczenia, czy element członkowski jest używany, czy nie. Tak więc w takich przypadkach drugi element musi fizycznie znajdować się w pamięci (chyba że zostanie później usunięty przez łącznik).
Dopóki znajdujesz się w granicach języka, nie możesz zauważyć, że ma miejsce jakakolwiek eliminacja. Jeśli zadzwonisz
sizeof(Foo)
, dostaniesz2*sizeof(int)
. Jeśli utworzysz tablicęFoo
s, odległość między początkami dwóch kolejnych obiektówFoo
wynosi zawszesizeof(Foo)
bajty.Twój typ to standardowy typ układu , co oznacza, że możesz również uzyskać dostęp do elementów członkowskich na podstawie przesunięć obliczonych w czasie kompilacji (por.
offsetof
Makro). Ponadto można sprawdzić reprezentację obiektu bajt po bajcie, kopiując ją do tablicychar
usingstd::memcpy
. We wszystkich tych przypadkach można zaobserwować obecność drugiego członka.źródło
gcc -fwhole-program -O3 *.c
teoretycznie mógłby to zrobić, ale w praktyce prawdopodobnie nie. (np. w przypadku, gdy program przyjmuje jakieś założenia dotyczące dokładnej wartościsizeof()
tego celu i ponieważ jest to naprawdę skomplikowana optymalizacja, którą programiści powinni wykonać ręcznie, jeśli chcą.)Przykłady innych odpowiedzi na to pytanie, które elide
var2
opierają się na jednej technice optymalizacji: ciągłej propagacji, a następnie elizji całej struktury (a nie elizji sprawiedliwejvar2
). Jest to prosty przypadek i optymalizujące kompilatory go implementują.W przypadku niezarządzanych kodów C / C ++ odpowiedź jest taka, że kompilator generalnie nie pominie
var2
. O ile wiem, nie ma wsparcia dla takiej transformacji struktury C / C ++ w debugowaniu informacji, a jeśli struktura jest dostępna jako zmienna w debugerze,var2
nie można jej usunąć. O ile wiem, żaden obecny kompilator C / C ++ nie może specjalizować funkcji zgodnie z elision ofvar2
, więc jeśli struktura jest przekazywana lub zwracana z funkcjivar2
niewymienionej, nie można jej usunąć.W przypadku języków zarządzanych, takich jak C # / Java z kompilatorem JIT, kompilator może być w stanie bezpiecznie usunąć,
var2
ponieważ może dokładnie śledzić, czy jest używany i czy przechodzi do niezarządzanego kodu. Fizyczny rozmiar struktury w językach zarządzanych może różnić się od rozmiaru zgłaszanego programiście.Kompilatory języka C / C ++ roku 2019 nie mogą zostać usunięte
var2
ze struktury, chyba że cała zmienna struktury zostanie usunięta. Dla interesujących przypadków elizjivar2
ze struktury odpowiedź brzmi: Nie.Niektóre przyszłe kompilatory C / C ++ będą mogły odejść
var2
od struktury, a ekosystem zbudowany wokół kompilatorów będzie musiał dostosować się do przetwarzania informacji generowanych przez kompilatory.źródło
Zależy to od Twojego kompilatora i jego poziomu optymalizacji.
W gcc, jeśli określisz
-O
, włączy następujące flagi optymalizacji :-fauto-inc-dec -fbranch-count-reg -fcombine-stack-adjustments -fcompare-elim -fcprop-registers -fdce -fdefer-pop ...
-fdce
oznacza eliminację martwego kodu .Możesz użyć,
__attribute__((used))
aby uniemożliwić gcc wyeliminowanie nieużywanej zmiennej z pamięcią statyczną:źródło