Dlaczego istnieje niestabilność?

222

Co robi volatile słowo kluczowe? W C ++ jaki problem rozwiązuje?

W moim przypadku nigdy tego świadomie nie potrzebowałem.

theschmitzer
źródło
Oto interesująca dyskusja na temat niestabilności w odniesieniu do wzorca Singleton: aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
chessguy
3
Istnieje intrygująca technika, która sprawia, że ​​Twój kompilator wykrywa możliwe warunki wyścigu, w dużym stopniu zależne od niestabilnego słowa kluczowego, o którym możesz przeczytać na stronie http://www.ddj.com/cpp/184403766 .
Neno Ganchev
Jest to dobry zasób z przykładem tego, kiedy volatilemożna go efektywnie wykorzystać, zestawiony w dość laicki sposób. Link: publications.gbdirect.co.uk/c_book/chapter8/…
Zoptymalizowany koder
Używam go do blokowania kodu bez blokady / podwójnej kontroli
paulm
Dla mnie volatilebardziej przydatne niż friendsłowo kluczowe.
acegs,

Odpowiedzi:

268

volatile jest potrzebny, jeśli czytasz z miejsca w pamięci, które, powiedzmy, całkowicie oddzielny proces / urządzenie / cokolwiek, do czego może pisać.

Kiedyś pracowałem z podwójnym portem RAM w systemie wieloprocesorowym w prostej C. Użyliśmy sprzętowo zarządzanej 16-bitowej wartości jako semafora, aby wiedzieć, kiedy skończył się drugi facet. Zasadniczo zrobiliśmy to:

void waitForSemaphore()
{
   volatile uint16_t* semPtr = WELL_KNOWN_SEM_ADDR;/*well known address to my semaphore*/
   while ((*semPtr) != IS_OK_FOR_ME_TO_PROCEED);
}

Bez volatiletego optymalizator uważa pętlę za bezużyteczną (facet nigdy nie ustawia wartości! On jest szalony, pozbywa się tego kodu!), A mój kod działałby bez uzyskania semafora, powodując problemy później.

Doug T.
źródło
W takim przypadku, co by się stało, gdyby uint16_t* volatile semPtrzamiast tego napisano? Powinno to oznaczać wskaźnik jako niestabilny (zamiast wskazywanej wartości), tak że kontrole samego wskaźnika, np. semPtr == SOME_ADDRMogą nie zostać zoptymalizowane. Implikuje to jednak również ponownie zmienną wartość punktową. Nie?
Zyl
@Zyl Nie, nie ma. W praktyce prawdopodobnie sugerujesz, co się stanie. Ale teoretycznie można skończyć z kompilatorem, który optymalizuje dostęp do wartości, ponieważ zdecydował, że żadna z tych wartości nigdy się nie zmieni. A jeśli miałeś na myśli niestabilność w odniesieniu do wartości, a nie wskaźnika, byłbyś przykręcony. Ponownie, mało prawdopodobne, ale lepiej jest popsuć się robieniem rzeczy, niż skorzystać z zachowań, które zdarzają się dzisiaj.
iheanyi
1
@Doug T. Lepszym wyjaśnieniem jest to
machineaddict
3
@curiousguy nie zdecydował się źle. Dokonał prawidłowego odliczenia na podstawie podanych informacji. Jeśli nie oznaczysz czegoś niestabilnego, kompilator może założyć, że nie jest niestabilny . To właśnie robi kompilator podczas optymalizacji kodu. Jeśli jest więcej informacji, a mianowicie, że wspomniane dane są w rzeczywistości niestabilne, odpowiedzialność za dostarczenie tych informacji ponosi programista. To, czego twierdzisz przez błędny kompilator, to po prostu złe programowanie.
iheanyi
1
@curiousguy nie, tylko dlatego, że słowo zmienne pojawia się raz, nie oznacza, że ​​wszystko nagle staje się niestabilne. Podałem scenariusz, w którym kompilator robi to, co właściwe i osiąga wynik, który jest sprzeczny z tym, czego błędnie oczekuje programista. Podobnie jak „najbardziej dokuczliwa analiza” nie jest oznaką błędu kompilatora, tak też nie jest w tym przypadku.
iheanyi
82

volatilejest potrzebny podczas opracowywania systemów wbudowanych lub sterowników urządzeń, w których należy odczytać lub zapisać urządzenie sprzętowe zamapowane w pamięci. Zawartość konkretnego rejestru urządzenia może ulec zmianie w dowolnym momencie, dlatego potrzebujesz volatilesłowa kluczowego, aby kompilator nie zoptymalizował takich dostępów.

ChrisN
źródło
9
Dotyczy to nie tylko systemów wbudowanych, ale także wszystkich sterowników sterowników.
Mladen Janković
Jedyny raz, kiedy potrzebowałem go na 8-bitowej magistrali ISA, gdzie dwa razy czytałeś ten sam adres - kompilator miał błąd i zignorował go (wczesny Zortech c ++)
Martin Beckett
Lotne bardzo rzadko jest odpowiednie do sterowania urządzeniami zewnętrznymi. Jego semantyka jest niewłaściwa dla współczesnego MMIO: musisz sprawić, by zbyt wiele obiektów było niestabilnych, a to boli optymalizację. Ale nowoczesne MMIO zachowuje się jak normalna pamięć, dopóki nie zostanie ustawiona flaga, więc zmienność nie powinna być potrzebna. Wielu kierowców nigdy nie używa lotnych.
ciekawy,
69

Niektóre procesory mają rejestry zmiennoprzecinkowe, które mają więcej niż 64 bity dokładności (np. 32-bitowy x86 bez SSE, patrz komentarz Petera). W ten sposób, jeśli uruchomisz kilka operacji na liczbach podwójnej precyzji, faktycznie uzyskasz odpowiedź o wyższej precyzji, niż gdybyś obciął każdy wynik pośredni do 64 bitów.

Jest to zwykle świetne, ale oznacza to, że w zależności od tego, w jaki sposób kompilator przypisał rejestry i dokonał optymalizacji, uzyskasz różne wyniki dla dokładnie tych samych operacji na dokładnie tych samych danych wejściowych. Jeśli potrzebujesz spójności, możesz zmusić każdą operację do powrotu do pamięci za pomocą niestabilnego słowa kluczowego.

Jest to również przydatne w przypadku niektórych algorytmów, które nie mają sensu algebraicznego, ale zmniejszają błąd zmiennoprzecinkowy, na przykład sumowanie Kahana. Algebraicznie jest to nop, więc często będzie niepoprawnie zoptymalizowane, chyba że niektóre zmienne pośrednie są zmienne.

tfinniga
źródło
5
Przy obliczaniu pochodnych numerycznych jest również przydatne, aby upewnić się, że x + h - x == h zdefiniujesz hh = x + h - x jako zmienną, aby można było obliczyć odpowiednią deltę.
Alexandre C.
5
+1, rzeczywiście z mojego doświadczenia wynika, że ​​obliczenia zmiennoprzecinkowe dały różne wyniki w Debugowaniu i Wydawaniu, więc testy jednostkowe napisane dla jednej konfiguracji zakończyły się niepowodzeniem dla innej. Rozwiązaliśmy go, zadeklarując jedną zmienną zmiennoprzecinkową jako volatile doublezamiast po prostu double, aby upewnić się, że jest ona skracana z precyzji FPU do 64-bitowej (RAM) przed kontynuowaniem dalszych obliczeń. Wyniki były zasadniczo różne ze względu na dalszą przesadę błędu zmiennoprzecinkowego.
Serge Rogatch,
Twoja definicja „nowoczesna” jest trochę nieprecyzyjna. Wpływa na to tylko 32-bitowy kod x86, który omija SSE / SSE2, i nie był „nowoczesny” nawet 10 lat temu. Wszystkie MIPS / ARM / POWER mają 64-bitowe rejestry sprzętowe, podobnie jak x86 z SSE2. Implementacje C ++ x86-64 zawsze używają SSE2, a kompilatory mają opcje takie jak g++ -mfpmath=sseużycie go również dla 32-bitowego x86. Można używać gcc -ffloat-storedo wymuszenia zaokrąglania wszędzie , nawet podczas korzystania z x87, czy można ustawić precyzję x87 dla 53-bitowej mantysy: randomascii.wordpress.com/2012/03/21/... .
Peter Cordes
Ale wciąż dobra odpowiedź, w przypadku przestarzałego kodu x87 można użyć volatilewymuszania zaokrąglania w kilku konkretnych miejscach bez utraty korzyści wszędzie.
Peter Cordes
1
Czy też mylę niedokładne z niekonsekwentnym?
Chipster
49

Z artykułu „Zmienna jak obietnica” Dana Saksa:

(...) obiekt lotny to taki, którego wartość może ulec spontanicznej zmianie. Oznacza to, że kiedy deklarujesz, że obiekt jest ulotny, informujesz kompilator, że obiekt może zmienić stan, nawet jeśli nie wydają się go zmieniać żadne instrukcje w programie. ”

Oto linki do trzech jego artykułów dotyczących volatilesłowa kluczowego:

MikeZ
źródło
23

MUSISZ używać niestabilnych podczas implementowania struktur danych bez blokady. W przeciwnym razie kompilator może optymalizować dostęp do zmiennej, która zmieni semantykę.

Innymi słowy, volatile mówi kompilatorowi, który uzyskuje dostęp do tej zmiennej, musi odpowiadać operacji odczytu / zapisu w pamięci fizycznej.

Na przykład tak deklaruje się InterlockedIncrement w API Win32:

LONG __cdecl InterlockedIncrement(
  __inout  LONG volatile *Addend
);
Frederik Slijkerman
źródło
Absolutnie NIE musisz deklarować zmiennej lotnej, aby móc korzystać z InterlockedIncrement.
ciekawy
Ta odpowiedź jest już nieaktualna, ponieważ C ++ 11 umożliwia std::atomic<LONG>bezpieczniejsze pisanie kodu bez blokady, bez problemów z optymalizacją czystych ładowań / czystych sklepów, zmianą kolejności lub cokolwiek innego.
Peter Cordes
10

Duża aplikacja, nad którą pracowałem na początku lat 90., zawierała obsługę wyjątków opartych na języku C przy użyciu setjmp i longjmp. Zmienne słowo kluczowe było konieczne w przypadku zmiennych, których wartości musiały zostać zachowane w bloku kodu, który służył jako klauzula „catch”, aby te zmienne nie zostały zapisane w rejestrach i usunięte przez longjmp.


źródło
10

W standardzie C jednym z miejsc do użycia volatilejest moduł obsługi sygnału. W rzeczywistości w standardzie C wszystko, co można bezpiecznie zrobić w module obsługi sygnału, to zmodyfikować volatile sig_atomic_tzmienną lub szybko wyjść. Rzeczywiście, AFAIK, jest to jedyne miejsce w standardzie C, którego użycie volatilejest wymagane, aby uniknąć nieokreślonego zachowania.

ISO / IEC 9899: 2011 §7.14.1.1 signal Funkcja

¶5 Jeśli sygnał występuje inaczej niż w wyniku wywołania funkcji abortlub raise, zachowanie jest niezdefiniowane, jeśli procedura obsługi sygnału odnosi się do dowolnego obiektu o statycznym lub czasowym przechowywaniu wątku, który nie jest obiektem atomowym bez blokady, innym niż poprzez przypisanie wartości do obiektu zadeklarowanego jako volatile sig_atomic_tlub procedura obsługi sygnału wywołuje dowolną funkcję w bibliotece standardowej inną niż abortfunkcja, _Exitfunkcja, quick_exitfunkcja lub signalfunkcja z pierwszym argumentem równym liczbie sygnałów odpowiadającej sygnałowi, który spowodował wywołanie treser. Ponadto, jeśli takie wywołanie signalfunkcji powoduje powrót SIG_ERR, wartość errnojest nieokreślona. 252)

252) Jeśli asynchroniczny moduł obsługi sygnału generuje jakikolwiek sygnał, zachowanie jest niezdefiniowane.

Oznacza to, że w standardzie C możesz pisać:

static volatile sig_atomic_t sig_num = 0;

static void sig_handler(int signum)
{
    signal(signum, sig_handler);
    sig_num = signum;
}

i niewiele więcej.

POSIX jest znacznie łagodniejszy w kwestii tego, co można zrobić w procedurze obsługi sygnałów, ale wciąż istnieją ograniczenia (jednym z ograniczeń jest to, że printf()nie można bezpiecznie używać standardowej biblioteki we / wy - i innych).

Jonathan Leffler
źródło
7

Opracowując dla osadzonego, mam pętlę, która sprawdza zmienną, którą można zmienić w module obsługi przerwań. Bez „niestabilnej” pętla staje się noop - o ile kompilator może powiedzieć, zmienna nigdy się nie zmienia, więc optymalizuje sprawdzenie.

To samo dotyczyłoby zmiennej, która może zostać zmieniona w innym wątku w bardziej tradycyjnym środowisku, ale tam często wykonujemy wywołania synchronizacji, więc kompilator nie jest tak wolny od optymalizacji.


źródło
7

Użyłem go w kompilacjach debugowania, gdy kompilator nalega na zoptymalizowanie zmiennej, którą chcę widzieć, gdy przechodzę przez kod.

wcięcie
źródło
7

Oprócz używania go zgodnie z przeznaczeniem, lotny jest wykorzystywany w metaprogramowaniu (szablonowym). Można go użyć, aby zapobiec przypadkowemu przeładowaniu, ponieważ zmienny atrybut (jak const) bierze udział w rozwiązywaniu problemu przeciążenia.

template <typename T> 
class Foo {
  std::enable_if_t<sizeof(T)==4, void> f(T& t) 
  { std::cout << 1 << t; }
  void f(T volatile& t) 
  { std::cout << 2 << const_cast<T&>(t); }

  void bar() { T t; f(t); }
};

To jest legalne; oba przeciążenia są potencjalnie wywoływalne i robią prawie to samo. Obsada w volatileprzeciążeniu jest legalna, ponieważ wiemy, że pasek i tak nie przejdzie nieulotnego T. volatileWersja jest surowo gorzej, choć, więc nigdy wybrany w rozdzielczości przeciążenia jeśli nieulotnaf jest dostępna.

Pamiętaj, że kod nigdy nie zależy od volatiledostępu do pamięci.

MSalters
źródło
Czy mógłbyś wyjaśnić to na przykładzie? To naprawdę pomogłoby mi lepiej zrozumieć. Dzięki!
batbrat
Obsada w zmiennym przeciążeniu ” Obsada jest wyraźną konwersją. To konstrukcja SYNTAX. Wiele osób wprowadza zamieszanie (nawet standardowi autorzy).
ciekawy
6
  1. musisz go użyć do implementacji spinlocków, a także niektórych (wszystkich?) struktur danych bez blokady
  2. używaj go z operacjami atomowymi / instrukcjami
  3. pomógł mi raz pokonać błąd kompilatora (niepoprawnie wygenerowany kod podczas optymalizacji)
Mladen Janković
źródło
5
Lepiej jest użyć biblioteki, wbudowanego kompilatora lub wbudowanego kodu asemblera. Lotne jest zawodne.
Zan Lynx
1
Zarówno 1, jak i 2 wykorzystują operacje atomowe, ale lotność nie zapewnia semantyki atomowej, a implementacje atomowe specyficzne dla platformy zastąpią potrzebę użycia lotności, więc w przypadku 1 i 2, nie zgadzam się, NIE potrzebujesz do nich lotności.
Kto mówi cokolwiek o niestabilności zapewniającej semantykę atomową? Powiedziałem, że musisz UŻYWAĆ lotnych Z operacjami atomowymi, a jeśli nie uważasz, że to prawda, spójrz na deklaracje powiązanych operacji Win32 API (ten facet również to wyjaśnił w swojej odpowiedzi)
Mladen Janković
4

Słowo volatilekluczowe ma zapobiegać stosowaniu przez kompilator optymalizacji na obiektach, które mogą się zmieniać w sposób niemożliwy do określenia przez kompilator.

Obiekty zadeklarowane jako volatilesą pominięte w optymalizacji, ponieważ ich wartości można w dowolnym momencie zmienić za pomocą kodu poza zakresem bieżącego kodu. System zawsze odczytuje bieżącą wartość volatileobiektu z miejsca w pamięci, zamiast utrzymywać jego wartość w rejestrze tymczasowym w punkcie, w którym jest wymagany, nawet jeśli poprzednia instrukcja prosiła o wartość z tego samego obiektu.

Rozważ następujące przypadki

1) Zmienne globalne zmodyfikowane przez procedurę obsługi przerwań poza zakresem.

2) Zmienne globalne w aplikacji wielowątkowej.

Jeśli nie użyjemy zmiennego kwalifikatora, mogą pojawić się następujące problemy

1) Kod może nie działać zgodnie z oczekiwaniami po włączeniu optymalizacji.

2) Kod może nie działać zgodnie z oczekiwaniami, gdy przerwania są włączone i używane.

Lotny: najlepszy przyjaciel programisty

https://en.wikipedia.org/wiki/Volatile_(computer_programming)

roottraveller
źródło
Link, który opublikowałeś jest bardzo nieaktualny i nie odzwierciedla aktualnych najlepszych praktyk.
Tim Seguine,
2

Oprócz tego, że zmienne słowo kluczowe jest używane do informowania kompilatora, aby nie optymalizował dostępu do niektórych zmiennych (które mogą być modyfikowane przez wątek lub procedurę przerwania), może być również używany do usuwania niektórych błędów kompilatora - TAK, może być ---.

Na przykład pracowałem na platformie osadzonej, w której kompilator dokonywał błędnych przypuszczeń dotyczących wartości zmiennej. Jeśli kod nie został zoptymalizowany, program działałby poprawnie. W przypadku optymalizacji (które były naprawdę potrzebne, ponieważ była to krytyczna procedura) kod nie działałby poprawnie. Jedynym rozwiązaniem (choć niezbyt poprawnym) było zadeklarowanie zmiennej „wadliwej” jako niestabilnej.

INS
źródło
3
Błędne jest założenie, że kompilator nie optymalizuje dostępu do składników lotnych. Standard nic nie wie o optymalizacjach. Kompilator jest zobowiązany do przestrzegania tego, co nakazuje standard, ale można dokonywać wszelkich optymalizacji, które nie zakłócają normalnego zachowania.
Terminus,
3
Z mojego doświadczenia wynika, że ​​99,9% wszystkich „błędów” optymalizacji w ramieniu gcc to błędy programisty. Nie mam pojęcia, czy dotyczy to tej odpowiedzi. Tylko rant na temat ogólny
odinthenerd
@ Terminus „ To błędne założenie, że kompilator nie optymalizuje dostępu do składników lotnych ” Źródło?
ciekawy,
2

Twój program wydaje się działać nawet bez volatilesłowa kluczowego? Być może jest to powód:

Jak wspomniano wcześniej, volatilesłowo kluczowe pomaga w przypadkach takich jak

volatile int* p = ...;  // point to some memory
while( *p!=0 ) {}  // loop until the memory becomes zero

Ale wydaje się, że prawie nie ma żadnego efektu po wywołaniu funkcji zewnętrznej lub nieliniowej. Na przykład:

while( *p!=0 ) { g(); }

Następnie volatilegenerowany jest prawie taki sam wynik z lub bez .

Tak długo, jak g () można całkowicie wstawić, kompilator może zobaczyć wszystko, co się dzieje, i dlatego może zoptymalizować. Ale kiedy program wywołuje miejsce, w którym kompilator nie widzi, co się dzieje, kompilator nie może już przyjmować żadnych założeń. Dlatego kompilator wygeneruje kod, który zawsze odczytuje bezpośrednio z pamięci.

Ale uważaj na dzień, w którym funkcja g () stanie się wbudowana (albo z powodu wyraźnych zmian, albo z powodu sprytności kompilatora / linkera), więc kod może się zepsuć, jeśli zapomnisz volatilesłowa kluczowego!

Dlatego zalecam dodanie volatilesłowa kluczowego, nawet jeśli Twój program wydaje się działać bez niego. Sprawia, że ​​intencja jest jaśniejsza i bardziej solidna w odniesieniu do przyszłych zmian.

Joachim
źródło
Zauważ, że funkcja może mieć wstawiony kod podczas generowania odwołania (rozwiązanego w czasie łączenia) do funkcji konspektu; będzie tak w przypadku częściowo wbudowanej funkcji rekurencyjnej. Funkcja może również mieć semantyczne „wstawione” przez kompilator, to znaczy, że kompilator zakłada, że ​​skutki uboczne i wynik mieszczą się w możliwych skutkach ubocznych i możliwych wynikach zgodnie z jego kodem źródłowym, ale nadal go nie inliniują. Opiera się to na „skutecznej regule jednej definicji”, która stanowi, że wszystkie definicje jednostki muszą być faktycznie równoważne (jeśli nie dokładnie identyczne).
ciekawy
Unikanie przenośnego wstawiania wywołania (lub „wstawiania” jego semantycznego) przez funkcję, której ciało jest widoczne przez kompilator (nawet w czasie łączenia z globalną optymalizacją) jest możliwe przy użyciu volatilewskaźnika kwalifikowanej funkcji:void (* volatile fun_ptr)() = fun; fun_ptr();
ciekawy
2

We wczesnych dniach C kompilatory interpretują wszystkie działania, które odczytują i zapisują wartości lv jako operacje pamięciowe, wykonywane w tej samej kolejności, w jakiej odczyty i zapisy pojawiły się w kodzie. Wydajność można by znacznie poprawić w wielu przypadkach, gdyby kompilatory miały pewną swobodę zmiany kolejności i konsolidacji operacji, ale był z tym problem. Nawet operacje były często określane w określonej kolejności tylko dlatego, że konieczne było ich podanie w niektórych kolejności, dlatego programista wybrał jedną z wielu równie dobrych alternatyw, co nie zawsze tak było. Czasami ważne jest, aby pewne operacje zachodziły w określonej kolejności.

To, które szczegóły sekwencjonowania są ważne, będzie się różnić w zależności od platformy docelowej i pola zastosowania. Zamiast zapewniać szczególnie szczegółową kontrolę, Standard zdecydował się na prosty model: jeśli sekwencja dostępu jest wykonywana z wartościami niekwalifikowanymi volatile, kompilator może zmienić ich kolejność i konsolidować według własnego uznania. Jeśli akcja jest wykonywana przy użyciu volatilekwalifikowanej wartości, implementacja wysokiej jakości powinna oferować wszelkie dodatkowe gwarancje porządkowania, które mogą być wymagane przez kod ukierunkowany na docelową platformę i pole aplikacji, bez konieczności stosowania niestandardowej składni.

Niestety, zamiast określać, jakich gwarancji będą potrzebować programiści, wielu kompilatorów zdecydowało się zaoferować gwarancje minimalne wymagane przez standard. To sprawia, że volatilejest mniej użyteczny niż powinien. Na przykład na gcc lub clang programista, który musi wdrożyć podstawowy „mutex hand-off” [taki, w którym zadanie, które nabyło i wydało mutex, nie zrobi tego ponownie, dopóki inne zadanie tego nie zrobi] musi to zrobić z czterech rzeczy:

  1. Umieść akwizycję i wydanie muteksu w funkcji, której kompilator nie może wstawić i do której nie może zastosować optymalizacji całego programu.

  2. Zakwalifikuj wszystkie obiekty strzeżone przez volatilemuteks jako - coś, co nie powinno być konieczne, jeśli wszystkie dostępy wystąpią po uzyskaniu muteksu i przed jego zwolnieniem.

  3. Użyj poziomu optymalizacji 0, aby wymusić na kompilatorze generowanie kodu tak, jakby wszystkie obiekty, które nie registersą kwalifikowane, były volatile.

  4. Użyj dyrektyw specyficznych dla gcc.

Natomiast w przypadku korzystania z kompilatora wyższej jakości, który jest bardziej odpowiedni do programowania systemów, takich jak icc, istnieje inna opcja:

  1. Upewnij się, że volatilekwalifikowane zapisy będą wykonywane wszędzie tam, gdzie potrzebne jest nabycie lub wydanie.

Zdobycie podstawowego „mutexu hand-off” wymaga volatileodczytu (aby zobaczyć, czy jest gotowy) i nie powinno również wymagać volatilezapisu (druga strona nie będzie próbowała go odzyskać, dopóki nie zostanie oddana), ale musi wykonanie bezsensownego volatilezapisu jest wciąż lepsze niż dowolna z opcji dostępnych w gcc lub clang.

supercat
źródło
1

Jednym z zastosowań, które powinienem przypomnieć, jest to, że w funkcji obsługi sygnału, jeśli chcesz uzyskać dostęp do / zmodyfikować zmienną globalną (na przykład oznaczyć ją jako exit = true), musisz zadeklarować tę zmienną jako „niestabilną”.

król błędów
źródło
1

Wszystkie odpowiedzi są doskonałe. Ale przede wszystkim chciałbym podzielić się przykładem.

Poniżej znajduje się mały program cpp:

#include <iostream>

int x;

int main(){
    char buf[50];
    x = 8;

    if(x == 8)
        printf("x is 8\n");
    else
        sprintf(buf, "x is not 8\n");

    x=1000;
    while(x > 5)
        x--;
    return 0;
}

Teraz wygenerujmy zestaw powyższego kodu (i wkleję tylko te części zestawu, które są tutaj istotne):

Polecenie generowania zestawu:

g++ -S -O3 -c -fverbose-asm -Wa,-adhln assembly.cpp

I montaż:

main:
.LFB1594:
    subq    $40, %rsp    #,
    .seh_stackalloc 40
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC0(%rip), %rcx     #,
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:10:         printf("x is 8\n");
    call    _ZL6printfPKcz.constprop.0   #
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    movl    $5, x(%rip)  #, x
    addq    $40, %rsp    #,
    ret 
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

W zestawie można zobaczyć, że kod zestawu nie został wygenerowany, sprintfponieważ kompilator założył, że xnie zmieni się on poza programem. To samo dotyczy whilepętli. whilePętla została całkowicie usunięta z powodu optymalizacji, ponieważ kompilator widział to jako bezużyteczny kod, a zatem bezpośrednio przypisany 5do x(patrz movl $5, x(%rip)).

Problem występuje, gdy co jeśli zewnętrzny proces / sprzęt zmieniłby wartość xgdzieś pomiędzy x = 8;a if(x == 8). Spodziewalibyśmy się, że elseblok zadziała, ale niestety kompilator usunął tę część.

Teraz, aby rozwiązać ten problem assembly.cpp, przejdźmy int x;do volatile int x;i wygenerujmy kod asemblera wygenerowany:

main:
.LFB1594:
    subq    $104, %rsp   #,
    .seh_stackalloc 104
    .seh_endprologue
 # assembly.cpp:5: int main(){
    call    __main   #
 # assembly.cpp:7:     x = 8;
    movl    $8, x(%rip)  #, x
 # assembly.cpp:9:     if(x == 8)
    movl    x(%rip), %eax    # x, x.1_1
 # assembly.cpp:9:     if(x == 8)
    cmpl    $8, %eax     #, x.1_1
    je  .L11     #,
 # assembly.cpp:12:         sprintf(buf, "x is not 8\n");
    leaq    32(%rsp), %rcx   #, tmp93
    leaq    .LC0(%rip), %rdx     #,
    call    _ZL7sprintfPcPKcz.constprop.0    #
.L7:
 # assembly.cpp:14:     x=1000;
    movl    $1000, x(%rip)   #, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_15
    cmpl    $5, %eax     #, x.3_15
    jle .L8  #,
    .p2align 4,,10
.L9:
 # assembly.cpp:16:         x--;
    movl    x(%rip), %eax    # x, x.4_3
    subl    $1, %eax     #, _4
    movl    %eax, x(%rip)    # _4, x
 # assembly.cpp:15:     while(x > 5)
    movl    x(%rip), %eax    # x, x.3_2
    cmpl    $5, %eax     #, x.3_2
    jg  .L9  #,
.L8:
 # assembly.cpp:18: }
    xorl    %eax, %eax   #
    addq    $104, %rsp   #,
    ret 
.L11:
 # assembly.cpp:10:         printf("x is 8\n");
    leaq    .LC1(%rip), %rcx     #,
    call    _ZL6printfPKcz.constprop.1   #
    jmp .L7  #
    .seh_endproc
    .p2align 4,,15
    .def    _GLOBAL__sub_I_x;   .scl    3;  .type   32; .endef
    .seh_proc   _GLOBAL__sub_I_x

Tutaj widać, że kody do montażu sprintf, printforaz whilepętla zostały wygenerowane. Zaletą jest to, że jeśli xzmienna zostanie zmieniona przez jakiś zewnętrzny program lub sprzęt, sprintfczęść kodu zostanie wykonana. Podobnie, whilepętla może być teraz używana do zajętego oczekiwania.

Rohit
źródło
0

Inne odpowiedzi już wspominają o unikaniu optymalizacji w celu:

  • użyj rejestrów zmapowanych w pamięci (lub „MMIO”)
  • napisz sterowniki urządzeń
  • umożliwiają łatwiejsze debugowanie programów
  • uczynić obliczenia zmiennoprzecinkowe bardziej deterministycznymi

Zmienna jest niezbędna, gdy potrzebujesz wartości, która wydaje się pochodzić z zewnątrz i być nieprzewidywalna, i unikać optymalizacji kompilatora w oparciu o znaną wartość, a gdy wynik nie jest faktycznie używany, ale trzeba go obliczyć lub jest on używany, ale chcesz go obliczyć kilka razy dla testu porównawczego i potrzebujesz, aby obliczenia rozpoczynały się i kończyły w ściśle określonych punktach.

Odczyt zmienny jest jak operacja wprowadzania (podobna scanflub zastosowanie cin): wydaje się, że wartość pochodzi z zewnątrz programu, więc każde obliczenie zależne od wartości musi rozpocząć się po niej .

Zapis niestabilny jest jak operacja wyjściowa (podobna printflub zastosowanie cout): wartość wydaje się być przekazywana poza programem, więc jeśli wartość zależy od obliczeń, należy ją wcześniej zakończyć .

Tak więc parę niestabilnych odczytów / zapisów można wykorzystać do oswojenia testów i nadania sensowi pomiaru czasu .

Bez niestabilności obliczenia mogłyby być wcześniej uruchomione przez kompilator, ponieważ nic nie stoi na przeszkodzie zmianie kolejności obliczeń za pomocą funkcji takich jak pomiar czasu .

ciekawy
źródło