Mejoras de ART en Android 8.0

Se mejoró considerablemente el entorno de ejecución de Android (ART) en la versión de Android 8.0. En la siguiente lista, se resumen las mejoras que los fabricantes de dispositivos pueden esperar en ART.

Recolector de elementos no utilizados de compactación simultánea

Como se anunció en Google I/O, ART incluye un nuevo recolector de elementos no utilizados (GC) de compactación simultánea en Android 8.0. Este recolector compacta el montón cada vez que se ejecuta GC y mientras la app está en ejecución, con solo una breve pausa para procesar las raíces de los subprocesos. Estos son sus beneficios:

  • GC siempre compacta el montón: tamaños de montón un 32% más pequeños en promedio en comparación con Android 7.0.
  • La compactación permite la asignación de objetos de puntero de incremento local del subproceso: Las asignaciones son un 70% más rápidas que en Android 7.0.
  • Ofrece tiempos de pausa un 85% más pequeños para la comparativa H2 en comparación con el GC de Android 7.0.
  • Los tiempos de pausa ya no se ajustan con el tamaño del montón; las apps deberían poder usar montones grandes sin preocuparse por el jank.
  • Detalles de implementación de GC: Barreras de lectura
    • Las barreras de lectura son una pequeña cantidad de trabajo que se realiza para cada campo de objeto leído.
    • Estos se optimizan en el compilador, pero podrían ralentizar algunos casos de uso.

Optimizaciones de bucle

ART emplea una amplia variedad de optimizaciones de bucle en la versión de Android 8.0:

  • Eliminaciones de verificación de límites
    • Estática: Se demuestra que los rangos están dentro de los límites en el tiempo de compilación.
    • Dinámica: Las pruebas de tiempo de ejecución garantizan que los bucles permanezcan dentro de los límites (de lo contrario, se desoptimizan).
  • Eliminaciones de variables de inducción
    • Quita la inducción no alcanzada.
    • Reemplaza la inducción que se usa solo después del bucle por expresiones de forma cerrada
  • Eliminación de código no alcanzado dentro del cuerpo del bucle, eliminación de bucles completos que se vuelven no alcanzados
  • Reducción de la fuerza
  • Transformaciones de bucle: inversión, intercambio, división, desenrollado, unimodular, etcétera
  • SIMDización (también llamada vectorización)

El optimizador de bucle reside en su propio paso de optimización en el compilador de ART. La mayoría de las optimizaciones de bucle son similares a las optimizaciones y simplificación en otros lugares. Los desafíos surgen con algunas optimizaciones que reescriben el CFG de una manera más elaborada de lo habitual, ya que la mayoría de las utilidades de CFG (consulta nodes.h) se enfocan en compilar un CFG, no en reescribirlo.

Análisis de jerarquía de clases

ART en Android 8.0 usa el análisis de jerarquía de clases (CHA), una optimización del compilador que desvirtualiza las llamadas virtuales en llamadas directas según la información generada mediante el análisis de jerarquías de clases. Las llamadas virtuales son costosas, ya que se implementan en torno a una búsqueda de vtable y toman un par de cargas dependientes. Además, las llamadas virtuales no se pueden insertar.

Este es un resumen de las mejoras relacionadas:

  • Actualización dinámica del estado del método de implementación única: Al final del tiempo de vinculación de la clase, cuando se completó la vtable, ART realiza una comparación entrada por entrada con la vtable de la superclase.
  • Optimización del compilador: El compilador aprovechará la información de implementación única de un método. Si se establece una marca de implementación única para un método A.foo, el compilador desvirtualizará la llamada virtual en una llamada directa y, además, intentará insertar la llamada directa como resultado.
  • Invalidación de código compilado: También al final del tiempo de vinculación de la clase, cuando se actualiza la información de implementación única, si el método A.foo que antes tenía una implementación única, pero ese estado ahora está invalidado, todo el código compilado que depende de la suposición de que el método A.foo tiene una implementación única debe tener su código compilado invalidado.
  • Desoptimización: Para el código compilado en vivo que está en la pila, se iniciará la desoptimización para forzar el código compilado invalidado al modo de intérprete para garantizar la exactitud. Se usará un nuevo mecanismo de desoptimización que es un híbrido de desoptimización síncrona y asíncrona.

Cachés insertadas en archivos .oat

ART ahora emplea cachés insertadas y optimiza los sitios de llamadas para los que existen suficientes datos. La función de cachés insertadas registra información adicional del tiempo de ejecución en los perfiles y la usa para agregar optimizaciones dinámicas a la compilación anticipada.

Dexlayout

Dexlayout es una biblioteca introducida en Android 8.0 para analizar archivos dex y reordenarlos según un perfil. El objetivo de Dexlayout es usar la información de generación de perfiles del tiempo de ejecución para reordenar las secciones del archivo dex durante la compilación de mantenimiento inactivo en el dispositivo. Al agrupar las partes del archivo dex a las que se accede con frecuencia, los programas pueden tener mejores patrones de acceso a la memoria a partir de una localidad mejorada, lo que ahorra RAM y reduce el tiempo de inicio.

Dado que la información del perfil actualmente solo está disponible después de que se ejecutan las apps, dexlayout se integra en la compilación en el dispositivo de dex2oat durante el mantenimiento inactivo.

Eliminación de la caché de dex

Hasta Android 7.0, el objeto DexCache poseía cuatro arrays grandes, proporcionales a la cantidad de ciertos elementos en el DexFile, a saber:

  • strings (una referencia por DexFile::StringId)
  • types (una referencia por DexFile::TypeId)
  • methods (un puntero nativo por DexFile::MethodId)
  • fields (un puntero nativo por DexFile::FieldId)

Estos arrays se usaban para recuperar rápidamente objetos que habíamos resuelto anteriormente. En Android 8.0, se quitaron todos los arrays, excepto el array de métodos.

Rendimiento del intérprete

El rendimiento del intérprete mejoró significativamente en la versión de Android 7.0 con la introducción de "mterp", un intérprete que incluye un mecanismo central de recuperación, decodificación e interpretación escrito en lenguaje ensamblador. Mterp se modela según el intérprete rápido de Dalvik y admite arm, arm64, x86, x86_64, mips y mips64. Para el código computacional, el mterp de Art es aproximadamente comparable al intérprete rápido de Dalvik. Sin embargo, en algunas situaciones, puede ser significativamente, e incluso drásticamente, más lento:

  1. Rendimiento de invocación
  2. Manipulación de cadenas y otros usuarios pesados de métodos reconocidos como intrínsecos en Dalvik.
  3. Mayor uso de memoria de pila

Android 8.0 soluciona estos problemas.

Más inserción

Desde Android 6.0, ART puede insertar cualquier llamada dentro de los mismos archivos dex, pero solo puede insertar métodos hoja de diferentes archivos dex. Hubo dos motivos para esta limitación:

  1. La inserción desde otro archivo dex requiere usar la caché de dex de ese otro archivo dex, a diferencia de la inserción del mismo archivo dex, que solo puede volver a usar la caché de dex del llamador. La caché de dex es necesaria en el código compilado para un par de instrucciones, como llamadas estáticas, carga de cadenas o carga de clases.
  2. Los mapas de pila solo codifican un índice de método dentro del archivo dex actual.

Para abordar estas limitaciones, Android 8.0 hace lo siguiente:

  1. Quita el acceso a la caché de dex del código compilado (consulta también la sección "Eliminación de la caché de dex").
  2. Extiende la codificación del mapa de pila.

Mejoras de sincronización

El equipo de ART ajustó las rutas de código MonitorEnter/MonitorExit y redujo nuestra dependencia de las barreras de memoria tradicionales en ARMv8, reemplazándolas por instrucciones más nuevas (adquirir/liberar) cuando fue posible.

Métodos nativos más rápidos

Las llamadas nativas más rápidas a la interfaz nativa de Java (JNI) están disponibles con las @FastNative y @CriticalNative anotaciones. Estas optimizaciones integradas del entorno de ejecución de ART aceleran las transiciones de JNI y reemplazan la notación !bang JNI ahora obsoleta. Las anotaciones no tienen ningún efecto en los métodos no nativos y solo están disponibles para el código Java Language de la plataforma en el bootclasspath (sin actualizaciones de Play Store).

La anotación @FastNative admite métodos no estáticos. Usa esta opción si un método accede a un jobject como parámetro o valor de retorno.

La anotación @CriticalNative proporciona una forma aún más rápida de ejecutar métodos nativos, con las siguientes restricciones:

  • Los métodos deben ser estáticos: no hay objetos para parámetros, valores de retorno ni un implícito this.
  • Solo se pasan tipos básicos al método nativo.
  • El método nativo no usa los JNIEnv y jclass parámetros en su definición de función.
  • El método debe registrarse con RegisterNatives en lugar de depender de la vinculación dinámica de JNI.

@FastNative puede mejorar el rendimiento del método nativo hasta 3 veces, y @CriticalNative hasta 5 veces. Por ejemplo, una transición de JNI medida en un dispositivo Nexus 6P:

Invocación de la interfaz nativa de Java (JNI) Tiempo de ejecución (en nanosegundos)
JNI normal 115
!bang JNI 60
@FastNative 35
@CriticalNative 25