Mitos de la interrupción del rendimiento de Android según Trcinskaagalina Octubre de 2020

A lo largo de los años, han surgido varios mitos sobre el rendimiento en Android. Si bien algunos mitos pueden ser divertidos o entretenidos, enviar en la dirección incorrecta al crear aplicaciones poderosas para Android no es divertido.

En esta publicación de blog, en el espíritu de MythBusters, vamos a tmiestos mitos. Usamos ejemplos y herramientas del mundo real que puede utilizar para erradicar mitos. Nos centramos en los patrones de uso dominantes: cosas que es probable que ustedes, como desarrolladores, hagan en su aplicación. Hay una advertencia importante: recuerde que es importante medir antes de decidir utilizar el procedimiento de codificación por motivos de rendimiento. Es decir, rompemos algunos mitos.

El equipo de Google Drive ha trasladado su aplicación de Java a Kotlin. Esta conversión involucró más de 16,000 líneas de código en 170 archivos que cubren más de 40 objetivos de compilación. Entre las métricas que sigue el equipo, fue una de las primeras en ejecutarse.

Como puede ver, la transferencia a Kotlin no tuvo ningún efecto material.

De hecho, el equipo no notó ninguna diferencia en el rendimiento en el conjunto de comparación. Vieron un pequeño aumento en el tiempo de compilación y el tamaño del código compilado, pero no hubo un impacto significativo en aproximadamente el 2%.

Por el lado de los beneficios, el equipo logró una reducción del 25% en las líneas de código. Su código es más limpio, claro y fácil de mantener.

Una cosa a tener en cuenta sobre Kotlin es que puede y debe usar herramientas de reducción de código como R8, que incluso tiene optimizaciones específicas para Kotlin.

Algunos desarrolladores optan por campos públicos en lugar de utilizar setters y getters por motivos de rendimiento. El patrón de código habitual se ve así, getFoo es nuestro getter:

public class ToyClass {   public int foo;   public int getFoo() { return foo; }}ToyClass tc = new ToyClass();

Comparamos esto usando un campo público, tc.foo, donde el código interrumpe la encapsulación de un objeto para acceder directamente a los campos.

Lo comparamos con Punto de referencia de la biblioteca Jetpack en Pixel 3 con Android 10. La biblioteca de comparación proporciona una manera fantástica de probar fácilmente su código. Las funciones de la biblioteca incluyen precalentar el código para que los resultados representen números estables.

Entonces, ¿qué mostraron los puntos de referencia?

La versión del captador funciona igual que la versión directa al campo. Este resultado no es sorprendente, porque Android Runtime (ART) inserta todos los métodos de acceso triviales en su código. Entonces, el código que se ejecuta después de compilar un JIT o AOT es el mismo. De hecho, cuando accede a un campo en Kotlin, en este ejemplo, tc.foo, accede a ese valor mediante un getter o setter, según el contexto. Sin embargo, como estamos insertando todos los objetos de acceso, ART lo cubrirá aquí: no hay diferencia en el rendimiento.

Si no usa Kotlin, a menos que tenga una buena razón para publicar el campo, no debe violar las buenas prácticas de encapsulación. Es útil ocultar los datos privados de su clase y no tiene que exponerlos solo por razones de rendimiento. Cíñete a getters y setters.

Lambdas, especialmente con la introducción de las API de transmisión, son un diseño de lenguaje conveniente que permite un código muy conciso.

Tomemos un código donde calculamos el valor de algunos campos internos a partir de una serie de objetos. Primero, use API de transmisión con una operación de reducción de mapa.

ArrayList<ToyClass> array = build();int sum = array.stream().map(tc -> tc.foo).reduce(0, (a, b) -> a + b);

Aquí, la primera lambda convierte el objeto en un número entero y la segunda agrega los dos valores que crea.

Esto se compara con la definición de clases equivalentes para expresiones lambda.

ToyClassToInteger toyClassToInteger = new ToyClassToInteger();SumOp sumOp = new SumOp();int sum = array.stream().map(toyClassToInteger).reduce(0, sumOp);

Hay dos clases anidadas: una es toyClassToInteger, que convierte objetos en un número entero, y la otra es una operación de suma.

Obviamente, el primer ejemplo, el que tiene lambdas, es mucho más elegante: la mayoría de los revisores de código probablemente dirán que vaya con la primera opción.

¿Y las diferencias de rendimiento? Nuevamente, usamos la biblioteca Jetpack Benchmark en Pixel 3 con Android 10 y no encontramos diferencias en el rendimiento.

Se puede ver en el gráfico que también estamos definiendo una clase de nivel superior, y tampoco hubo diferencia en el rendimiento.

La razón de esta similitud en el rendimiento es que las lambdas se traducen en clases internas anónimas.

Entonces, en lugar de escribir clases internas, opte por lambda: crea un código mucho más corto y más limpio del que se enamorarán sus revisores de código.

Android utiliza la asignación de memoria y la liberación de memoria de última generación. La asignación de objetos ha mejorado en casi todas las versiones, como muestra el siguiente gráfico.

La liberación de memoria también ha mejorado significativamente de una versión a otra. La recolección de basura hoy en día no tiene ningún impacto en la suavidad de la aplicación o en el jank. El siguiente gráfico muestra las mejoras que hicimos en Android 10 para una colección de objetos de corta duración con una colección concurrente generacional. Mejoras que también son visibles en la nueva versión de Android 11.

El rendimiento aumentó significativamente en más del 170% para las pruebas de referencia de GC como H2 y en un 68% para aplicaciones del mundo real como Google Spreadsheets.

Entonces, ¿cómo afecta esto a las opciones de codificación, como la asignación de objetos mediante grupos?

Si asume que liberar memoria es ineficaz y que asignar memoria es costoso, asume que cuanta menos memoria liberadora cree, menos memoria liberadora tendrá que funcionar. Entonces, en lugar de crear nuevos objetos cada vez que los usa, ¿mantiene un grupo de tipos de uso frecuente y luego obtiene objetos de allí? Entonces puedes implementar algo como esto:

Pool<A> pool[] = new Pool<>[50];void foo() {   A a = pool.acquire();   …   pool.release(a);}

Aquí se omiten algunos detalles del código, pero usted define un grupo en su código, obtiene un objeto del grupo y finalmente lo libera.

Para probar esto, implementamos un microbanco para medir dos cosas: la sobrecarga de la asignación estándar para recuperar un objeto del grupo y la sobrecarga de la CPU para ver si la recolección de basura afecta el rendimiento de la aplicación.

En este caso, usamos Pixel 2 XL con Android 10 ejecutando el código de asignación mil veces en un ciclo muy ajustado. También simulamos diferentes tamaños de objetos agregando campos adicionales, ya que el rendimiento puede variar para objetos pequeños o grandes.

Primero, los resultados de la sobrecarga de asignación de objetos:

En segundo lugar, los resultados de la sobrecarga de la CPU para liberar memoria:

Puede ver que la diferencia entre la asignación estándar y la agrupación de objetos es mínima. Sin embargo, cuando se trata de la recolección de basura para instalaciones más grandes, la solución de la piscina se deteriorará ligeramente.

Este comportamiento es realmente lo que esperamos al liberar memoria, porque al asociar objetos, aumenta la huella de su aplicación. Tiene demasiada memoria a la vez, e incluso si el número de invocaciones de memoria libre disminuye a medida que agrupa objetos, el costo de cada llamada de memoria libre es mayor. Esto se debe a que el recolector de basura tiene que pasar por mucha más memoria para decidir qué está vivo y qué se debe recolectar.

¿Está aclarado este mito? No completamente. Que los grupos de objetos sean más eficientes depende de las necesidades de su aplicación. Primero, recuerde la desventaja de usar fondos, además de la complejidad del código:

  • Puede tener una pista de memoria más alta
  • Riesgo de mantener los elementos con vida más tiempo del necesario
  • Requiere una implementación muy efectiva del fondo

Sin embargo, el enfoque de grupo podría ser útil para objetos asignados grandes o costosos.

La clave para recordar es probar y medir antes de elegir su elección.

Crear perfiles de una aplicación mientras se depura sería muy conveniente, después de todo, normalmente la codificas en modo ajustable. Y aunque la profusión en el depurable es un poco inexacta, la capacidad de iterar más rápido debería compensarlo. Lamentablemente no.

Para probar este mito, analizamos algunos puntos de referencia para los flujos de trabajo comunes relacionados con el trabajo. Los resultados se muestran en el siguiente gráfico.

https://www.sepa.es/eur/Video-zoraek_12.html
https://de.toto.com/i/Video-zoraek_10.html
https://www.fmirobcn.org/fjm/eul/Video-zoraek_11.html
https://www.cristobalbalenciagamuseoa.com/eu/Video-zoraek_9.html
https://www.radurlaub.com/Video-zoraek_17.html

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *