Czy kompilator może to zoptymalizować (zgodnie ze standardem C ++ 17):
int fn() {
volatile int x = 0;
return x;
}
do tego?
int fn() {
return 0;
}
Jeśli tak, dlaczego? Jeśli nie, dlaczego nie?
Oto kilka przemyśleń na ten temat: obecne kompilatory kompilują się fn()
jako zmienna lokalna umieszczana na stosie, a następnie zwracają ją. Na przykład na x86-64, gcc tworzy to:
mov DWORD PTR [rsp-0x4],0x0 // this is x
mov eax,DWORD PTR [rsp-0x4] // eax is the return register
ret
O ile wiem, standard nie mówi, że lokalna zmienna zmienna powinna być umieszczona na stosie. Tak więc ta wersja byłaby równie dobra:
mov edx,0x0 // this is x
mov eax,edx // eax is the return
ret
Tutaj edx
sklepy x
. Ale teraz, po co się tu zatrzymać? Jak edx
i eax
to zarówno zero, możemy tylko powiedzieć:
xor eax,eax // eax is the return, and x as well
ret
I przeszliśmy fn()
do wersji zoptymalizowanej. Czy ta transformacja jest ważna? Jeśli nie, który krok jest nieprawidłowy?
Odpowiedzi:
Nie. Dostęp do
volatile
obiektów jest uważany za obserwowalne zachowanie, dokładnie tak, jak we / wy, bez szczególnego rozróżnienia między lokalnymi i globalnymi.N3690, [intro.execution], ¶8
To , w jaki sposób można to dokładnie zaobserwować, wykracza poza zakres standardu i dotyczy bezpośrednio obszaru specyficznego dla implementacji, dokładnie tak jak we / wy i dostęp do
volatile
obiektów globalnych .volatile
oznacza "myślisz, że wiesz wszystko, co się tutaj dzieje, ale tak nie jest; zaufaj mi i rób to bez bycia zbyt sprytnym, ponieważ jestem w twoim programie i robię swoje tajne rzeczy z twoimi bajtami". Faktycznie jest to wyjaśnione w [dcl.type.cv] ¶7:źródło
Ta pętla może zostać zoptymalizowana za pomocą reguły as-if, ponieważ nie ma obserwowalnego zachowania:
for (unsigned i = 0; i < n; ++i) { bool looped = true; }
Ten nie może:
for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }
Druga pętla robi coś w każdej iteracji, co oznacza, że pętla zajmuje O (n) czasu. Nie mam pojęcia, jaka jest stała, ale mogę ją zmierzyć i wtedy mam sposób na zapętlenie przez (mniej lub bardziej) znany czas.
Mogę to zrobić, ponieważ standard mówi, że dostęp do substancji lotnych musi mieć miejsce, w porządku. Gdyby kompilator zdecydował, że w tym przypadku standard nie ma zastosowania, myślę, że miałbym prawo zgłosić błąd.
Jeśli kompilator zdecyduje się umieścić
looped
w rejestrze, przypuszczam, że nie mam przeciwko temu dobrego argumentu. Ale nadal musi ustawić wartość tego rejestru na 1 dla każdej iteracji pętli.źródło
xor ax, ax
(gdzieax
jest uważana za znajdującą sięvolatile x
) w pytaniu jest ważna, czy nieważna? IOW, jaka jest twoja odpowiedź na pytanie?xor ax, ax
tego, tego kodu nie można wyeliminować, nawet jeśli wygląda na bezużyteczny, ani nie można go scalić. W moim przykładzie pętli skompilowany kod musiałby zostać wykonanyxor ax, ax
n razy, aby spełnić regułę obserwowalnego zachowania. Mam nadzieję, że zmiana odpowiada na Twoje pytanie.volatile
obiektach są - same w sobie - rodzajem efektu ubocznego. Implementacja mogłaby zdefiniować ich semantykę w sposób, który nie wymagałby od nich generowania żadnych rzeczywistych instrukcji procesora, ale pętla, która uzyskuje dostęp do obiektu o zmiennej zmienności, ma efekty uboczne, a zatem nie kwalifikuje się do usunięcia.Błagam o odmowę zdania większości, pomimo pełnego zrozumienia, które
volatile
oznacza obserwowalne I / O.Jeśli masz ten kod:
{ volatile int x; x = 0; }
Uważam, że kompilator może go zoptymalizować zgodnie z regułą as-if , zakładając, że:
W
volatile
innym przypadku zmienna nie jest widoczna na zewnątrz za pomocą np. Wskaźników (co oczywiście nie stanowi tutaj problemu, ponieważ nie ma czegoś takiego w danym zakresie)Kompilator nie zapewnia mechanizmu zewnętrznego dostępu do tego
volatile
Uzasadnieniem jest po prostu to, że i tak nie można było zaobserwować różnicy ze względu na kryterium nr 2.
Jednak w twoim kompilatorze kryterium nr 2 może nie być spełnione ! Kompilator może próbować zapewnić dodatkowe gwarancje dotyczące obserwacji
volatile
zmiennych z „zewnątrz”, na przykład poprzez analizę stosu. W takich sytuacjach zachowanie jest naprawdę obserwowalne, więc nie można go zoptymalizować.Teraz pytanie brzmi, czy poniższy kod różni się od powyższego?
{ volatile int x = 0; }
Wydaje mi się, że zaobserwowałem inne zachowanie tego w Visual C ++ w odniesieniu do optymalizacji, ale nie jestem do końca pewien z jakich powodów. Być może inicjalizacja nie liczy się jako „dostęp”? Nie jestem pewny. To może być warte osobnego pytania, jeśli jesteś zainteresowany, ale poza tym uważam, że odpowiedź jest taka, jak wyjaśniłem powyżej.
źródło
Teoretycznie mógłby to zrobić program obsługi przerwań
fn()
funkcji. Może uzyskać dostęp do tablicy symboli lub numerów linii źródłowych za pośrednictwem oprzyrządowania lub dołączonych informacji debugowania.x
, która byłaby przechowywana z przewidywalnym przesunięciem względem wskaźnika stosu.… W ten sposób
fn()
zwracając wartość różną od zera.źródło
fn()
. Użycievolatile
tworzy kod-gen, który jest podobnygcc -O0
do tej zmiennej: spill / reload pomiędzy każdą instrukcją C. (-O0
nadal można łączyć wiele dostępów w jednej instrukcji bez naruszania spójności debugera, alevolatile
nie jest to dozwolone).x
? A co, jeśli na x86-64xor rax, rax
przechowuje zero (mam na myśli, że rejestr wartości zwracanej przechowujex
), co oczywiście może być łatwo obserwowane / modyfikowane przez debugger (tj. Informacje o symbolach debugowaniax
są przechowywane wrax
). Czy to narusza standard?fn()
może być wstawione. Z MSVC 2017 i domyślnym trybem wydania jest. Nie ma więc „w ramachfn()
funkcji”. Niezależnie od tego, ponieważ zmienna jest zapisywana automatycznie, nie ma „przewidywalnego przesunięcia”.volatile
, i ponieważvolatile
nie zmusza go do zapewnienia takiej obsługi. Dlatego usuwam głos przeciw (myliłem się), ale nie głosuję za, ponieważ myślę, że ten tok rozumowania nie wyjaśnia.Dodam tylko szczegółowe odniesienie do reguły as-if i zmiennej niestabilnej słowa kluczowego . (Na dole tych stron postępuj zgodnie z instrukcjami „zobacz także” i „Referencje”, aby prześledzić oryginalne specyfikacje, ale wydaje mi się, że cppreference.com jest znacznie łatwiejszy do odczytania / zrozumienia).
W szczególności chcę, abyś przeczytał tę sekcję
Więc zmienne słowo kluczowe szczególności wyłączania optymalizacji kompilatora na glvalues . Jedyną rzeczą, na którą słowo kluczowe volatile może mieć wpływ, jest prawdopodobnie to
return x
, że kompilator może zrobić, co zechce, z resztą funkcji.To, jak bardzo kompilator może zoptymalizować zwrot, zależy od tego, jak bardzo kompilator może zoptymalizować dostęp x w tym przypadku (ponieważ nie zmienia kolejności, a ściśle mówiąc, nie usuwa wyrażenia zwrotnego. Jest dostęp , ale to odczytuje i zapisuje na stosie, co powinno być w stanie usprawnić.) Więc kiedy to czytam, jest to szary obszar określający, ile kompilator może optymalizować, i można go łatwo spierać w obie strony.
Uwaga dodatkowa: w takich przypadkach zawsze zakładaj, że kompilator zrobi odwrotnie niż chciałeś / potrzebowałeś. Powinieneś albo wyłączyć optymalizację (przynajmniej dla tego modułu), albo spróbować znaleźć bardziej zdefiniowane zachowanie dla tego, co chcesz. (To jest również powód, dla którego testowanie jednostkowe jest tak ważne) Jeśli uważasz, że to wada, powinieneś porozmawiać o tym z twórcami C ++.
To wszystko jest nadal bardzo trudne do odczytania, więc spróbuj uwzględnić to, co uważam za istotne, abyś mógł przeczytać to sam.
zasada jak gdyby
Jeśli chcesz przeczytać specyfikacje, uważam, że to są te, które musisz przeczytać
źródło
_AddressOfReturnAddress
obejmuje na przykład analizę stosu. Ludzie analizują stos z ważnych powodów i niekoniecznie dlatego, że sama funkcja polega na nim pod względem poprawności.return x;
Myślę, że nigdy nie widziałem zmiennej lokalnej używającej volatile, która nie byłaby wskaźnikiem do zmiennej volatile. Jak w:
int fn() { volatile int *x = (volatile int *)0xDEADBEEF; *x = 23; // request data, 23 = temperature return *x; // return temperature }
Jedyne inne przypadki niestabilności, jakie znam, używają globalnego, który jest zapisany w obsłudze sygnału. Nie ma tam żadnych wskazówek. Lub dostęp do symboli zdefiniowanych w skrypcie konsolidatora pod określonymi adresami dotyczącymi sprzętu.
O wiele łatwiej jest tam uzasadnić, dlaczego optymalizacja miałaby zmienić obserwowalne efekty. Ale ta sama reguła dotyczy lokalnej zmiennej lotnej. Kompilator musi zachowywać się tak, jakby dostęp do x był obserwowalny i nie może go zoptymalizować.
źródło
x
w Twoim kodzie jest „lokalna zmienna zmienna”. Tak nie jest.volatile
i nie ma nic wspólnego z tym, że jest lokalny. Równie dobrze mógłby miećstatic volatile int *const x = ...
zasięg globalny i wszystko, co powiesz, byłoby dokładnie takie samo. To jest jak dodatkowa wiedza, która jest niezbędna do zrozumienia pytania, na które chyba nie każdy ma, ale nie jest to prawdziwa odpowiedź.