R8 zrychlil korutiny v Androidu až dvakrát, předtím kazil start aplikacím s moduly
Google popsal, jak kompilátor R8 zrychlil v Androidu spouštění korutin až dvojnásobně: přestal se ptát systému na to, co si může spočítat při překladu. Čtyři dny předtím dostal hlášení, že pomocná třída, kterou si k tomu generuje, skončila v nesprávné části aplikace. Ta pak spadla hned při startu.

Aplikace pro Android se dnes většinou píšou v jazyce Kotlin a souběžné operace v nich obstarávají korutiny – lehké úlohy, které umí uprostřed práce ustoupit a později pokračovat tam, kde skončily. Načítání dat ze sítě, animace, reakce na dotyk prstu: pod tím vším běží korutina. Tým Androidu 27. července popsal, proč byly pomalé a co s tím udělal kompilátor R8.
Výsledek: od verze 9.2.0 vývojářského nástroje Android Gradle Plugin zrychlí R8 běžné atomické operace dvakrát až čtyřikrát a spuštění a zrušení korutiny až dvojnásobně. Vývojář pro to nemusí udělat nic než přejít na novou verzi nástroje.
Osmdesát procent času padlo na dotaz, který mohl odpadnout
Nález přišel od týmu kolem Jetpack Compose, což je knihovna, kterou se dnes v Androidu skládá vzhled aplikací. Když se měřilo, kde se ztrácí čas, vyšlo najevo, že u prvku Modifier.clickable – tedy u obyčejného „na tohle se dá kliknout“ – spolklo osmdesát procent času vytváření a rušení vnitřních korutin.
Příčina je v knihovně kotlinx.atomicfu, kterou korutiny používají na atomické zápisy, tedy takové, co nemůže rozpůlit jiné vlákno uprostřed. Podle dokumentace projektu se atomická hodnota na Javě nahrazuje třídou AtomicXxxFieldUpdater ze standardní knihovny. Ta si ale položku hledá podle jména za běhu a pokaždé kontroluje, jestli existuje a jestli je přístupná. Každé spuštění, pozastavení, zrušení i dokončení korutiny volá aspoň jednu takovou operaci.
Kolik to dělá, ukázalo měření na telefonu Pixel 5 s rozhraním API 33. Přímá atomická reference zvládla dvojici zápisů za 50,7 nanosekundy, tatáž operace přes atomicfu potřebovala 135 nanosekund. Blog z toho počítá zpomalení přibližně 2,7krát a dodává, že běhové prostředí ART tuhle režii samo neodstraní.
Kompilátor to spočítá dopředu
R8 je nástroj, který v překladu aplikace zmenšuje a optimalizuje výsledný kód. Vidí přitom celý program naráz, takže pozná, že se ta reflexivní kontrola ptá pořád na totéž. Postup má tři kroky: k původní položce přidá číslo, které říká, kde přesně v paměti objektu hodnota leží, pak nahradí volání aktualizátoru přímým voláním do třídy sun.misc.Unsafe – a nakonec uklidí to, co zůstalo nepoužité.
Náhrada se nedělá naslepo. R8 ji provede jen tam, kde umí dokázat, že aktualizátor pochází z instrumentované položky a že typy sedí; jinde volání nechá být. Kde nemá jistotu, že hodnota není prázdná, vloží kontrolu. Jedna aplikace tak může mít část volání optimalizovanou a část ne.
Zisk se podle blogu projevil hlavně v Compose, jehož vlastní měřítka výkonu korutiny sledují. Po přechodu na novou verzi R8 v nich vyšlo dvojnásobné zlepšení u spouštění a rušení korutiny v LaunchedEffect. Číslo tedy pochází z měřítek jedné knihovny, ne z měření hotových aplikací.
Pomocná třída skončila v modulu, který na telefonu nebyl
K přístupu do sun.misc.Unsafe si R8 generuje vlastní pomocnou třídu, kterou v aplikaci sdílejí všechna optimalizovaná místa. A právě ta se stala předmětem hlášení, které do sledovače chyb dorazilo 23. července. Nepodal ho nikdo z Googlu, ale vývojář, kterému aplikace přestala fungovat po přechodu na verzi 9.3.0 zmíněného nástroje s R8 ve verzi 9.3.16.
Aplikace v hlášení má vedle základního modulu ještě jeden doinstalovatelný, který si telefon stáhne, teprve když ho uživatel potřebuje. R8 pojmenoval pomocnou třídu podle třídy, u které vznikla, a ta patřila právě do doinstalovatelného modulu. Podle hlásitele určilo právě zvolené jméno i to, kam ji kompilátor uloží. V základním balíčku ji přitom potřebovalo přes dvacet dalších tříd – a jsou to samé korutiny a zámky, od StateFlowImpl po vnitřní struktury knihovny Ktor.
Na telefonu to dopadlo tak, že aplikace nedoběhla ani do konce svého startu. Ohlásila NoClassDefFoundError, tedy že hledanou třídu nenašla. Když se stejná aplikace sestavila do jednoho spojeného balíčku, fungovala; chyba se projevila jen tam, kde si telefon stáhl samotný základ – tedy při běžné instalaci z obchodu. Hlásitel to doložil výpisem tříd z jednotlivých částí balíčku.
Oprava vznikla týž den. Autorem obou změn v repozitáři R8 je Søren Gjesse z Googlu a jejich smysl je prostý: pomocná třída pro Unsafe patří vždy do základního balíčku, ať se jmenuje po čemkoli. Vedle opravy přibyl i test, který stav znovu vyvolá, aby se nevrátil. Do hlavní vývojové větve obojí dorazilo 27. července, tedy v den, kdy vyšel blog o zrychlení.
Co z toho pozná majitel telefonu
Přímo nic – a to je na tom podstatné. Aplikace se nezrychlí tím, že si ji uživatel aktualizuje ze současné verze, ale až tehdy, když ji její autor znovu sestaví novějším nástrojem a novou verzi nahraje. U živě vyvíjené aplikace to je otázka nejbližšího vydání, u té, o kterou se nikdo nestará, se nestane nic.
Část zrychlení navíc přišla i bez R8. Blog uvádí, že po úpravách překladu za běhu v novějších verzích běhového prostředí Androidu se táž měřítka zlepšila asi o patnáct procent sama od sebe. Kdo má telefon s čerstvým systémem, kus zisku už tedy má.
Stojí za zaznamenání, že poznámky k vydání Android Gradle Pluginu 9.2.0 z letošního dubna o téhle optimalizaci nemluví ani slovem – mezi změnami v R8 popisují jen dvě jiné úpravy pravidel. Vývojář se o ní dozvěděl až z blogu o tři měsíce později. Podle nás je to škoda: změna, která přepisuje způsob práce s pamětí ve všech knihovnách postavených na atomických operacích, si zmínku v poznámkách zaslouží, i kdyby jen jednořádkovou.
Zdroje: Android Developers Blog, hlášení b/537973315, repozitář R8, kotlinx.atomicfu, poznámky k vydání AGP 9.2.0. O tom, co se v systému změnilo pro uživatele, jsme psali v článku Android 17 přinesl bubliny a strop na paměť aplikací.