Java >> Java tutorial >  >> Tag >> volatile

Kunne JIT kollapse to flygtige læsninger som én i visse udtryk?

Kort svar:

Ja, denne optimering er tilladt. Sammenlægning af to sekventielle læseoperationer frembringer den observerbare adfærd af sekvensen, der er atomisk , men vises ikke som en genbestilling af operationer. Enhver sekvens af handlinger udført på en enkelt udførelsestråd kan udføres som en atomenhed. Generelt er det vanskeligt at sikre, at en sekvens af operationer udføres atomisk, og det resulterer sjældent i en præstationsforøgelse, fordi de fleste eksekveringsmiljøer introducerer overhead at udføre emner atomisk.

I eksemplet givet af det oprindelige spørgsmål er rækkefølgen af ​​operationer som følger:

read(a)
read(a)

At udføre disse operationer garanterer, at værdien aflæst på den første linje er lig med værdien aflæst på den anden linje. Ydermere betyder det, at værdien læst på den anden linje er værdien indeholdt i a på det tidspunkt, hvor den første læsning blev udført (og omvendt, fordi atomic begge læsningsoperationer fandt sted på samme tid i henhold til programmets observerbare udførelsestilstand). Den pågældende optimering, som er at genbruge værdien af ​​den første læsning til den anden læsning, svarer til, at compileren og/eller JIT'en udfører sekvensen atomisk og er således gyldig.

Originalt længere svar:

Java-hukommelsesmodellen beskriver operationer ved hjælp af en sker-før delbestilling. For at udtrykke den begrænsning, at den første læste r1 og anden læsning r2 af a ikke kan skjules, skal du vise, at en eller anden handling er semantisk nødvendig for at dukke op mellem dem.

Operationerne på tråden med r1 og r2 er følgende:

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

At udtrykke kravet om, at noget (sig y ) ligger mellem r1 og r2 , skal du kræve den r1 sker-før y og y sker-før r2 . Som det sker, er der ingen regel, hvor en læseoperation vises på venstre side af en sker-før forhold. Det nærmeste du kan komme er at sige y sker-før r2 , men den delvise rækkefølge ville tillade y til også at forekomme før r1 , og dermed kollapser læseoperationerne.

Hvis der ikke eksisterer et scenarie, som kræver en operation, der falder mellem r1 og r2 , så kan du erklære, at ingen handling nogensinde vises mellem r1 og r2 og ikke krænke sprogets påkrævede semantik. Brug af en enkelt læseoperation ville svare til denne påstand.

Rediger Mit svar bliver nedstemt, så jeg vil gå ind i yderligere detaljer.

Her er nogle relaterede spørgsmål:

  • Er Java-kompileren eller JVM påkrævet for at skjule disse læseoperationer?

    Nej. Udtrykkene a og a brugt i add-udtrykket er ikke konstante udtryk, så der er ingen krav om, at de skal skjules.

  • Gør at JVM kollapser disse læseoperationer?

    Hertil er jeg ikke sikker på svaret. Ved at kompilere et program og bruge javap -c , er det let at se, at Java-kompileren ikke skjuler disse læseoperationer. Desværre er det ikke så let at bevise, at JVM ikke kollapser operationerne (eller endnu hårdere, selve processoren).

  • Bør at JVM kollapser disse læseoperationer?

    Sikkert ikke. Hver optimering tager tid at udføre, så der er en balance mellem den tid, det tager at analysere koden, og den fordel, du forventer at få. Nogle optimeringer, såsom array bounds check elimination eller checking for nul-referencer, har vist sig at have omfattende fordele for applikationer i den virkelige verden. Det eneste tilfælde, hvor denne særlige optimering har mulighed for at forbedre ydeevnen, er tilfælde, hvor to identiske læseoperationer vises sekventielt.

    Ydermere, som vist af svaret på dette svar sammen med de andre svar, ville denne særlige ændring resultere i en uventet adfærdsændring for visse applikationer, som brugerne måske ikke ønsker.

Rediger 2: Vedrørende Rafaels beskrivelse af en påstand om, at to læste operationer, der ikke kan genbestilles. Denne erklæring er designet til at fremhæve det faktum, at cachelagring af læseoperationen af ​​a i følgende rækkefølge kunne give et forkert resultat:

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

Antag til at begynde med a og b har deres standardværdi 0. Så udfører du kun den første read(a) .

Antag nu, at en anden tråd udfører følgende sekvens:

a = 1
b = 1

Antag endelig, at den første tråd udfører linjen read(b) . Hvis du skulle cache den oprindeligt læste værdi af a , ville du ende med følgende opkald:

op(0, 1, 0)

Dette er ikke korrekt. Siden den opdaterede værdi af a blev gemt før skrivning til b , er der ingen måde at læse værdien b1 = 1og derefter læs værdien a2 = 0 . Uden caching fører den korrekte rækkefølge af hændelser til det følgende opkald.

op(0, 1, 1)

Men hvis du skulle stille spørgsmålet "Er der nogen måde at tillade læsning af a skal cachelagres?", er svaret ja. Hvis du kan udføre alle tre læse operationer i den første trådsekvens som en atomenhed , så er cachelagring af værdien tilladt. Selvom synkronisering på tværs af flere variabler er svært og sjældent giver en opportunistisk optimeringsfordel, er det bestemt tænkeligt at støde på en undtagelse. Antag for eksempel a og b er hver 4 bytes, og de vises sekventielt i hukommelsen med a justeret på en 8-byte grænse. En 64-bit proces kunne implementere sekvensen read(a) read(b) som en atomær 64-bit indlæsningsoperation, som ville tillade værdien a skal cachelagres (behandler effektivt alle tre læseoperationer som en atomoperation i stedet for kun de to første).


I mit oprindelige svar argumenterede jeg imod lovligheden af ​​den foreslåede optimering. Jeg støttede dette hovedsageligt fra oplysninger fra JSR-133-kogebogen, hvor det hedder, at en flygtig læsning må ikke genbestilles med en anden flygtig læsning og hvor det yderligere står, at en cachelagret læsning skal behandles som en genbestilling. Sidstnævnte udsagn er dog formuleret med en vis tvetydighed, hvorfor jeg gennemgik den formelle definition af JMM, hvor jeg ikke fandt en sådan indikation. Derfor vil jeg nu mene, at optimeringen er tilladt. Men JMM er ret komplekst, og diskussionen på denne side indikerer, at denne hjørnesag kan blive afgjort anderledes af en person med en mere grundig forståelse af formalismen.

Betegner tråd 1 at udføre

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

og tråd 2 at udføre:

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

De to lyder r_i og to skriver w_i af a er synkroniseringshandlinger som a er volatile (JSR 17.4.2). De er eksterne handlinger som variabel a bruges i flere tråde. Disse handlinger er indeholdt i sættet af alle handlinger A . Der findes en samlet rækkefølge af alle synkroniseringshandlinger, synkroniseringsrækkefølgen hvilket er i overensstemmelse med programrækkefølge til tråd 1 og tråd 2 (JSR 17.4.4). Fra definitionen af ​​synkroniserer-med delvis rækkefølge, er der ingen kant defineret for denne rækkefølge i ovenstående kode. Som en konsekvens heraf skeder-før-ordren afspejler kun intra-tråd semantikken af hver tråd (JSR 17.4.5).

Med dette definerer vi W som en skrive-set funktion hvor W(r_i) = w_2 og en værdiskrevet funktion V(w_i) = w_2 (JLS 17.4.6). Jeg tog lidt frihed og eliminerede w_1 da det gør denne oversigt over et formelt bevis endnu enklere. Spørgsmålet er denne foreslåede udførelse E er velformet (JLS 17.5.7). Den foreslåede udførelse E adlyder intra-tråd semantik, er sker-før konsistent, adlyder synkroniseret-med rækkefølge og hver læsning observerer en konsistent skrivning. Det er trivielt at kontrollere kausalitetskravene (JSR 17.4.8). Jeg kan heller ikke se, hvorfor reglerne for ikke-afsluttede henrettelser ville være relevant, da loopet dækker hele den diskuterede kode (JLS 17.4.9), og vi behøver ikke at skelne mellem observerbare handlinger .

Til alt dette kan jeg ikke finde nogen indikation af, hvorfor denne optimering ville være forbudt. Ikke desto mindre anvendes den ikke til volatile læses af HotSpot VM'en, som man kan observere ved hjælp af -XX:+PrintAssembly . Jeg antager, at præstationsfordelene dog er små, og at dette mønster normalt ikke observeres.

Bemærkning:Efter at have set Java-hukommelsesmodellens pragmatik (flere gange), er jeg ret sikker på, at denne begrundelse er korrekt.


På den ene side er selve formålet med en flygtig læsning, at den altid skal være frisk fra hukommelsen.

Det er ikke sådan, Java-sprogspecifikationen definerer flygtig. JLS siger blot:

En skrivning til en flygtig variabel v (§8.3.1.4) synkroniserer-med alle efterfølgende læsninger af v af enhver tråd (hvor "efterfølgende" er defineret i henhold til synkroniseringsrækkefølgen).

Derfor sker en skrivning til en flygtig variabel happens-before (og er synlig for) enhver efterfølgende læsning af den samme variabel.

Denne begrænsning er trivielt opfyldt for en læsning, der ikke er efterfølgende. Det vil sige, at volatile kun sikrer synlighed af en skrivning, hvis læsningen vides at ske efter skrivningen.

Dette er ikke tilfældet i dit program. For hver veludformet udførelse, der observerer, at a er 1, kan jeg konstruere en anden velformet udførelse, hvor a observeres til at være 0, blot flytte læsningen efter skrivningen. Dette er muligt, fordi sker-før-relationen ser ud som følger:

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                 

Det vil sige, at alle JMM-garantier for dit program er, at a+a vil give 0, 1 eller 2. Det er opfyldt, hvis a+a altid giver 0. Ligesom operativsystemet har tilladelse til at udføre dette program på en enkelt kerne, og altid afbryde tråd 1 før den samme instruktion af løkken, har JVM'en tilladelse til at genbruge værdien - trods alt forbliver den observerbare adfærd den samme.

Generelt overtræder det at flytte læsningen over skrivningen - før konsistens, fordi en anden synkroniseringshandling er "i vejen". I mangel af sådanne mellemliggende synkroniseringshandlinger kan en flygtig læsning opfyldes fra en cache.


Java tag