Czy lotność jest droga?

111

Po przeczytaniu książki kucharskiej JSR-133 dla autorów kompilatorów o implementacji ulotnych, szczególnie sekcji "Interakcje z instrukcjami Atomic", zakładam, że odczyt zmiennej ulotnej bez aktualizacji wymaga LoadLoad lub bariery LoadStore. W dalszej części strony widzę, że LoadLoad i LoadStore skutecznie nie działają na procesorach X86. Czy to oznacza, że ​​ulotne operacje odczytu mogą być wykonywane bez jawnego unieważnienia pamięci podręcznej na x86 i są tak szybkie, jak zwykły odczyt zmiennej (pomijając ograniczenia zmiany kolejności zmiennych)?

Myślę, że nie rozumiem tego poprawnie. Czy ktoś mógłby mnie oświecić?

EDYCJA: Zastanawiam się, czy istnieją różnice w środowiskach wieloprocesorowych. W systemach z jednym procesorem procesor może patrzeć na własne pamięci podręczne wątków, jak stwierdza John V., ale w systemach z wieloma procesorami musi być jakaś opcja konfiguracji dla procesorów, że to nie wystarczy i pamięć główna musi zostać uderzona, co powoduje wolniejsze działanie ulotności w systemach z wieloma procesorami, prawda?

PS: W drodze, aby dowiedzieć się więcej na ten temat, natknąłem się na następujące świetne artykuły, a ponieważ to pytanie może być interesujące dla innych, udostępnię tutaj moje linki:

Daniel
źródło
1
Możesz przeczytać moją edycję o konfiguracji z wieloma procesorami, do których się odnosisz. Może się zdarzyć, że w systemach wieloprocesorowych dla krótkotrwałego odniesienia, nie będzie więcej niż pojedynczy odczyt / zapis do pamięci głównej.
John Vint
2
sam lotny odczyt nie jest drogi. głównym kosztem jest to, jak zapobiega optymalizacjom. w praktyce średni koszt również nie jest zbyt wysoki, chyba że zmienne są używane w wąskiej pętli.
niezaprzeczalny
2
Ten artykuł na infoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) również może Cię zainteresować, pokazuje wpływ niestabilności i synchronizacji na wygenerowany kod dla różnych architektur. Jest to również jeden przypadek, w którym jvm może działać lepiej niż kompilator wyprzedzający, ponieważ wie, czy działa w systemie jednoprocesorowym i może ominąć niektóre bariery pamięci.
Jörn Horstmann

Odpowiedzi:

123

W przypadku Intela niekontrolowany, niestabilny odczyt jest dość tani. Jeśli weźmiemy pod uwagę następujący prosty przypadek:

public static long l;

public static void run() {        
    if (l == -1)
        System.exit(-1);

    if (l == -2)
        System.exit(-1);
}

Używając możliwości Java 7 do drukowania kodu asemblera, metoda run wygląda mniej więcej tak:

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb396ce80: mov    %eax,-0x3000(%esp)
0xb396ce87: push   %ebp
0xb396ce88: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 33)
0xb396ce8e: mov    $0xffffffff,%ecx
0xb396ce93: mov    $0xffffffff,%ebx
0xb396ce98: mov    $0x6fa2b2f0,%esi   ;   {oop('Test2')}
0xb396ce9d: mov    0x150(%esi),%ebp
0xb396cea3: mov    0x154(%esi),%edi   ;*getstatic l
                                    ; - Test2::run@0 (line 33)
0xb396cea9: cmp    %ecx,%ebp
0xb396ceab: jne    0xb396ceaf
0xb396cead: cmp    %ebx,%edi
0xb396ceaf: je     0xb396cece         ;*getstatic l
                                    ; - Test2::run@14 (line 37)
0xb396ceb1: mov    $0xfffffffe,%ecx
0xb396ceb6: mov    $0xffffffff,%ebx
0xb396cebb: cmp    %ecx,%ebp
0xb396cebd: jne    0xb396cec1
0xb396cebf: cmp    %ebx,%edi
0xb396cec1: je     0xb396ceeb         ;*return
                                    ; - Test2::run@28 (line 40)
0xb396cec3: add    $0x8,%esp
0xb396cec6: pop    %ebp
0xb396cec7: test   %eax,0xb7732000    ;   {poll_return}
;... lines removed

Jeśli spojrzysz na 2 odwołania do getstatic, pierwsze dotyczy ładowania z pamięci, drugie pomija ładowanie, ponieważ wartość jest ponownie używana z rejestrów, do których jest już załadowany (long jest 64-bitowy i na moim laptopie 32-bitowym wykorzystuje 2 rejestry).

Jeśli zmienimy l zmienną jako ulotną, wynikowy zestaw będzie inny.

# {method} 'run2' '()V' in 'Test2'
#           [sp+0x10]  (sp of caller)
0xb3ab9340: mov    %eax,-0x3000(%esp)
0xb3ab9347: push   %ebp
0xb3ab9348: sub    $0x8,%esp          ;*synchronization entry
                                    ; - Test2::run2@-1 (line 32)
0xb3ab934e: mov    $0xffffffff,%ecx
0xb3ab9353: mov    $0xffffffff,%ebx
0xb3ab9358: mov    $0x150,%ebp
0xb3ab935d: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab9365: movd   %xmm0,%eax
0xb3ab9369: psrlq  $0x20,%xmm0
0xb3ab936e: movd   %xmm0,%edx         ;*getstatic l
                                    ; - Test2::run@0 (line 32)
0xb3ab9372: cmp    %ecx,%eax
0xb3ab9374: jne    0xb3ab9378
0xb3ab9376: cmp    %ebx,%edx
0xb3ab9378: je     0xb3ab93ac
0xb3ab937a: mov    $0xfffffffe,%ecx
0xb3ab937f: mov    $0xffffffff,%ebx
0xb3ab9384: movsd  0x6fb7b2f0(%ebp),%xmm0  ;   {oop('Test2')}
0xb3ab938c: movd   %xmm0,%ebp
0xb3ab9390: psrlq  $0x20,%xmm0
0xb3ab9395: movd   %xmm0,%edi         ;*getstatic l
                                    ; - Test2::run@14 (line 36)
0xb3ab9399: cmp    %ecx,%ebp
0xb3ab939b: jne    0xb3ab939f
0xb3ab939d: cmp    %ebx,%edi
0xb3ab939f: je     0xb3ab93ba         ;*return
;... lines removed

W tym przypadku oba odwołania getstatic do zmiennej l są ładowane z pamięci, tj. Wartość nie może być przechowywana w rejestrze podczas wielu ulotnych odczytów. Aby zapewnić atomowy odczyt, wartość jest odczytywana z pamięci głównej do rejestru MMX, movsd 0x6fb7b2f0(%ebp),%xmm0dzięki czemu operacja odczytu jest pojedynczą instrukcją (z poprzedniego przykładu widzieliśmy, że wartość 64-bitowa normalnie wymagałaby dwóch 32-bitowych odczytów w systemie 32-bitowym).

Zatem całkowity koszt ulotnego odczytu będzie mniej więcej równy obciążeniu pamięci i może być tak tani jak dostęp do pamięci podręcznej L1. Jeśli jednak inny rdzeń zapisuje zmienną ulotną, linia pamięci podręcznej zostanie unieważniona, wymagając pamięci głównej lub być może dostępu do pamięci podręcznej L3. Rzeczywisty koszt będzie w dużej mierze zależał od architektury procesora. Nawet między Intelem a AMD protokoły spójności pamięci podręcznej są różne.

Michael Barker
źródło
uwaga boczna, java 6 ma taką samą możliwość pokazywania asemblera (robi to hotspot)
bestsss
+1 W JDK5 ulotne nie można zmienić kolejności w odniesieniu do żadnego odczytu / zapisu (co na przykład rozwiązuje blokowanie podwójnego sprawdzenia). Czy to oznacza, że ​​wpłynie to również na sposób manipulowania polami nieulotnymi? Byłoby interesujące połączenie dostępu do pól niestabilnych i nieulotnych.
ewernli
@evemli, musisz być ostrożny, sam raz złożyłem to oświadczenie, ale okazało się, że jest nieprawidłowe. Jest przypadek krawędzi. Java Memory Model umożliwia stosowanie semantyki motelu typu roach, kiedy można zamawiać sklepy przed niestabilnymi sklepami. Jeśli wziąłeś to z artykułu Briana Goetza w serwisie IBM, to warto wspomnieć, że ten artykuł upraszcza specyfikację JMM.
Michael Barker
20

Ogólnie rzecz biorąc, w większości nowoczesnych procesorów obciążenie ulotne jest porównywalne z normalnym. Niestabilny sklep to około 1/3 czasu wejścia montiora / wyjścia monitora. Jest to widoczne w systemach, które są spójne z pamięcią podręczną.

Odpowiadając na pytanie OP, niestabilne zapisy są drogie, podczas gdy odczyty zwykle nie.

Czy to oznacza, że ​​ulotne operacje odczytu mogą być wykonywane bez jawnego unieważnienia pamięci podręcznej na x86 i są tak szybkie, jak zwykły odczyt zmiennej (pomijając ograniczenia zmiany kolejności zmiennych)?

Tak, czasami podczas walidacji pola procesor może nawet nie trafić do pamięci głównej, zamiast tego szpieguje inne pamięci podręczne wątków i uzyskuje stamtąd wartość (bardzo ogólne wyjaśnienie).

Jednak popieram sugestię Neila, że ​​jeśli masz pole, do którego ma dostęp wiele wątków, powinieneś zawijać je jako AtomicReference. Będąc AtomicReference, wykonuje z grubsza taką samą przepustowość dla odczytów / zapisów, ale jest również bardziej oczywiste, że pole będzie dostępne i modyfikowane przez wiele wątków.

Edytuj, aby odpowiedzieć na edycję OP:

Spójność pamięci podręcznej to trochę skomplikowany protokół, ale w skrócie: procesory będą dzielić wspólną linię pamięci podręcznej, która jest podłączona do pamięci głównej. Jeśli procesor ładuje pamięć, a żaden inny procesor jej nie ma, to procesor przyjmie, że jest to najbardziej aktualna wartość. Jeśli inny procesor spróbuje załadować tę samą lokalizację pamięci, już załadowany procesor będzie tego świadomy i faktycznie udostępni buforowane odniesienie do żądającego procesora - teraz żądający procesor ma kopię tej pamięci w swojej pamięci podręcznej procesora. (Nigdy nie musiał szukać w pamięci głównej w celach informacyjnych)

Jest więcej protokołów, ale to daje wyobrażenie o tym, co się dzieje. Odpowiadając również na inne pytanie, przy braku wielu procesorów, ulotne odczyty / zapisy mogą być w rzeczywistości szybsze niż w przypadku wielu procesorów. Istnieją aplikacje, które w rzeczywistości działałyby szybciej jednocześnie z jednym procesorem niż z wieloma.

John Vint
źródło
5
AtomicReference jest po prostu opakowaniem zmiennego pola z dodanymi funkcjami natywnymi zapewniającymi dodatkowe funkcje, takie jak getAndSet, compareAndSet itp., Więc z punktu widzenia wydajności użycie go jest przydatne, jeśli potrzebujesz dodatkowej funkcjonalności. Ale zastanawiam się, dlaczego odnosisz się tutaj do systemu operacyjnego? Funkcjonalność jest implementowana bezpośrednio w opkodach procesora. I czy to oznacza, że ​​w systemach wieloprocesorowych, w których jeden procesor nie ma wiedzy o zawartości pamięci podręcznej innych procesorów, składniki ulotne są wolniejsze, ponieważ procesory zawsze muszą trafiać do pamięci głównej?
Daniel
Masz rację, tęsknię za mówieniem o tym, że system operacyjny powinien napisać procesor, naprawiając to teraz. I tak, wiem, że AtomicReference jest po prostu opakowaniem dla zmiennych zmiennych, ale dodaje również jako rodzaj dokumentacji, że samo pole będzie dostępne przez wiele wątków.
John Vint
@John, dlaczego miałbyś dodać kolejne pośrednie za pośrednictwem AtomicReference? Jeśli potrzebujesz CAS - ok, ale AtomicUpdater może być lepszą opcją. O ile pamiętam, nie ma żadnych wewnętrznych elementów związanych z AtomicReference.
bestsss
@bestsss W przypadku wszystkich ogólnych celów masz rację, nie ma różnicy między AtomicReference.set / get a niestabilnym ładowaniem i magazynami. Biorąc to pod uwagę, miałem to samo uczucie (i do pewnego stopnia) co do tego, kiedy go użyć. Ta odpowiedź może nieco wyszczególnić stackoverflow.com/questions/3964317/… . Używanie któregokolwiek z nich jest bardziej preferencją, moim jedynym argumentem za używaniem AtomicReference zamiast prostej zmiennej jest jasna dokumentacja - to samo w sobie nie jest największym argumentem, który rozumiem
John Vint
Na marginesie, niektórzy spierają się, że użycie pola zmiennego / AtomicReference (bez potrzeby CAS) prowadzi do błędnego
John Vint
12

W słowach Java pamięci model (jak zdefiniowano dla Java 5+ w JSR 133), każda operacja - czytać lub pisać - na volatilezmiennej tworzy się dzieje, zanim relacji w odniesieniu do jakiejkolwiek innej operacji na tej samej zmiennej. Oznacza to, że kompilator i JIT są zmuszone unikać pewnych optymalizacji, takich jak zmiana kolejności instrukcji w wątku lub wykonywanie operacji tylko w lokalnej pamięci podręcznej.

Ponieważ niektóre optymalizacje nie są dostępne, wynikowy kod jest koniecznie wolniejszy niż byłby, chociaż prawdopodobnie nie bardzo.

Niemniej jednak nie powinieneś tworzyć zmiennej, volatilechyba że wiesz, że będzie ona dostępna z wielu wątków poza synchronizedblokami. Mimo to należy rozważyć, czy lotny jest najlepszym wyborem w porównaniu synchronized, AtomicReferencea jego przyjaciele, wyraźnych Lockklas itp

Neil Bartlett
źródło
4

Dostęp do zmiennej nietrwałej jest pod wieloma względami podobny do zawijania dostępu do zwykłej zmiennej w zsynchronizowanym bloku. Na przykład dostęp do zmiennej nietrwałej uniemożliwia procesorowi ponowne uporządkowanie instrukcji przed i po uzyskaniu dostępu, a to generalnie spowalnia wykonanie (chociaż nie mogę powiedzieć o ile).

Mówiąc bardziej ogólnie, w systemie wieloprocesorowym nie widzę, jak można uzyskać dostęp do zmiennej ulotnej bez ponoszenia kary - musi istnieć sposób, aby zapewnić, że zapis na procesorze A zostanie zsynchronizowany z odczytem na procesorze B.

krakover
źródło
4
Czytanie zmiennych nietrwałych ma taką samą karę jak wprowadzanie do monitora, biorąc pod uwagę możliwości zmiany kolejności instrukcji, podczas gdy zapisanie zmiennej lotnej jest równoznaczne z wyjściem z monitora. Różnica może polegać na tym, które zmienne (np. Pamięci podręczne procesora) zostaną opróżnione lub unieważnione. Podczas gdy zsynchronizowane opróżnia lub unieważnia wszystko, dostęp do zmiennej nietrwałej powinien zawsze ignorować pamięć podręczną.
Daniel,
12
-1, Dostęp do zmiennej nietrwałej jest nieco inny niż użycie zsynchronizowanego bloku. Wprowadzenie zsynchronizowanego bloku wymaga atomowego zapisu opartego na porównaniu i ustawieniach w celu usunięcia blokady i zapisu ulotnego w celu zwolnienia go. Jeśli blokada jest zadowolona, ​​sterowanie musi przejść z przestrzeni użytkownika do przestrzeni jądra, aby rozstrzygnąć blokadę (jest to kosztowny bit). Dostęp do zmiennego zawsze pozostanie w przestrzeni użytkownika.
Michael Barker,
@MichaelBarker: Czy jesteś pewien, że wszystkie monitory muszą być chronione przez jądro, a nie przez aplikację?
Daniel
@Daniel: Jeśli reprezentujesz monitor za pomocą zsynchronizowanego bloku lub blokady, to tak, ale tylko wtedy, gdy monitor jest zadowolony. Jedynym sposobem na zrobienie tego bez arbitrażu jądra jest użycie tej samej logiki, ale zajęty spin zamiast parkowania wątku.
Michael Barker,
@MichaelBarker: Okey, rozumiem to, co do zadowolonych zamków.
Daniel