8

Quale pensi sia il modo migliore per implementare la parte di acquisizione di una coppia di rilascio/acquisizione in Java?Implementazione di un acquisto per un rilascio da Unsafe.putOrdered *()?

Sto provando a modellare alcune delle azioni in un'applicazione mia utilizzando la versione classica/acquisizione semantica (senza StoreLoad e senza coerenza sequenziale tra thread).

Ci sono un paio di modi per ottenere l'equivalente approssimativo di un rilascio in negozio nel JDK. java.util.concurrent.Atomic*.lazySet() e il sottostante sun.misc.Unsafe.putOrdered*() sono gli approcci più spesso citati per farlo. Tuttavia non esiste un modo ovvio per implementare una acquisizione del carico.

  • Le API JDK che permettono lazySet() uso soprattutto volatile variabili internamente, quindi il loro negozio-release sono accoppiati con carichi volatili. In teoria, i carichi volatili dovrebbero essere più costosi di quelli acquisiti dal carico e non dovrebbero fornire nulla di più di un puro acquisto del carico nel contesto di un precedente rilascio del negozio.

  • sun.misc.Unsafe non fornisce getAcquire()* equivalenti delle putOrdered*() metodi, anche se tali metodi acquisire sono previste per il prossimo API VarHandles.

  • Qualcosa che suona come funzionerebbe è un carico normale, seguito da sun.misc.Unsafe.loadFence(). È piuttosto sconcertante che non l'abbia visto da nessun'altra parte. Questo potrebbe essere collegato al fatto che si tratta di un brutto attacco.

P.S. Comprendo bene che questi meccanismi non sono coperti dal JMM, che non sono sufficienti per mantenere la coerenza sequenziale e che le azioni che creano non sono azioni di sincronizzazione (ad esempio, capisco che per esempio interrompano l'IRIW). Capisco anche che le versioni del negozio fornite da Atomic*/Unsafe sono più spesso utilizzate sia per i riferimenti che annullano con interesse o negli scenari produttore/consumatore, come meccanismo ottimizzato di trasmissione dei messaggi per alcuni indici importanti.

risposta

3

Lettura volatile è esattamente quello che stai cercando.

Infatti, le operazioni volatili corrispondenti hanno già una semantica di rilascio/acquisizione (altrimenti accade - prima non è possibile per la lettura volatile accoppiata), ma le operazioni volatili accoppiate non dovrebbero essere solo coerentemente sequenziali (~ accade prima), ma inoltre dovrebbero essere in total synchronization order, ecco perché la barriera StoreLoad viene inserita dopo la scrittura volatile: per garantire l'ordine totale delle scritture volatili in posizioni diverse, quindi tutti i thread vedranno quei valori nello stesso ordine.

lettura volatile ha acquisire semantica: proof dal Hotspot codice base, v'è anche la raccomandazione diretta da Doug Lea in JSR-133 cookbook (LoadLoad e LoadStore barriere dopo ogni lettura volatili).

Unsafe.loadFence() ha anche acquisito la semantica (proof), ma non ha utilizzato il valore di lettura (è possibile fare lo stesso con la lettura volatile normale), ma per evitare di riordinare le letture semplici con la lettura volatile successiva. Viene utilizzato in StampedLock per la lettura ottimistica (vedere l'implementazione del metodo StampedLock#validate e gli usi).

Aggiornamento dopo discussione nei commenti.

Controlliamo se Unsafe#loadStore() e le letture volatili sono le stesse e hanno acquisito la semantica.

Sto guardando all'hotspot C1 compiler source code per evitare di leggere tutte le ottimizzazioni in C2. Trasforma il bytecode (in effetti, non il bytecode, ma la sua rappresentazione dell'interprete) in LIR (rappresentazione intermedia di basso livello) e quindi traduce il grafico in opcode effettivi dipende dalla microarchitettura di destinazione.

Unsafe#loadFence è intrinsic che ha l'alias _loadFence. In C1 LIR generator genera questo:

case vmIntrinsics::_loadFence : 
if (os::is_MP()) __ membar_acquire(); 

dove __ è macro per la generazione di LIR.

Ora diamo un'occhiata alla lettura volatile implementation nello stesso generatore LIR. Prova ad inserire controlli nulli, controlla IRIW, controlla se siamo su x32 e prova a leggere il valore a 64 bit (per fare magie con SSE/FPU) e, infine, ci conduce allo stesso codice:

if (is_volatile && os::is_MP()) { 
    __ membar_acquire(); 
} 

Il generatore di assemblatore inserisce quindi le istruzioni di acquisizione specifiche della piattaforma here.

Guardando implementazioni specifiche (nessun link qui, ma tutti possono essere trovati in src/cpu/{$ cpu_model}/vm/c1_LIRAssembler _ {$ cpu_model} cpp)

  • SPARC

    void LIR_Assembler::membar_acquire() { 
        // no-op on TSO 
    } 
    
  • x86

    void LIR_Assembler::membar_acquire() { 
        // No x86 machines currently require load fences 
    } 
    
  • Aarch64 (modello di memoria debole, le barriere devono essere pre inviato)

    void LIR_Assembler::membar_acquire() { 
        __ membar(Assembler::LoadLoad|Assembler::LoadStore); 
    } 
    

    Secondo Aarch architecture description tale membar sarà compilato come dmb ishld istruzione dopo il carico.

  • PowerPC (anche modello di memoria debole)

    void LIR_Assembler::membar_acquire() { 
        __ acquire(); 
    } 
    

    che trasforma poi in specifiche istruzioni PowerPC lwsync.Secondo il commentslwsync è semanticamente equivalente a

    lwsync ordini negozio | Store, carico | Store, carico | carico, ma non STORE | carico

    Ma finché PowerPC hasn' Per eventuali ostacoli più deboli, questa è l'unica scelta per implementare la semantica acquisita su PowerPC.

Conclusioni

Volatile legge e Unsafe#loadFence() sono uguali in termini di ordinamento della memoria (ma forse non in termini di possibili ottimizzazioni del compilatore), su x86 più popolare è no-op, e PowerPC è il solo l'architettura supportata non ha precisi ostacoli all'acquisto.

+0

Grazie per aver trovato il tempo di scrivere una risposta! È pieno di belle informazioni generali, ma mentre è utile per alcune persone, non fornisce alcuna nuova visione non presente nella domanda o nessuna giustificazione sul perché usare il carico specificamente volatile (al contrario di 'loadFence()', o qualcos'altro). –

+0

Forse non capisco esattamente di cosa hai bisogno. Perché non risponde? Non c'è niente da usare tranne vread/'loadFence()'.Usa la lettura volatile se vuoi una semantica/r su una variabile specifica e 'loadFence()' se hai bisogno di barriere solo per es. per set di variabili o * prima * lettura della variabile per evitare il riordino specifico. Modificherò la mia risposta non appena ti avrò capito :) Suppongo di poter rimuovere una parte di no-ops e VH e aggiungere il confronto di 'loadFence()' e vandalo se lo vuoi – qwwdfsad

+0

Ottieni la semantica acquisita da entrambe le letture volatili e ' loadFence() ', e in teoria, il carico volatile può essere più costoso su alcune architetture perché deve essere parte delle azioni di sincronizzazione. Quindi non è così semplice come "basta usare la lettura volatile", almeno non senza argomentazioni leggermente più sostanziali. Ho assunto che volatile read e 'loadFence()' sono le uniche opzioni praticabili, ma spero ancora che qualcuno faccia emergere un'altra opzione interessante. –

1

A seconda dei requisiti specifici, eseguire un carico non volatile, possibilmente seguito da un possibile carico volatile è il massimo che si possa ottenere in Java.

È possibile fare questo con una combinazione di

int permits = theUnsafe.getInt(object, offset); 
if (!enough(permits)) 
    permits = theUnsafe.getVolatileInt(object, offset); 

Questo modello può essere utilizzato in buffer circolare per minimizzare zangola di linee di cache.

+1

Questo frammento non è molto utile: Carico volatile e carico normale sono letteralmente la stessa, perché x86 ha ordine totale deposito e la maggior parte delle barriere sono gabbie, l'unica differenza è nel barriere compilatore (non vale per questo frammento), quindi in realtà questo è legge solo due pianura :) – qwwdfsad

+0

@qwwdfsad si stanno assumendo che specificamente x86 verrà utilizzato :) che può essere una scommessa sicura la maggior parte del tempo, ma non è il caso qui (sto correndo su Aarch64, Raspberry Pi 3 per essere specifici). Anche se io sono/siamo già in acque pericolose con 'sun.misc.Unsafe' e la versione/acquisizione sottostimata, è utile ragionare in termini generali quando possibile. @PeterLawrey non è possibile che i due carichi vengano compressi speculativamente in un unico carico volatile? Immagino che non sarà un grosso problema, mi sto solo chiedendo. –

+0

@DimitarDimitrov potrebbero essere combinati ma abbiamo visto vantaggi di prestazioni nell'utilizzo di carichi non volatili per evitare di assumere la proprietà della linea cache. –

Problemi correlati