Elektroda.pl
Elektroda.pl
X
Proszę, dodaj wyjątek www.elektroda.pl do Adblock.
Dzięki temu, że oglądasz reklamy, wspierasz portal i użytkowników.

[C++] - Ulotność (volatile) obiektu a ulotność jego składników

szczywronek 06 Lut 2016 17:07 717 15
  • #1 06 Lut 2016 17:07
    szczywronek
    Poziom 27  

    Witam,
    mam pytanie dotyczące działania modyfikatora volatile zastosowanego przy definiowaniu obiektu klasy/struktury. Książki/poradniki, na które trafiłem, nie tłumaczą zagadnienia zbyt dokładnie, zaś lektura standardu języka mnie przerasta :)

    Na logikę i zdrowy rozsądek, wydawało mi się, że "ulotność" obiektu implikuje "ulotność" wszystkich jego składników. Potwierdzenie tej teorii znalazłem na stronie www.cppreference.com, cytat:

    Cytat:
    volatile object - an object whose type is volatile-qualified, or a subobject of a volatile object
    (źródło)

    Jednak podczas eksperymentów trafiłem na coś, co nie pasuje do tej teorii.

    W poniższym kodzie definiuję ulotny obiekt klasy K. W konstruktorze składnik "a" jest inicjalizowany stałą wartością 1. Zgodnie z moją teorią, ulotność obiektu (jako całości) powinna pociągać za sobą ulotność składników - w tym zmiennej "a". Tak się jednak nie dzieje - optymalizator wyrzuca inicjalizację zmiennej. Pomaga dopiero dopisanie volatile w ciele klasy, przy zmiennej "a".
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Proszę o podpowiedź, w którym miejscu mój tok myślenia zbacza z właściwej drogi ;)

    0 15
  • #2 06 Lut 2016 21:36
    Flesz
    Poziom 20  

    Volatile Oznacza że kompilator ma tej zmiennej nie optymalizować, bo zmiana wartości zmiennej może być niezależna od programu. Polecam Książkę symfonia C++ Jerzego Grębosza - tam to jest obrazowo wytłumaczone.

    0
  • #3 07 Lut 2016 11:02
    krru
    Poziom 32  

    Trochę mieszasz poziomy - kompilator generując kod klasy nie wie czy zadeklarowane gdzieś indziej instancje tej klasy będą volatile czy nie. A kod konstruktora tej klasy jest jeden, kompilator nie tworzy oddzielnego kodu dla zmiennych volatile, const i zwykłych.
    Można spróbować zastować modyfikator volatile do poszczególnych metod. Być może wtedy kompilator zastosował by wariant volatile do zmniennych volatile.
    np.

    Kod: c
    Zaloguj się, aby zobaczyć kod


    Nawet jeśli treść obu metod będzie jednakowa (na poziomie kodu źródłowego)
    to kompilator może inaczej je skompilować, ze względu właśnie na volatile,
    a mechanizm przeciążania funkcji spowoduje użycie pierwszego wariantu do zmiennych volatile, a drugiego do pozostałych. Nie wiem czy to przejdzie z konstruktorem.

    0
  • #4 07 Lut 2016 12:34
    szczywronek
    Poziom 27  

    @Flesz - dziękuję za odpowiedź. Niestety nie do końca dotyka ona sedna problemu ;)
    @krru - ok. Wiem, że coś mieszam bo nic mi się nie zgadza :roll: Co w takim razie oznacza volatile przy definiowaniu obiektu klasy? Tylko to, że na rzecz takiego obiektu mogą być wywołane jedynie metody volatile? Tak twierdzi np. wspomniana Symfonia, ale to się nie sprawdza w praktyce. W poniższym programie, kompilator traktuje zmienną "a" jako ulotną:

    Kod: c
    Zaloguj się, aby zobaczyć kod

    Czyli ulotność obiektu "ob" pociągnęła za sobą ulotne traktowanie zmiennej "a" - to pasuje do cytatu z www.cppreference.com (z pierwszego posta), ale nijak się ma do tego co jest np. w Symfonii (ulotny obiekt to tylko stosowanie ulotnych metod) i do poprzedniego przykładu z konstruktorem (obiekt był ulotny, a zmienna i tak wyleciała).

    Inicjalizacja z konstruktora nie jest wyrzucana dopiero przy takim zapisie:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Wyrzucenie któregokolwiek z volatile'i powoduje, że kompilator wywala inicjalizację zmiennej. Żeby już mi się wszystko pomieszało, to taki zapis:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Nie powoduje "ulotnego" traktowania zmiennej "a".

    Tak sobie podejrzewam, że część mojego zamieszania bierze się z trywialności przykładów. Czy kompilator widząc ulotną, automatyczną zmienną lokalną ma prawo ją wyoptymalizować? Zauważyłem, że sposób dostępu do pamięci (np. w powyższych przykładach) zmienia się diametralnie, gdy obiekt klasy jest definiowany jako globalny/statyczny - wtedy kompilator nie jest już taki odważny w optymalizowaniu dostępu.

    0
  • #5 07 Lut 2016 18:16
    krru
    Poziom 32  

    Z tego co sprawdziłem (być może można to zmienić opcjami kompilatora) C toleruje podanie wskaźnika na zmienną volatile do funkcji, która przyjmuje zwykły wskaźnik, dostaje się warning. C++ takie coś traktuje jako błąd. Z tym, że w C++ jest przeciążanie funkcji i można dać dwie funkcje/metody o tej samej nazwie a różnych parametrach, w tym dla obiektów volatile i zwykłych.

    Problem z obiektami jest taki sam jak z funkcjami, które dostają jakieś zmienne jako wskaźniki poprzez parametr. Funkcja jest kompilowana raz i kompilator nie wie na jakiej zmiennej zostanie wykonana. W klasach jest to logicznie spójne - to metody klasy definiują jak mają być wykonywane operacje na klasie. Na zewnątrz kompilator zakłada, że klasa i jej metody są poprawnie zdefiniowana.

    Jak sprawdziłem kompilator nie dopuszcza oznaczenia konstruktora słowem const ani volatile.

    Fakt, że częściowo problemy jakie rozważasz biorą się z dziwnych zastosować słowa volatile. Twoje przykłady właśnie tego dotyczą np. w tym środkowym wywalenie volatile z deklaracji samej zmiennej w main oznacza defacto, że kompilator zoptymalizował w ogóle istnienie zmiennej i jako nieużywaną ją wywalił.

    Konstruktora nie należy traktować jako zwykłą metodę, coś wywoływanego w przewidywalny sposób. To jest informacja dla kompilatora w jaki sposób na zainicjować zmienną danego typu w zależności od kontekstu (konstruktor zwykły kopiujący itp). Kompilator w pewnych okolicznościach może 'dopisać sobie' konstruktor, jeśli użytkownik tego nie zrobił. Dlatego też operacji z efektami ubocznymi nie należy umieszczać w konstruktorach. Bo różne zmienne tymczasowe wewnątrz wyrażeń mogą inicjowane w dziwnych miejscach.

    Nie rozumiem twojej uwago co do nieulotnego traktowania zmiennej 'o' w ostatnim przykładzie. Na czym to polegało?

    0
  • #6 07 Lut 2016 19:55
    szczywronek
    Poziom 27  

    W C wszystko jest prostsze :) Z możliwości przeładowania funkcji volatile i zwykłych zdaję sobie sprawę. O generowaniu domyślnych konstruktorów i obiektów tymczasowych również wiem.

    krru napisał:
    to metody klasy definiują jak mają być wykonywane operacje na klasie
    No dobra. W takim razie volatile w poniższym kodzie nic nie znaczy?
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Wiem, że przykłady pewnie są nieco wydumane, ale staram się zrozumieć "raz na zawsze" jak to działa. W C z "zamkniętymi oczami" wiedziałem co się znajdzie w kompilacie, C++ jak na razie dosyć często mnie zaskakuje.

    krru napisał:
    Nie rozumiem twojej uwago co do nieulotnego traktowania zmiennej 'o' w ostatnim przykładzie. Na czym to polegało?
    Nie tyle zmiennej "o" co "o.a" (przepraszam jeśli stosuję niewłaściwe nazewnictwo). Chodzi mi o to, że pomimo oznaczenia zmiennej "int a" jako ulotnej (w ciele klasy), operacja "o.a = 1" nie przetrwała optymalizacji. Innymi słowy obecność volatile w ciele klasy nic nie zmienia.

    Generalnie wszystkie moje wątpliwości sprowadzają się do próby zrozumienia co powoduje volatile postawione w trzech różnych miejscach:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Po lekturze poradników/książek nt. C++ wydawało mi się, że:
    - (1) powoduje, że każdy dostęp do składnika "a" obiektu klasy "K", będzie traktowany jak dostęp do danych ulotnych; bez względu na to czy będzie to dostęp z zewnątrz (obiekt.a) czy z metody klasy (oznaczonej jako volatile lub nie)
    - (2) powoduje, że wszystkie dostępy do pamięci realizowane w tej funkcji będą "ulotne"
    - (3) wymusi ulotność składników klasy (coś jak dopisanie volatile przed każdą zmienną w ciele klasy - jak w punkcie (1)), i ponadto wymusi stosowanie na obiekcie jedynie metod z modyfikatorem volatile

    I wszystko byłoby fajnie, gdyby nie kompilator, który ma odmienne zdanie na ten temat.

    0
  • #7 08 Lut 2016 07:31
    mi14chal
    Poziom 27  

    szczywronek napisał:
    I wszystko byłoby fajnie, gdyby nie kompilator, który ma odmienne zdanie na ten temat.

    A czy problem jednak nie leży w trywialności tego kodu? Wszak nie pokazałeś jaki kod generuje kompilator dla danych przypadków, więc ciężko się odnieść. Ale dla takiego przykładu kompilator to optymalizuje bo i tak w main się nic nie dzieje. Najlepiej dodać trochę kodu np: pobranie wartości od użytkowania, potem jakieś operacje, a na końcu wypisanie.

    0
  • #8 08 Lut 2016 09:59
    szczywronek
    Poziom 27  

    W kompilacie nie ma nic ciekawego: albo jest zapis do zmiennej (wtedy zakładam, że została potraktowana jako volatile), albo nie ma nic (powrót z main do procedury "rozbiegowej") :) Nie chciałem jeszcze bardziej wydłużać postów wklejaniem listingów.

    mi14chal napisał:
    A czy problem jednak nie leży w trywialności tego kodu?
    Tak przypuszczam, m.in. stąd pytanie:
    szczywronek napisał:
    Czy kompilator widząc ulotną, automatyczną zmienną lokalną ma prawo ją wyoptymalizować?


    Im dłużej się tym bawię, tym bardziej utwierdzam się w przekonaniu, że volatile umieszczone w różnych miejscach, działa z grubsza tak jak przypuszczałem. Zamieszanie zaś bierze się z "inteligencji" kompilatora, który widząc np. coś takiego:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Doskonale zdaje sobie sprawę, że i tak nie dobiorę się do zmiennej lokalnej z innego miejsca programu (np. z ISR), zaś to że jest tworzona na stosie wyklucza jej "sprzętowe" powiązania - stąd pozwala sobie na jej wyoptymalizowanie (obecność/brak volatile w ciele klasy w powyższym kodzie, nic nie zmienia). Wystarczy jednak, aby "obiekt" był globalny i wtedy już zmienna nie jest wyrzucana (kompilowane w GCC 5.3 dla ARMów, w roli figuranta - "jakiś_rejestr_sprzętowy" - wykorzystany został jeden z rejestrów licznika):
    Kod: armasm
    Zaloguj się, aby zobaczyć kod


    Powyższą teorię psuje niestety to, że optymalizator nie wyrzuca "o.a", jeśli cały "obiekt" jest oznaczony jako volatile:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Kod: armasm
    Zaloguj się, aby zobaczyć kod


    Pojawia się więc pytanie: jaka jest różnica w dostępie do składników, między "zwykłym" obiektem klasy zawierającej ulotną zmienną, a ulotnym obiektem klasy zawierającej "zwykłą" zmienną?

    0
  • #9 08 Lut 2016 18:29
    alagner
    Poziom 25  

    Aż zacząłem z tym eksperymentować, w końcu wylądowałem z tym na stackoverflow i okazuje się, że to wcale nie jest taki banalny problem.

    Generalnie volatile stojący obok obiektu powinien przenosić się na wszystkie jego pola. I co ciekawe, jeśli zrobimy

    Kod: c
    Zaloguj się, aby zobaczyć kod


    to while wyrzucony nie będzie (bo to statement with no effect, więc kwalifikowałby się).
    Ale już jego brak spowoduje wycięcie zmiennej całkowicie. Problem rozwiązałby konstruktor specyfikowany volatile...ale ctor nie może być cv.

    Co ciekawe, gcc, MSVC oraz clang dają te same wyniki, także albo wszyscy (autorzy kompilatorów) się mylą, albo czegoś nie potrafimy odczytać ze standardu..

    0
  • #10 08 Lut 2016 21:48
    mi14chal
    Poziom 27  

    alagner napisał:
    to while wyrzucony nie będzie (bo to statement with no effect, więc kwalifikowałby się).

    Tylko widzisz jeśli by taki while był wyrzucany nie ważne czy dana zmienna jest volatile czy też nie, to by nie miało by wtedy sensu, a tak to możemy powiedzieć kompilatorowi, że nie chcemy wyrzucać pętli dodając volatile. W końcu takie konstrukcje są często stosowane jako proste delay na mikrokontrolery.

    0
  • #11 09 Lut 2016 08:48
    alagner
    Poziom 25  

    Wiem i to ma sens. Bardziej zastanawia mnie brak wywowołania konstruktora jeśli tej pętli nie ma.

    0
  • #12 09 Lut 2016 12:00
    mi14chal
    Poziom 27  

    Z tego co sprawdziłem dla tego kodu z postu #9 to kompilator po prostu tworzy zmienną int i przypisuje do niej 23. A jak się usunie volatile czy też pętle to wtedy już nic nie ma.

    0
  • #13 09 Lut 2016 15:05
    szczywronek
    Poziom 27  

    alagner napisał:
    to wcale nie jest taki banalny problem
    Tym bardziej dziwi mnie, że nigdzie nie znalazłem wyczerpującego opisu.

    alagner napisał:
    Generalnie volatile stojący obok obiektu powinien przenosić się na wszystkie jego pola
    O! I to mi się podoba - konkret i do tego zgodny z moimi "przeczuciami" :D

    Przez przypadek znalazłem jeszcze coś śmiesznego:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    Po skompilowaniu powyższego, inicjalizacja składnika "a" jest całkowicie wyrzucana (main jest pusty). A teraz niespodzianka: po odkomentowaniu drugiego składnika klasy (int b) inicjalizacja zmiennej "a" nie zostaje wyoptymalizowana. Żadnych innych zmian w kodzie, tylko dodanie drugiego (nie ulotnego) pola... 8-O

    W powyższym przykładzie na stosie rezerwowane jest miejsce na dwa "inty" (a i b). Z ciekawości sprawdziłem czy po zmianie konstruktora na:
    Kod: c
    Zaloguj się, aby zobaczyć kod

    zadzieje się coś ciekawego. Efekt jest taki, że pomimo rezerwowania 8B na stosie (dwa integery po 4B) tylko jeden z nich jest inicjalizowany.

    0
  • #14 09 Lut 2016 16:02
    mi14chal
    Poziom 27  

    To bardzo ciekawe bo dla takiego kodu:

    Kod: c
    Zaloguj się, aby zobaczyć kod

    Main jest pusty. Kompilowane na -O2.

    0
  • #15 09 Lut 2016 16:55
    szczywronek
    Poziom 27  

    A jaki "komplikator"?
    GCC 5.3 dla armów, optymalizacja O1,2,3,s; z/bez flto (bez różnicy). Listing nieco odkurzony z nadmiaru komentarzy:

    Kod: armasm
    Zaloguj się, aby zobaczyć kod

    Sprawdziłem jeszcze inne wersje gcc, które mam na komputerze + kilka kompilatorów ze strony http://gcc.godbolt.org/ i wyniki wskazują na... wolną amerykankę :)

    0
  • #16 09 Lut 2016 17:41
    mi14chal
    Poziom 27  

    Na mingw 4.9.2. A co do wolnej amerykanki to powiedzmy sobie szczerze, że ten przykładowy kod z postu #9 sensu nie ma, bo tworzysz obiekt i potem nic z nim nie robisz to kompilator to usuwa. Choć ciekawe, że GCC dla arm tego nie usuwa.

    0