R8, Android'de Kotlin eş yordamlarını nasıl 2 kat daha hızlı hale getirdi?
Okuma süresi 7 dakika
AGP 9.2.0'dan itibaren R8, çoğu Atomic*FieldUpdater çağrısını yaygın işlemlerde 2 ila 4 kat daha iyi performans gösteren Unsafe varyantlarına göre optimize eder. Bu durum, özellikle kotlinx.coroutines için atomik işlemler uygulayan kotlinx.atomicfu kitaplığını büyük ölçüde etkiler ve eşzamanlı rutinlerin başlatılmasını ve iptal edilmesini 2 kat daha hızlı hale getirir. Avantajlardan yararlanmak için AGP'nizi 9.2.0 veya sonraki bir sürüme güncelleyin.
Android uygulamalarının büyük çoğunluğu Kotlin'i ana dil olarak seçtiğinden kotlinx.coroutines, eşzamansız programlama için fiili standart haline geldi. Kitaplık, Kotlin'e özgü olan eşzamanlı akışları yönetmek için iyi tasarlanmış ve yapılandırılmış bir yol sunar. Jetpack Compose da bu konuda bir istisna değildi ve işaretçi etkinliklerini, animasyonları ve diğer etkileşimleri yönetmek için eş yordamları kullanmaya başladı. Bu makalenin yazıldığı sırada, Compose'daki çoğu eşzamanlı API, arka planda suspend işlevlerini çağırıyor ve güncellemeleri işlemek için eş yordamları başlatıp/iptal ediyordu.
Compose ekibi performansı araştırmaya başladığında, eş yordamların oluşturma dışında gerçekleşen birçok işlem için performans sorunu olduğu keşfedildi. Örneğin, Modifier.clickable oluşturma ve güncelleme işlemine harcanan sürenin% 80'i, InteractionSource güncellemelerini işleyen dahili eş yordamları başlatma ve iptal etme işleminde kullanıldı. Bu gözlemlere dayanarak, ilk performans çalışmalarının çoğu, eş yordamları varsayılan yoldan kaldırmaya ve başlatmayı gerekli olana kadar ertelemeye odaklandı.
Eş yordamın maliyeti
Bir işlevin Android'deki dahili davranışını analiz etmenin en kolay yolu, Android Çalışma Zamanı (ART) yöntemi izi yakalamaktır. ART yöntemi izi, bir uygulamanın yürütme akışını kaydeden bir araçtır. Hangi yöntemlerin çağrıldığını, bunların sırasını ve her birinde ne kadar zaman harcandığını tam olarak göstererek geliştiricilerin performans darboğazlarını belirlemesine olanak tanır. Boş bir LaunchedEffect { } çağrısı için şöyle görünür:
Yukarıdaki yöntem izi üç bölüme ayrılabilir:
- Yeni bir eş yordam başlatma
- Coroutine başlatılıyor
- Eş yordam tamamlanıyor (çünkü hemen çıkılıyor)
İptal, normal tamamlama işlemine benzer ancak LaunchedEffect oluşturur.CancellationException
Yukarıdaki profilde hemen şüphe uyandıran bir durum, java.util.concurrent.AtomicReferenceFieldUpdater (j… etiketli mor veya yeşil kutular) ile sık sık yapılan görüşmelerdir. Her çağrı nispeten hızlı olsa da sıklık endişe vericidir. Birden fazla çağırmaya yayılan, göz ardı edilemeyecek ek yükler, fark edilebilir bir gerilemeye yol açabilir. Bir görüşmeye yakınlaştırdığınızda, zamanın çoğunun yansıtma kontrolleriyle mi geçtiğini görüyorsunuz?
Coroutine'lar, üst-alt ilişkileri için kilit içermeyen bir ağaç yapısı uygular. Bu yapı, yapılandırılmış eşzamanlılığı mümkün kılar. Kitaplığın, iyi bilinen bir JVM temel öğesi olan AtomicReferenceFieldUpdater'ı kullanarak kilitlenmeyen atomik işlemler uyguladığı ortaya çıktı.kotlinx.atomicfu Güncelleyici, çalışma zamanında atomik işlemler gerçekleştirmek için bir sınıf referansı ve alan adı kullanır. Alanın mevcut olduğundan ve erişilebilir olduğundan emin olmak için çeşitli yansıtıcı güvenlik kontrolleri çalıştırması gerekir. Eş yordamlardaki her işlem (başlatma, askıya alma, iptal etme, tamamlama) en az bir atomik işlemi çağırır. Bu nedenle, yavaşsa eş yordamlar iyi performans göstermez.
AtomicReferenceFieldUpdater'ı inceleme
Ancak bu konuya daha sonra değineceğiz. AtomicReferenceFieldUpdater, 10 yılı aşkın süredir JVM'de iyi bir şekilde optimize edilmiştir ve yöntem izlemeleri, sanal makine düzeyinde optimizasyon (tam zamanında (JIT) veya önceden (AOT) derlemeler) ile tamamen kaldırılan ek yükü yakalayabilir. Performansı doğrulamak için kotlinx.atomicfu ve java.util.concurrent.atomic kaynaklı atomik referanslar arasındaki farkı ölçmek üzere birkaç karşılaştırma ölçütü yazalım.
@RunWith(AndroidJUnit4::class) class AtomicReferenceBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() private val atomicReference = java.util.concurrent.atomic.AtomicReference(false) private val atomicRef = kotlinx.atomicfu.atomic<Boolean>(false) @Test fun atomicReference_compareAndSet() { benchmarkRule.measureRepeated { atomicReference.compareAndSet(true, false) atomicReference.compareAndSet(false, true) } } @Test fun atomicRef_compareAndSet() { benchmarkRule.measureRepeated { atomicRef.compareAndSet(true, false) atomicRef.compareAndSet(false, true) } } /* measuring other methods from the method traces above */ }
Bu karşılaştırma testini Pixel 5'te çalıştırmak (ısınma sırasında AtomicReferenceFieldUpdater#compareAndSet öğesinin JIT derlendiğinden emin olarak) Pixel 5'te (API 33) aşağıdaki sonuçları verir:
50.7 ns atomicReference_compareAndSet 135 ns atomicRef_compareAndSet
Ölçümler, kotlinx.atomicfu sürümünün yaklaşık 2,7 kat daha yavaş olduğunu açıkça göstererek aradaki farkı doğruluyor. Bu, ART'nin herhangi bir gizli optimizasyon yapmadığını ve yansıtıcı erişim kontrollerinin çalışma zamanında gerçek ek yük oluşturduğunu doğrular.
Orijinal yöntem izine baktığımızda, AtomicReferenceFieldUpdater tarafından gerçekleştirilen tek anlamlı çalışmanın, temel atomik işlemi gerçekten yürüten Unsafe.getObjectVolatile'ye yapılan dahili çağrı olduğu görülür. Çoğu durumda, güncelleme hizmeti ilk kullanıma hazırlayıcısı statiktir ve çevreleyen sınıfın yapısına göre her zaman doğru olduğu kanıtlanabilir. Bu nedenle, AtomicReferenceFieldUpdater kullanımlarının çoğu statik olarak analiz edilebilir ve derleme sırasında dahili bir Unsafe varyantıyla değiştirilebilir. Android derleme araç zincirinin de tam olarak bunu yapabilen kendi optimize edici derleyicisi vardır.
R8 ile optimizasyon
Atomic*FieldUpdater sınıfları, ince, dinamik ve yansımaya dayalı kullanımı destekler ancak genellikle statik olarak belirgin desenlerde kullanılır. Bu durum, hem temel performansın yavaş olmasını hem de optimizasyon ihtiyacını açıklar. R8, programın tamamını optimize eden bir derleyicidir ve yansıtıcı güvenlik kontrollerinin ek yükünü azaltmak için daha basit kalıpları incelemeye uygundur. R8, Java veya Kotlin derleyicisinden sonra JVM bayt kodu alır ancak okunabilirliği kolaylaştırmak için bu örnekler Java söz diziminde sunulur. Bu nedenle, AtomicReferenceFieldUpdater için tür bağımsız değişkenleri yoktur.
class Example { volatile String data = ""; static final AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Example.class, String.class, "data"); void example() { // ... updater.compareAndSet(this, "", "new"); // ... } }
Temel örnek, tutucu, tür ve alan adı için basit sabit bağımsız değişkenlerle değişken bir alana erişen statik bir son güncelleyici oluşturur. Kullanılan yansıtma tamamen şeffaftır. Bu güncelleyicinin geçerli bir alana referans verdiği ve güncelleyici oluşturma sitesinin alana geçerli erişimi olduğu açıkça görülmektedir.
Atomic*FieldUpdater, temelde bir alan uzaklığı ve Unsafe çağrıları etrafındaki bir sarmalayıcıdır. Optimizasyon için en iyi senaryo, güncelleyici alanını bir ofset alanıyla ve güncelleyici çağrılarını Unsafe çağrılarıyla değiştirmektir.
Atomic*FieldUpdater'ı optimize etme
Optimizasyon üç bölüm halinde uygulanır: Enstrümantasyon, Değiştirme ve Temizleme.
Araçlar
İlk adım, Unsafe çağrısı üzerinden doğrudan erişimi kolaylaştırmak için güncelleyici alanının yanında ofset alanlarını kullanmaktır.
static final long updater$offset = SyntheticUnsafe.UNSAFE.objectFieldOffset(Example.class.getDeclaredField("data"))
Alana yansıtma yoluyla erişilir ve sınıftaki alan ofsetini çıkarmak için Unsafe kullanılır. Bu kod, yansıtma doğrulamasını göz ardı ederseniz Atomic*FieldUpdater öğesinin dahili kısımlarını temsil eder. Bunun yerine, güncelleyicinin tutucu türü ve değişken alanın alan türü derleyicide statik olarak izlenir.
Orijinal alanın ve başlatma işleminin olduğu gibi bırakıldığını unutmayın. Optimizasyon süreci, kullanımları iyimser bir şekilde kolaylaştırır ve optimize eder, ardından temizleme işlemini gerçekleştirir. Bu, uygulamaya yönelik basit bir yaklaşımdır ancak bazı kullanımlar olduğu gibi bırakılırken diğerleri optimize edildiğinden güncelleme hizmeti alanlarının kısmi optimizasyonuna da olanak tanır.
Değiştirme
Derleyicinin bu noktasında, uygun bir eşzamanlılık birleştirme noktasından sonra, enstrümanlı güncelleyici alanlarının bir listesi bulunur. Bu, her bir çağrı sitesini birkaç koşula göre ayrı ayrı optimize edebileceğimiz anlamına gelir. Örnek bir görüşmeyi inceleyelim:
updater.compareAndSet(holder, expectedValue, newValue);
Atomic*FieldUpdater için gereken koşullar şunlardır:
updater, ölçümlendirilmiş bir alandan mı geliyor? Yani statik analiz, nesnenin değerini izleyerek izleme kodu eklenmiş bir güncelleme hizmetinin alan okumasına kadar takip edebilir mi?holder, başlangıçta tanımlanan sahip türüyle aynı sınıf mı yoksa bu sınıfın bir alt sınıfı mı?newValue, başlangıçta tanımlanan alan türüyle aynı sınıf mı yoksa bu sınıfın bir alt sınıfı mı?
Tüm koşullar karşılanırsa arama, yansıtma kontrolleri yapılmadan Unsafe numarasına yapılan bir aramayla değiştirilir.
SyntheticUnsafe.UNSAFE.compareAndSwapObject(holder, Example.updater$offset, expectedValue, newValue)
Bu yeni çağrı daha hızlı ve basittir ancak updater ve holder içindeki boş değerlerin işlenmesi konusunda orijinal çağrıdan farklıdır. Statik olarak hariç tutulmadığı sürece her ikisi için de boşluk kontrolleri eklenir.
Temizleme
Bu noktada, tutma sınıfında orijinal güncelleyici alanı ve yeni uzaklık alanı ile ikisinden birini kullanabilecek çağrı siteleri bulunur. Arama sitelerinin hiçbiri optimize edilmediyse uzaklık alanı kaldırılmalı, tüm arama siteleri optimize edildiyse güncelleyici alanı kaldırılmalıdır. Her iki durumda da başlatma çağrısı da silinmelidir. Kullanılmayan alanların silinmesi ve ölü kodun kaldırılması derleyicide zaten yapılıyor ancak başlatma kodunun kaldırılması için birkaç numara daha gerekiyor.
Hem newUpdater hem de getDeclaredField çağrısı, istisnalar oluşturabildikleri için yan etkilere sahip olabilir (API sürümüne bağlı olduklarından uygulamaları da bilinmemektedir). Bu nedenle, genel optimizasyonla güvenli bir şekilde kaldırılamazlar. Bu nedenle, bu temizleme işlemi için izlenen alanların açıkça dikkate alınması gerekiyordu. Çünkü bu alanların statik olarak istisnalardan arındırılmış olduğu biliniyordu.
Sonuç olarak, yukarıda gösterilen basit güncelleme hizmeti örneği optimizasyondan sonra şu şekilde görünür:
Sonuçlar
Bu optimizasyonlardan sonra kotlinx.atomicfu ve AtomicInt/Long/ReferenceFieldUpdater'nin en açık kullanımları artık R8 uygulandığında AtomicReference performansıyla eşleşiyor. Hatta bazı kıyaslamalarda daha da hızlıdır. kotlinx.atomicfu, atomic örneklerini alanlara yerleştirebilen bir derleyici eklentisine sahiptir. Bu sayede, atomik olarak güncellenen bir alan oluşturmak için gereken ayırmalar azaltılır.
Bu çalışmadan en çok yararlanan Jetpack Compose oldu. Compose çalışma zamanında, performans gerilemelerini erken aşamada yakalamak için coroutine performansını çok yakından izleyen bir dizi mikro kıyaslama bulunur. Karşılaştırma testleri R8'in yeni bir sürümüne güncellendiğinde, LaunchedEffect içinde eş yordamlar başlatılırken ve iptal edilirken 2 kat iyileşme olduğunu fark ettik.
Bunun dışında, ART ekibi bu optimizasyonları sanal makine düzeyinde yerel olarak uyguluyor. Uygulamanız API 36'yı hedefliyorsa ve Android'in yeni bir sürümünde çalışıyorsa cihazınız eş yordamları benzer şekilde optimize ediyor olabilir. Yukarıdaki eş yordam karşılaştırma testlerinde, ART'nin son sürümlerindeki JIT güncellemelerinden sonra performansta yaklaşık% 15 iyileşme gözlemlendi.
Uygulamanız, AGP 9.2.0'a yükseltildiğinde veya doğrudan R8 9.2.0 kullanıldığında bu optimizasyonu varsayılan olarak alır. Daha fazla bilgi için D8 dexer ve R8 shrinker başlıklı makaleyi inceleyin.
-
Başarılı ÖrneklerPerformans regresyonlarının yeniden üretilmesi zordur ve bu durum, mobil geliştiriciler için büyük bir performans sorunu oluşturur.
Alice Yuan, Arti Arutiunov, Nikita Ogorodnikov • Okuma süresi 4 dakika -
Başarılı ÖrneklerFotMob, kısa süre önce Wear OS'te 5 yıl içinde en büyük tek günlük artışını yaşadı. Bu artış, günlük ortalamanın 2-3 katıydı. Sırrı ne mi? Kullanıcıların Wear OS uygulamalarını doğrudan telefonlarından keşfetmelerine yardımcı olan basit bir cihazlar arası yükleme akışı.
Garan Jenkin • Okuma süresi 3 dakika -
Başarılı ÖrneklerFarkındalık uygulaması Gratitude, günlük tutma, olumlama ve vizyon panoları aracılığıyla tutarlılığı teşvik eder. Uygulama 6 milyondan fazla kez indirildi, 150 bin 5 yıldızlı puan aldı ve 100 milyon günlük girişi kaydedildi.
Amrit Sanjeev, Ash Nohe • Okuma süresi 3 dakika
Android geliştirmeyle ilgili en son analizleri her hafta gelen kutunuza alın.