L'environnement d'exécution Android (ART) a été considérablement amélioré dans la version Android 8.0 release. La liste ci-dessous récapitule les améliorations que les fabricants d'appareils peuvent attendre d'ART.
Collecteur de mémoire compactant concurrent
Comme annoncé lors de la Google I/O, ART propose un nouveau collecteur de mémoire compactage concurrent dans Android 8.0. Ce collecteur compacte le tas de mémoire chaque fois que la récupération de mémoire s'exécute et pendant l'exécution de l'application, avec une seule courte pause pour le traitement des racines de thread. Voici ses avantages :
- Le compresseur de mémoire compacte toujours le tas de mémoire : la taille du tas de mémoire est en moyenne 32% inférieure à celle d'Android 7.0.
- La compression permet l'allocation d'objets de pointeur de bump local au thread : les allocations sont 70% plus rapides que dans Android 7.0.
- Les temps de pause sont 85% plus courts pour le benchmark H2 que pour le compresseur de mémoire d'Android 7.0.
- Les temps de pause ne sont plus proportionnels à la taille du tas de mémoire. Les applications devraient pouvoir utiliser des tas de mémoire volumineux sans se soucier des saccades.
- Détails d'implémentation du compresseur de mémoire : barrières de lecture :
- Les barrières de lecture représentent une petite quantité de travail effectuée pour chaque champ d'objet lu.
- Elles sont optimisées dans le compilateur, mais peuvent ralentir certains cas d'utilisation.
Optimisations de boucle
ART utilise une grande variété d'optimisations de boucle dans la version Android 8.0 :
- Élimination des vérifications de limites
- Statique : les plages sont prouvées comme étant dans les limites au moment de la compilation.
- Dynamique : les tests d'exécution garantissent que les boucles restent dans les limites (sinon, elles sont désoptimisées).
- Élimination des variables d'induction
- Suppression de l'induction morte
- Remplacement de l'induction utilisée uniquement après la boucle par des expressions de forme fermée expressions
- Élimination du code mort dans le corps de la boucle, suppression des boucles entières qui deviennent mortes
- Réduction de la force
- Transformations de boucle : inversion, échange, fractionnement, déroulement, unimodulaire, etc.
- SIMDisation (également appelée vectorisation)
L'optimiseur de boucle réside dans son propre passage d'optimisation dans le compilateur ART. La plupart des optimisations de boucle sont semblables aux optimisations et simplifications ailleurs. Certaines optimisations qui réécrivent le CFG de manière plus élaborée que d'habitude posent problème, car la plupart des utilitaires CFG (voir nodes.h) se concentrent sur la création d'un CFG, et non sur sa réécriture.
Analyse de la hiérarchie des classes
ART dans Android 8.0 utilise l'analyse de la hiérarchie des classes (CHA), une optimisation du compilateur qui dévirtualise les appels virtuels en appels directs en fonction des informations générées par l'analyse des hiérarchies de classes. Les appels virtuels sont coûteux, car ils sont implémentés autour d'une recherche de vtable et nécessitent quelques chargements dépendants. De plus, les appels virtuels ne peuvent pas être intégrés.
Voici un résumé des améliorations associées :
- Mise à jour dynamique de l'état de la méthode à implémentation unique : à la fin du temps de liaison de la classe, lorsque la vtable a été remplie, ART effectue une comparaison entrée par entrée avec la vtable de la superclasse.
- Optimisation du compilateur : le compilateur tire parti des informations d'implémentation unique d'une méthode. Si l'indicateur d'implémentation unique est défini pour une méthode A.foo, le compilateur dévirtualise l'appel virtuel en appel direct, puis tente d'intégrer l'appel direct.
- Invalidation du code compilé : également à la fin du temps de liaison de la classe, lorsque les informations d'implémentation unique sont mises à jour, si la méthode A.foo qui avait auparavant une implémentation unique, mais dont l'état est désormais invalidé, tout le code compilé qui dépend de l'hypothèse selon laquelle la méthode A.foo a une implémentation unique doit être invalidé.
- Désoptimisation : pour le code compilé en direct qui se trouve dans la pile, la désoptimisation est lancée pour forcer le code compilé invalidé en mode interpréteur afin de garantir l'exactitude. Un nouveau mécanisme de désoptimisation, qui est un hybride de désoptimisation synchrone et asynchrone, sera utilisé.
Caches intégrés dans les fichiers .oat
ART utilise désormais des caches intégrés et optimise les sites d'appel pour lesquels suffisamment de données existent. La fonctionnalité de caches intégrés enregistre des informations d'exécution supplémentaires dans les profils et les utilise pour ajouter des optimisations dynamiques à la compilation anticipée.
Dexlayout
Dexlayout est une bibliothèque introduite dans Android 8.0 pour analyser les fichiers dex et les réorganiser en fonction d'un profil. Dexlayout vise à utiliser les informations de profilage d'exécution pour réorganiser les sections du fichier dex lors de la compilation de maintenance inactive sur l'appareil. En regroupant les parties du fichier dex qui sont souvent consultées ensemble, les programmes peuvent avoir de meilleurs modèles d'accès à la mémoire grâce à une meilleure localité, ce qui permet d'économiser de la RAM et de réduire le temps de démarrage.
Étant donné que les informations de profil ne sont actuellement disponibles qu'après l'exécution des applications, dexlayout est intégré à la compilation sur l'appareil de dex2oat lors de la maintenance inactive.
Suppression du cache Dex
Jusqu'à Android 7.0, l'objet DexCache possédait quatre grands tableaux, proportionnels au nombre de certains éléments du DexFile, à savoir :
- chaînes (une référence par DexFile::StringId),
- types (une référence par DexFile::TypeId),
- méthodes (un pointeur natif par DexFile::MethodId),
- champs (un pointeur natif par DexFile::FieldId).
Ces tableaux étaient utilisés pour récupérer rapidement les objets que nous avions résolus précédemment. Dans Android 8.0, tous les tableaux ont été supprimés, à l'exception du tableau des méthodes.
Performances de l'interpréteur
Les performances de l'interpréteur ont été considérablement améliorées dans la version Android 7.0 avec l'introduction de "mterp", un interpréteur doté d'un mécanisme de récupération/décodage/interprétation de base écrit en langage assembleur. Mterp est modelé d'après l'interpréteur Dalvik rapide et est compatible avec arm, arm64, x86, x86_64, mips et mips64. Pour le code de calcul, le mterp d'Art est à peu près comparable à l'interpréteur rapide de Dalvik. Toutefois, dans certaines situations, il peut être beaucoup plus lent, voire considérablement plus lent :
- Performances d'appel
- Manipulation de chaînes et autres utilisateurs intensifs de méthodes reconnues comme intrinsèques dans Dalvik.
- Utilisation plus élevée de la mémoire de la pile
Android 8.0 résout ces problèmes.
Plus d'intégration
Depuis Android 6.0, ART peut intégrer n'importe quel appel dans les mêmes fichiers dex, mais pourrait intégrer uniquement les méthodes feuilles à partir de différents fichiers dex. Cette limitation était due à deux raisons :
- L'intégration à partir d'un autre fichier dex nécessite l'utilisation du cache dex de cet autre fichier dex, contrairement à l'intégration du même fichier dex, qui peut simplement réutiliser le cache dex de l'appelant. Le cache dex est nécessaire dans le code compilé pour quelques instructions telles que les appels statiques, le chargement de chaînes ou le chargement de classes.
- Les mappages de pile n'encodent qu'un index de méthode dans le fichier dex actuel.
Pour résoudre ces limitations, Android 8.0 :
- Supprime l'accès au cache dex du code compilé (voir également la section "Suppression du cache Dex")
- Étend l'encodage du mappage de pile.
Améliorations de la synchronisation
L'équipe ART a ajusté les chemins de code MonitorEnter/MonitorExit et a réduit notre dépendance aux barrières de mémoire traditionnelles sur ARMv8, en les remplaçant par des instructions plus récentes (acquisition/libération) lorsque cela était possible.
Méthodes natives plus rapides
Des appels natifs plus rapides à l'interface JNI (Java Native Interface) sont disponibles à l'aide
des @FastNative et @CriticalNative annotations. Ces optimisations d'exécution ART
intégrées accélèrent les transitions JNI et remplacent la notation !bang JNI désormais obsolète. Les annotations n'ont aucun effet sur les méthodes non natives
et ne sont disponibles que pour le code Java Language de la plate-forme sur le
bootclasspath (aucune mise à jour du Play Store).
L'annotation @FastNative est compatible avec les méthodes non statiques. Utilisez-la
si une méthode accède à un jobject en tant que paramètre ou valeur de retour.
L'annotation @CriticalNative offre un moyen encore plus rapide d'exécuter
des méthodes natives, avec les restrictions suivantes :
-
Les méthodes doivent être statiques : aucun objet pour les paramètres, les valeurs de retour ou un
implicit
this. - Seuls les types primitifs sont transmis à la méthode native.
-
La méthode native n'utilise pas les
JNIEnvetjclassparamètres dans sa définition de fonction. -
La méthode doit être enregistrée avec
RegisterNativesau lieu de s'appuyer sur la liaison JNI dynamique.
@FastNative peut améliorer les performances des méthodes natives jusqu'à 3 fois, et
@CriticalNative jusqu'à 5 fois. Par exemple, une transition JNI mesurée
sur un appareil Nexus 6P :
| Invocation de l'interface JNI (Java Native Interface) | Temps d'exécution (en nanosecondes) |
|---|---|
| JNI standard | 115 |
| !bang JNI | 60 |
@FastNative |
35 |
@CriticalNative |
25 |