Uçucu pahalı mı?


111

Derleyici Yazarları için JSR-133 Cookbook for Compiler Writers'ı okuduktan sonra , özellikle "Atomik Talimatlarla Etkileşimler" bölümünü, güncellemeden uçucu bir değişkeni okumanın bir LoadLoad veya LoadStore bariyerine ihtiyaç duyduğunu varsayıyorum. Sayfanın ilerleyen kısımlarında LoadLoad ve LoadStore'un X86 CPU'larda etkin bir şekilde işlem yapılmadığını görüyorum. Bu, uçucu okuma işlemlerinin x86'da açık bir önbellek geçersiz kılma olmadan yapılabileceği ve normal bir değişken okuma kadar hızlı olduğu anlamına mı geliyor (uçucu maddenin yeniden sıralama kısıtlamalarını göz ardı ederek)?

Bunu doğru anlamadığıma inanıyorum. Biri beni aydınlatabilir mi?

DÜZENLEME: Çok işlemcili ortamlarda farklılıklar olup olmadığını merak ediyorum. John V.'nin belirttiği gibi tek CPU sistemlerinde CPU kendi iş parçacığı önbelleklerine bakabilir, ancak çoklu CPU sistemlerinde CPU'lara bunun yeterli olmadığı ve ana belleğin vurulması gerektiği ve uçuculuğu daha yavaş hale getirecek bazı yapılandırma seçenekleri olmalıdır. çoklu cpu sistemlerinde, değil mi?

Not: Bu konuda daha fazla bilgi edinme yolunda, aşağıdaki harika makalelere rastladım ve bu soru başkaları için ilginç olabileceğinden bağlantılarımı burada paylaşacağım:


1
Bahsettiğiniz birden çok CPU ile konfigürasyon hakkındaki düzenlememi okuyabilirsiniz. Kısa ömürlü bir referans için çoklu CPU sistemlerinde, ana belleğe tek bir okuma / yazma gerçekleşmeyebilir.
John Vint

2
uçucu okuma kendisi pahalı değildir. ana maliyet, optimizasyonları nasıl engellediğidir. pratikte, volatil sıkı bir döngüde kullanılmadıkça, ortalama maliyet de çok yüksek değildir.
irreputable

2
İnfoq ( infoq.com/articles/memory_barriers_jvm_concurrency ) ile ilgili bu makale de ilginizi çekebilir, farklı mimariler için üretilen kod üzerinde uçucu ve senkronize edilmiş etkileri gösterir. Bu aynı zamanda jvm'nin tek işlemcili bir sistemde çalışıp çalışmadığını bildiğinden ve bazı bellek engellerini atlayabildiğinden, zamanın ötesinde bir derleyiciden daha iyi performans gösterdiği bir durumdur.
Jörn Horstmann

Yanıtlar:


123

Intel'de tartışmasız bir uçucu okuma oldukça ucuzdur. Aşağıdaki basit durumu ele alırsak:

public static long l;

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

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

Java 7'nin montaj kodunu yazdırma yeteneğini kullanarak çalıştırma yöntemi şuna benzer:

# {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

Getstatic için 2 referansa bakarsanız, birincisi bellekten bir yükleme içerir, ikincisi, değer zaten yüklendiği kayıtlardan (kayıtlardan) yeniden kullanıldığında yükü atlar (uzun 64 bit ve 32 bit dizüstü bilgisayarımda 2 kayıt kullanır).

L değişkenini uçucu yaparsak, ortaya çıkan montaj farklı olur.

# {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

Bu durumda, l değişkenine yapılan getstatic referanslarının her ikisi de bellekten bir yük içerir, yani değer, çok sayıda uçucu okumada bir kayıtta tutulamaz. Atomik bir okuma olduğundan emin olmak için, değer ana bellekten bir MMX yazmacına movsd 0x6fb7b2f0(%ebp),%xmm0okunur ve bu da okuma işlemini tek bir talimat yapar (önceki örnekte 64 bit değerinin 32 bitlik bir sistemde normalde iki 32 bit okuma gerektirdiğini gördük).

Dolayısıyla, geçici bir okumanın toplam maliyeti, kabaca bir bellek yüküne eşit olacaktır ve bir L1 önbellek erişimi kadar ucuz olabilir. Bununla birlikte, başka bir çekirdek geçici değişkene yazıyorsa, önbellek satırı, bir ana bellek veya belki de bir L3 önbellek erişimi gerektirecek şekilde geçersiz kılınacaktır. Gerçek maliyet, büyük ölçüde CPU mimarisine bağlı olacaktır. Intel ve AMD arasında bile önbellek tutarlılık protokolleri farklıdır.


yan not, java 6 aynı montajı gösterme yeteneğine sahiptir (bunu yapan etkin
noktadır

+1 JDK5'te uçucu herhangi bir okuma / yazma ile ilgili olarak yeniden sıralanamaz (örneğin, çift kontrol kilidini düzeltir). Bu, uçucu olmayan alanların nasıl manipüle edileceğini de etkileyeceği anlamına mı geliyor? Uçucu ve uçucu olmayan alanlara erişimi karıştırmak ilginç olacaktır.
ewernli

@evemli, dikkatli olmalısın, bu açıklamayı bir kez de yapmıştım ama yanlış olduğu tespit edildi. Bir uç durum var. Java Bellek Modeli, mağazalar geçici mağazalardan önce yeniden sipariş edilebildiğinde roach motel anlambilimine izin verir. Bunu IBM sitesindeki Brian Goetz makalesinden aldıysanız, bu makalenin JMM spesifikasyonunu gereğinden fazla basitleştirdiğini belirtmekte fayda var.
Michael Barker

20

Genel olarak konuşursak, çoğu modern işlemcide uçucu bir yük, normal bir yük ile karşılaştırılabilir. Uçucu bir mağaza, montior-giriş / monitör-çıkış süresinin yaklaşık 1 / 3'ü kadardır. Bu, önbellek uyumlu sistemlerde görülür.

OP'nin sorusuna cevap vermek için, uçucu yazılar pahalıdır, ancak okumalar genellikle pahalı değildir.

Bu, uçucu okuma işlemlerinin x86'da açık bir önbellek geçersiz kılma olmadan yapılabileceği ve normal bir değişken okuma kadar hızlı olduğu anlamına mı geliyor (uçucu maddelerin yeniden sıralama kısıtlamalarını göz ardı ederek)?

Evet, bazen bir alanı doğrularken CPU ana belleğe bile ulaşmayabilir, bunun yerine diğer iş parçacığı önbelleklerini gözetleyebilir ve oradan değeri alabilir (çok genel açıklama).

Bununla birlikte, Neil'in önerisine, birden fazla iş parçacığı tarafından erişilen bir alana sahipseniz, onu bir AtomicReference olarak sarmalamanız gerektiği şeklindeki önerisini ikinci kez görüyorum. Bir AtomicReference olarak, okumalar / yazmalar için kabaca aynı verimi yürütür, ancak aynı zamanda, alana birden çok iş parçacığı tarafından erişilip değiştirileceği daha açıktır.

OP'nin düzenlemesine cevap vermek için düzenleyin:

Önbellek tutarlılığı biraz karmaşık bir protokoldür, ancak kısaca: CPU'lar ana belleğe eklenmiş ortak bir önbellek hattını paylaşacaktır. Bir CPU belleği yüklerse ve başka bir CPU'ya sahip değilse, CPU bunun en güncel değer olduğunu varsayacaktır. Başka bir CPU aynı bellek konumunu yüklemeye çalışırsa, önceden yüklenmiş CPU bunun farkında olur ve aslında önbelleğe alınmış referansı talep eden CPU ile paylaşır - şimdi istek CPU'su, CPU önbelleğinde bu belleğin bir kopyasına sahiptir. (Referans için asla ana belleğe bakmak zorunda kalmadı)

Biraz daha fazla protokol var ama bu neler olup bittiğine dair bir fikir veriyor. Ayrıca diğer sorunuzu yanıtlamak için, birden fazla işlemcinin olmaması nedeniyle, geçici okuma / yazma işlemleri aslında birden çok işlemciden daha hızlı olabilir. Aslında tek bir CPU ile birden çok CPU ile aynı anda daha hızlı çalışan bazı uygulamalar vardır.


5
Bir AtomicReference, getAndSet, CompareAndSet vb. Gibi ek işlevsellik sağlayan eklenmiş yerel işlevlere sahip geçici bir alana sarmalayıcıdır, bu nedenle, ek işlevselliğe ihtiyacınız varsa, performans açısından onu kullanmak yararlıdır. Ama neden buradaki işletim sistemine başvurduğunuzu merak ediyorum? İşlevsellik, doğrudan CPU işlem kodlarında uygulanır. Ve bu, bir CPU'nun diğer CPU'ların önbellek içerikleri hakkında hiçbir bilgisinin olmadığı çok işlemcili sistemlerde, CPU'ların her zaman ana belleği vurması gerektiği için uçucuların daha yavaş olduğu anlamına mı geliyor?
Daniel

Haklısın, işletim sistemi hakkında konuşmayı özledim, şimdi bunu düzelterek CPU yazmalıydı. Ve evet, AtomicReference'ın sadece geçici alanlar için bir sarmalayıcı olduğunu biliyorum, ancak aynı zamanda alanın kendisine birden çok iş parçacığı tarafından erişilebileceğini bir tür belge olarak da ekler.
John Vint

@John, AtomicReference aracılığıyla neden başka bir yönlendirme eklemelisiniz? CAS'a ihtiyacınız varsa - tamam, ancak AtomicUpdater daha iyi bir seçenek olabilir. Hatırladığım kadarıyla AtomicReference hakkında hiçbir içkinlik yok.
bestsss

@bestsss Tüm genel amaçlar için, AtomicReference.set / get ile uçucu yük ve depolar arasında hiçbir fark olmadığında haklısınız. Bununla birlikte, hangisini ne zaman kullanacağım konusunda aynı hislere sahip olduğumu (ve bir dereceye kadar yaptım) söyleniyor. Bu yanıt, biraz stackoverflow.com/questions/3964317/… detaylandırabilir . Bunlardan birini kullanmak daha çok bir tercih, AtomicReference'ı basit bir uçucu madde yerine kullanmak için tek argümanım açık dokümantasyon içindir - bu da anladığım kadarıyla en büyük argümanı oluşturmuyor
John Vint

Bir yan nota göre, bazıları geçici bir alan / AtomicReference kullanmanın (bir CAS'a ihtiyaç duymadan) buggy koduna yol açtığını iddia ediyor old.nabble.com/…
John Vint

12

Java Bellek Modelinin ifadesiyle (JSR 133'te Java 5+ için tanımlandığı gibi), bir volatiledeğişken üzerindeki herhangi bir işlem - okuma veya yazma - aynı değişkendeki diğer herhangi bir işlemle ilgili olarak bir önceden-olur ilişkisi oluşturur . Bu, derleyicinin ve JIT'in, iş parçacığı içindeki talimatları yeniden sıralama veya işlemleri yalnızca yerel önbellek içinde gerçekleştirme gibi belirli optimizasyonlardan kaçınmaya zorlandığı anlamına gelir.

Bazı optimizasyonlar mevcut olmadığından, ortaya çıkan kod, muhtemelen çok fazla olmasa da, olması gerekenden daha yavaştır.

Yine de volatile, synchronizedblokların dışındaki birden çok iş parçacığından erişilebileceğini bilmediğiniz sürece bir değişken oluşturmamalısınız . Hatta o zaman uçucu karşı en iyi seçimdir olmadığını düşünmelisiniz synchronized, AtomicReferenceonun arkadaşları, açık ve Lockvb sınıfları,


4

Uçucu bir değişkene erişim, birçok yönden, senkronize bir bloktaki sıradan bir değişkene erişimi sarmaya benzer. Örneğin, uçucu bir değişkene erişim, CPU'nun erişimden önce ve sonra talimatları yeniden düzenlemesini engeller ve bu genellikle yürütmeyi yavaşlatır (ancak ne kadar olduğunu söyleyemem).

Daha genel olarak, çok işlemcili bir sistemde, uçucu bir değişkene erişimin cezasız bir şekilde nasıl yapılabileceğini görmüyorum - A işlemcisine yazmanın, B işlemcisindeki okumayla senkronize edilmesini sağlamanın bir yolu olmalı.


4
Uçucu değişkenleri okumak, talimatların yeniden sıralama olasılıklarıyla ilgili olarak bir monitör girişi yapmakla aynı cezaya sahiptir, oysa uçucu bir değişken yazmak bir monitör çıkışına eşittir. Bir fark, hangi değişkenlerin (örneğin işlemci önbellekleri) temizlendiği veya geçersiz kılınması olabilir. Senkronize edilmiş her şeyi temizlerken veya geçersiz kılarken, uçucu değişkene erişim her zaman önbelleği göz ardı etmelidir.
Daniel

12
-1, Uçucu bir değişkene erişmek, senkronize bir blok kullanmaktan oldukça farklıdır. Senkronize bir bloğa girmek, kilidi çıkarmak için atomik bir karşılaştırma ve ayarlama tabanlı yazma ve serbest bırakmak için geçici bir yazma gerektirir. Kilit yeterliyse, kilide karar vermek için kontrolün kullanıcı alanından çekirdek alanına geçmesi gerekir (bu pahalı bittir). Bir uçucuya erişim her zaman kullanıcı alanında kalacaktır.
Michael Barker

@MichaelBarker: Tüm monitörlerin uygulama tarafından değil, çekirdek tarafından korunması gerektiğinden emin misiniz?
Daniel

@Daniel: Senkronize bir blok veya bir Kilit kullanan bir monitörü temsil ediyorsanız, evet, ancak sadece monitör memnunsa. Bunu çekirdek tahkimi olmadan yapmanın tek yolu, iş parçacığını park etmek yerine aynı mantığı, ancak meşgul dönüşü kullanmaktır.
Michael Barker

@MichaelBarker: Okey, memnun kilitler için bunu anlıyorum.
Daniel
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.