Czy w niektórych wyrażeniach JIT może się załamać dwa lotne odczyty jako jeden?

Załóżmy, że mamy volatile int a. Jeden wątek robi

while (true) {
    a = 1;
    a = 0;
}

A inny wątek robi

while (true) {
    System.out.println(a+a);
}

Czy byłoby nielegalne aby kompilator JIT emitował assembly odpowiadające 2*a zamiast a+a?

Z jednej strony celem ulotnego czytania jest to, aby zawsze było świeże z pamięci.

Z drugiej strony, nie ma punktu synchronizacji między dwoma odczytami, więc nie widzę, że byłoby nielegalne traktowanie a+a atomicznie, w takim przypadku nie widzisz, jak taka optymalizacja jak 2*a złamałaby specyfikację.

Odniesienia do JLS będą mile widziane.

Author: aioobe, 2014-12-19

5 answers

Krótka odpowiedź:

Tak, ta optymalizacja jest dozwolona. Dwie kolejne operacje odczytu wywołują obserwowalne zachowanie sekwencji będącej atomową , ale nie pojawia się jako Zmiana kolejności operacji. Każda sekwencja czynności wykonywanych na jednym wątku wykonania może być wykonana jako jednostka atomowa. Ogólnie rzecz biorąc, trudno jest zapewnić sekwencję operacji wykonywaną atomicznie i rzadko skutkuje to wzrostem wydajności, ponieważ większość środowiska wykonawcze wprowadzają napowietrzne do wykonywania elementów atomicznie.

W przykładzie podanym przez pytanie pierwotne, sekwencja operacji jest następująca:

read(a)
read(a)

Wykonanie tych operacji atomicznie gwarantuje, że wartość odczytana w pierwszym wierszu jest równa wartości odczytanej w drugim wierszu. Co więcej, oznacza to, że wartość odczytana w drugiej linii jest wartością zawartą w a w momencie wykonania pierwszego odczytu (i vice versa, ponieważ atomowe obie operacje odczytu nastąpiły w tym samym czasie zgodnie z obserwowalnym stanem wykonania programu). Omawiana optymalizacja, która polega na ponownym wykorzystaniu wartości pierwszego odczytu dla drugiego odczytu, jest równoważna kompilatorowi i / lub JIT wykonującemu sekwencję atomicznie i dlatego jest poprawna.


Oryginalna dłuższa odpowiedź:

Model pamięci Java opisuje operacje za pomocą happens-before częściowego porządkowania. W celu wyrażenia ograniczenie, że pierwszy odczyt r1 i drugi Odczyt r2 z a nie mogą być zwinięte, musisz pokazać, że pewna operacja jest semantycznie wymagana, aby pojawić się między nimi.

Operacje na wątku z r1 i r2 są następujące:

--> r(a) --> r(a) --> add -->

Aby wyrazić wymóg, że coś (powiedzmy y) leży między r1 a r2, musisz wymagać, aby r1 happens-before y oraz y happens-before r2. Jak to się dzieje, nie ma reguły, w której operacja odczytu pojawia się po lewej stronie relacjihappens-before . Najbliższe co można dostać to powiedzenie y happens-before r2, jednak kolejność częściowa pozwalałaby y wystąpić również przed r1, co powodowałoby załamanie operacji odczytu.

Jeśli nie istnieje żaden scenariusz, który wymaga operacji, która powinna mieścić się pomiędzy r1 a r2, możesz zadeklarować, że żadna operacja nigdy nie pojawia się między r1 a r2 i nie naruszać wymaganą semantykę języka. Użycie pojedynczej operacji odczytu byłoby równoważne temu twierdzeniu.

Edit moja odpowiedź zostaje odrzucona, więc przejdę do dodatkowych szczegółów.

Oto kilka powiązanych pytań:

  • Czy kompilator Java lub JVM jest wymagany do zwinięcia tych operacji odczytu?

    Nie. Wyrażenia a i a użyte w wyrażeniu add nie są wyrażeniami stałymi, więc nie ma wymogu, aby były zawalone.
  • czy JVM zawala te operacje odczytu?

    Na to, nie jestem pewien odpowiedzi. Kompilując program i używając javap -c, łatwo zauważyć, że kompilator Javy nie zwija tych operacji odczytu. Niestety nie jest tak łatwo udowodnić, że JVM nie zawala operacji (a nawet trudniej, sam procesor).

  • powinien JVM zawalić te operacje odczytu?

    Prawdopodobnie nie. Każda optymalizacja wymaga czasu, więc istnieje równowaga między czasem potrzebnym do analizy kodu a oczekiwanymi korzyściami. Niektóre optymalizacje, takie jak eliminacja kontrolek array bounds lub sprawdzanie referencji null, okazały się mieć szerokie korzyści dla rzeczywistych aplikacji. Jedynym przypadkiem, w którym ta konkretna optymalizacja ma możliwość poprawy wydajności, są przypadki, w których dwa identyczne operacje odczytu pojawiają się kolejno.

    Ponadto, jak pokazuje odpowiedź na tę odpowiedź wraz z innymi odpowiedziami, ta konkretna zmiana spowodowałaby nieoczekiwaną zmianę zachowania dla niektórych aplikacji, których użytkownicy mogą nie chcieć.

Edit 2: w odniesieniu do opisu twierdzenia Rafaela, że dwie operacje odczytu, których nie można zmienić kolejności. Instrukcja ta ma na celu podkreślenie faktu, że buforowanie operacji odczytu z a następująca sekwencja może dać niepoprawny wynik:

a1 = read(a)
b1 = read(b)
a2 = read(a)
result = op(a1, b1, a2)

Załóżmy, że początkowo a i b mają domyślną wartość 0. Następnie wykonujesz tylko pierwszy read(a).

Przypuśćmy, że inny wątek wykona następującą sekwencję:

a = 1
b = 1

Na koniec Załóżmy, że pierwszy wątek wykona linię read(b). Jeśli chcesz buforować pierwotnie odczytaną wartość a, skończysz z następującym wywołaniem:

op(0, 1, 0)
To nie jest poprawne. Ponieważ zaktualizowana wartość a została zapisana przed zapisem do b, nie ma możliwości odczytania wartości b1 = 1 następnie odczytuje wartość a2 = 0. Bez buforowania prawidłowa kolejność zdarzeń prowadzi do następującego wywołania.
op(0, 1, 1)

Jednakże, jeśli miałbyś Zadać pytanie " Czy Jest jakiś sposób, aby umożliwić odczyt a być buforowane?", odpowiedź brzmi tak. Jeśli możesz wykonać wszystkie trzy odczytać operacje w pierwszej sekwencji wątku jako Jednostka atomowa, to buforowanie wartość jest dozwolona. Podczas gdy synchronizacja między wieloma zmiennymi jest trudna i rzadko zapewnia oportunistyczną przewagę optymalizacyjną, z pewnością można napotkać wyjątek. Na przykład, załóżmy, że a i b są po 4 bajty i pojawiają się kolejno w pamięci z a wyrównanymi na granicy 8 bajtów. Proces 64-bitowy może zaimplementować sekwencję read(a) read(b) jako atomową 64-bitową operację obciążenia, która pozwoliłaby na buforowanie wartości a (skutecznie traktując wszystkie trzy operacje odczytywane jako operacja atomowa zamiast tylko dwóch pierwszych).

 13
Author: Sam Harwell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-21 17:38:32

W mojej pierwotnej odpowiedzi, argumentowałem przeciwko legalności sugerowanej optymalizacji. Poparłem to głównie z informacji z JSR-133 cookbook gdzie stwierdza, że volatile read nie może być zmieniany z innym volatile read i gdzie dalej stwierdza, że odczyt w pamięci podręcznej ma być traktowany jako zmiana kolejności. To ostatnie stwierdzenie jest jednak sformułowane z pewną dwuznacznością, dlatego przeszedłem przez formalną definicję JMM gdzie nie znalazłem takiego wskazania. Dlatego chciałbym teraz argumentować, że optymalizacja jest dozwolona. Jednak JMM jest dość złożony i dyskusja na tej stronie wskazuje, że ta sprawa narożna może być rozstrzygnięta inaczej przez kogoś z bardziej dogłębnym zrozumieniem formalizmu.

Oznaczanie wątku 1 do wykonania

while (true) {
  System.out.println(a // r_1 
    + a); // r_2
} 

I wątek 2 do wykonania:

while (true) {
  a = 0; // w_1
  a = 1; // w_2
}

Dwa odczyty r_i i dwa zapisy w_i z a to synchronizacja działania jako a to volatile (JSR 17.4.2). Są to działania zewnętrzne jako zmienna {[4] } jest używana w kilku wątkach. Akcje te są zawarte w zbiorze wszystkich akcji A. Istnieje całkowita kolejność wszystkich działań synchronizacji, kolejność synchronizacji, która jest zgodna z kolejność programów dla wątek 1 i wątek 2 (JSR 17.4.4). Z definicji synchronizuje-z porządkiem częściowym, nie ma krawędzi zdefiniowane dla tej kolejności w powyższym kodzie. W rezultacie dzieje się-przed kolejnością odzwierciedla tylko semantykę wewnątrz wątku każdego wątku (JSR 17.4.5).

Z tym definiujemy W jako funkcja zapisu widzianego gdzie W(r_i) = w_2oraz funkcja zapisu wartości V(w_i) = w_2 (JLS 17.4.6). Wziąłem trochę swobody i wyeliminowałem w_1, ponieważ to czyni ten zarys formalnego dowodu jeszcze prostszym. Pytanie dotyczy tego proponowanego wykonania E jest dobrze uformowany (JLS 17.5.7). Proponowane wykonanie E jest zgodne z semantyką wewnątrz wątku, dzieje się-przed spójnym, jest zgodne-z porządkiem i każdy odczyt przestrzega spójnego zapisu. Sprawdzenie wymagań dotyczących przyczynowości jest trywialne (JSR 17.4.8). Nie rozumiem też, dlaczego Zasady dla niekończących się egzekucji byłyby istotne, ponieważ pętla obejmuje cały omawiany kod (JLS 17.4.9) i nie musimy rozróżniać obserwowalnych działań.

Dla wszystko to, nie mogę znaleźć żadnych wskazówek, dlaczego ta optymalizacja byłaby zabroniona. Niemniej jednak, nie jest on stosowany do odczytu volatile przez maszynę wirtualną HotSpot, jak można zaobserwować przy użyciu -XX:+PrintAssembly. Zakładam, że korzyści z wykonania są jednak niewielkie i ten wzór nie jest normalnie przestrzegany.

Uwaga: po obejrzeniuJava memory model (wiele razy), jestem prawie pewien, że to rozumowanie jest poprawne.

 11
Author: Rafael Winterhalter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-12 14:49:56

Trochę zmodyfikowałem problem OP

   volatile int a

    //thread 1
    while (true) {
        a = some_oddNumber;
        a = some_evenNumber;
    }

    // Thread 2 
    while (true) {
        if(isOdd(a+a)) {
            break;
        }
    }

Jeśli powyższy kod został wykonany sekwencyjnie, to istnieje poprawne sekwencyjne wykonanie, które złamie pętlę thread2 while .

Natomiast if kompilator optymalizuje a + a do 2a wtedy thread2 while loop nigdy nie będzie istniał .

Tak więc powyższa optymalizacja zabroni jednej konkretnej realizacji, gdyby była ona wykonywana sekwencyjnie.

Głównym pytaniem jest ta optymalizacja to Problem ?

Q.   Is the Transformed code Sequentially Consistent.

Ans. program jest poprawnie zsynchronizowany, Jeśli, gdy jest wykonywany w spójny sposób sekwencyjnie, nie ma wyścigów danych. Zobacz przykład 17.4.8-1 z JLS Rozdział 17

   Sequential consistency: the result of any execution is the same as
   if the read and write operations by all processes were executed in
   some sequential order and the operations of each individual
   process appear in this sequence in the order specified by its
   program [Lamport, 1979].

   Also see http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4.3

Sekwencyjna spójność jest silną gwarancją. Ścieżka wykonania, w której kompilator optymalizuje a + a jako 2a, jest również poprawnym sekwencyjnie spójnym wykonaniem . Więc odpowiedź brzmi: tak.

  Q.   Is the code violates happens before guarantees.

Ans. Konsystencja Sekwencyjna oznacza to, że dzieje się to przed gwarancją jest ważna tutaj . Więc odpowiedź brzmi: tak. JLS ref

więc nie sądzę, aby optymalizacja była nieważna prawnie przynajmniej w przypadku OP. Przypadek, w którym Thread 2 while loops stacza się w nieskończoność, jest również całkiem możliwy bez transformacji kompilatora.

 2
Author: veritas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-24 23:59:43

Z jednej strony celem ulotnego czytania jest to, aby zawsze było świeże z pamięci.

Tak nie definiuje specyfikacja języka Java. JLS po prostu mówi:

Zapis do zmiennej lotnej v (§8.3.1.4) synchronizuje-z wszystkie kolejne odczyty v przez dowolny wątek (gdzie "następny" jest zdefiniowany zgodnie z porządkiem synchronizacji).

Dlatego zapis do zmiennej lotnej happens-before (and is visible to) any next reads of that same variable.

To ograniczenie jest trywialnie spełnione dla odczytu, który nie jest późniejszy. Oznacza to, że lotność zapewnia widoczność zapisu tylko wtedy, gdy odczyt jest znany po zapisie.

Tak nie jest w twoim programie. Dla każdego dobrze uformowanego wykonania, które obserwuje a do 1, Mogę skonstruować kolejne dobrze uformowane wykonanie, gdzie A jest obserwowane do 0, po prostu przesuwać odczyt po pisz. Jest to możliwe, ponieważ relacja happens-before wygląda następująco:

write 1   -->   read 1                    write 1   -->   read 1
   |              |                          |              |
   |              v                          v              |
   v      -->   read 1                    write 0           v
write 0           |             vs.          |      -->   read 0
   |              |                          |              |
   v              v                          v              v
write 1   -->   read 1                    write 1   -->   read 1                 

Oznacza to, że wszystkie gwarancje JMM dla Twojego programu są takie, że a + a da 0, 1 lub 2. Jest to spełnione, jeśli a + a zawsze daje 0. Podobnie jak system Operacyjny może wykonać ten program na jednym rdzeniu i zawsze przerwać wątek 1 przed tą samą instrukcją pętli, JVM może ponownie użyć wartości - w końcu obserwowalne zachowanie pozostaje takie samo.

Ogólnie, przenoszenie odczytu przez zapis narusza dzieje się-przed konsystencją, ponieważ inna akcja synchronizacji jest"w drodze". W przypadku braku takich pośrednich działań synchronizacji, Lotny odczyt może być spełniony z pamięci podręcznej.

 2
Author: meriton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-12-25 22:48:30

Jak podano w innych odpowiedziach są dwa odczyty i dwa zapisy. Wyobraź sobie następujące wykonanie (T1 i T2 oznaczają dwa wątki), używając adnotacji zgodnych z instrukcją JLS poniżej:

  • T1: a = 0 //W(r)
  • T2: read temp1 = a //r_initial
  • T1: a = 1 //w
  • T2: read temp2 = a //r
  • T2: print temp1+temp2

W środowisku współbieżnym jest to zdecydowanie możliwe przeplatanie wątków. Twoje pytanie brzmi: czy JVM będzie mógł wykonać r obserwować W(r) i czytać 0 zamiast 1?

JLS #17.4.5 stwierdza:

Zachodzi zbiór akcji A-przed spójnością, jeśli dla wszystkich odczytów r W A, gdzie W (r) jest akcją zapisu widzianą przez r, nie jest tak, że albo hb(r, W(R)), albo że istnieje zapis w w taki, że w.V = r.V i hb(w(R), w) i hb (w, r).

Zaproponowana optymalizacja (temp = a; print (2 * temp);) naruszyłaby ten wymóg. Tak więc optymalizacja może działać tylko wtedy, gdy między r_initial i r, których nie można zagwarantować w typowym wielowątkowym frameworku.

Jako komentarz boczny, zauważ jednak, że nie ma gwarancji, jak długo potrwa, zanim zapisy staną się widoczne z wątku czytania. Zobacz na przykład: szczegółowa semantyka lotności dotycząca terminowości widoczności .

 -2
Author: wha'eve',
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:36