El código de la app es memoria

El código que escribes es en sí mismo una forma de uso de la memoria. Cada clase, método y constante de cadena de tu aplicación se debe cargar en la RAM cuando se ejecuta. Cuanto más grande sea la base de código de tu aplicación, más memoria consumirá solo para existir.

Memoria respaldada por archivos y paginación a demanda

Android carga código ejecutable desde tu .apk (como archivos .oat o .so) con mmap. Esto significa que el código está respaldado por un archivo.

Fundamentalmente, Android usa la paginación a demanda. Cuando se inicia la app, el kernel no carga todo el APK en la RAM de inmediato. En cambio, solo asigna el archivo al espacio de direcciones virtuales del proceso. A medida que se ejecuta tu app y la CPU salta a una función nueva, se activa un "error de página". El kernel pausa el subproceso, lee esa página específica de 4 KB de código del almacenamiento en la RAM física y reanuda la ejecución.

Un diagrama que ilustra la paginación por demanda, en el que se muestran las páginas virtuales que se asignan a las páginas de RAM físicas solo cuando se accede a ellas

Esto significa que el código que empaquetas pero nunca ejecutas no usa memoria física para las páginas de código. Sin embargo, las bibliotecas sin usar aumentan el tamaño general del APK y pueden incrementar significativamente la memoria que usan los metadatos internos del sistema (como los índices DEX y los descriptores de clase), que se deben leer incluso para saber que existe el código. Además, muchas bibliotecas contienen inicializadores estáticos o se ven afectadas por frameworks de inyección de dependencias durante el inicio de la app, lo que hace que se paginen en la RAM de todos modos.

Desalojo y ralentizaciones de páginas

Dado que la memoria respaldada por archivos siempre se puede volver a leer desde el almacenamiento, el kernel considera que estas páginas están "limpias". Cuando el sistema experimenta presión de memoria, el kernel expulsará (descartará) estas páginas de código limpias de la RAM para dejar espacio para otras cosas.

Si tu app necesita ejecutar ese código más adelante, la CPU fallará y el kernel deberá volver a leer la página del almacenamiento. Cuanto más código tenga tu app, más vulnerable será a que se quite su código. Cuando un usuario regresa a tu app sobrecargada después de usar otras apps, experimentará bloqueos y ralentizaciones aleatorios, ya que la CPU se detendrá constantemente a la espera de que se vuelva a cargar el código desde el almacenamiento.

El costo de un error de página: Si bien varía mucho según la velocidad de almacenamiento del dispositivo (UFS frente a eMMC) y el estado del kernel, un error de página grave (lectura de 4 KB del almacenamiento) puede costar entre 0.5 ms y 5 ms. Si tu ruta de inicio toca 500 páginas diferentes de código no optimizado, podrías introducir fácilmente varios cientos de milisegundos de latencia de E/S pura en el tiempo de inicio de tu app.

Explora el tamaño del código con Compiler Explorer

Para desarrollar una intuición sobre cómo tu código Java o Kotlin se traduce en código máquina nativo (y, por lo tanto, en bytes de memoria), puedes usar Compiler Explorer.

La compatibilidad con Android está integrada directamente en Godbolt. Te permite ver cómo las diferentes partes de la cadena de herramientas de Android (D8, R8 y dex2oat) transforman tu código fuente.

Cómo usar Compiler Explorer con Android

  1. Navega a godbolt.org.
  2. Selecciona Android Java o Android Kotlin en el menú desplegable de idiomas (esquina superior izquierda).
  3. En el menú desplegable del compilador (en la parte superior derecha del panel de código), puedes elegir entre diferentes herramientas:
    • d8: Muestra el código de bytes de Dalvik (.dex). Esta es la representación más cercana a tu código original y es más fácil de leer.
    • r8: Muestra cómo el optimizador R8 reduce y optimiza tu código de bytes.
    • dex2oat: Muestra el código máquina ARM64 final que se ejecuta en el dispositivo. Aquí es donde puedes ver el impacto real en la memoria (4 bytes por instrucción). dex2oat puede segmentar diferentes ISA, pero ARM64 es la más común para teléfonos celulares.
  4. Destacado de fuente y resultado: Si colocas el cursor sobre una línea de código, se destacarán las instrucciones correspondientes de código de bytes o código máquina, lo que facilita el seguimiento del impacto de sentencias específicas.
  5. Canalización de optimización: En la vista de desensamblado, puedes hacer clic en Agregar nuevo… -> Canalización de Opt. Esto te permite ver los pasos internos que realiza el compilador. Puedes inspeccionar cómo se transforma la Representación interna (IR) en cada etapa (p.ej., entre los pasos "Inliner (antes)" y "Inliner (después)") antes de que se convierta en el código máquina ARM64 final.

Captura de pantalla de la IU de Compiler Explorer que muestra un programa de muestra y su canalización de optimización y desensamblaje de salida de dex2oat con el paso de la inserción en línea

Por qué es importante para la memoria

Cada instrucción que ves en el resultado de dex2oat que se orienta a la ISA de ARM64 ocupa 4 bytes en el archivo ejecutable (.odex o .oat) de tu app.

Intenta ingresar código que utilice diferentes funciones del lenguaje y estudia el resultado del compilador:

  • Acceso a arrays vs. iteradores de listas:
    • Un simple bucle de array sobre int[] podría compilarse en alrededor de 10 instrucciones (alrededor de 40 bytes).
    • Un bucle foreach en un List usa implícitamente un Iterator. Esto puede generar entre 30 y 40 instrucciones (~160 bytes) debido a las llamadas a métodos adicionales (hasNext(), next()) y la asignación del objeto iterador en sí.
    • Optimización de R8: En las condiciones adecuadas (p.ej., cuando se demuestra que List es un ArrayList), el optimizador de R8 puede transformar un bucle foreach en un bucle indexado simple, lo que elimina la sobrecarga del iterador y reduce el tamaño del código y la saturación de la memoria en el tiempo de ejecución.
  • Llamadas a métodos virtuales: Implican cargar la clase del objeto, buscar el método en vtable y, luego, bifurcar. Por lo general, esto requiere entre 4 y 5 instrucciones (~20 bytes).
  • Llamadas directas o estáticas: A menudo, se traducen en una sola instrucción bl (rama con vínculo) (4 bytes).
  • Lambdas de Kotlin: Pueden generar clases anónimas completas y métodos de puente adicionales, lo que agrega cientos de bytes de sobrecarga de código y metadatos para un bloque funcional simple.

Con Compiler Explorer, puedes ver cómo las funciones de lenguaje sofisticadas (como las expresiones lambda de Kotlin, las APIs de transmisión o el uso intensivo de genéricos) afectan el tamaño final compilado de tu aplicación y cómo los optimizadores, como R8, pueden contrarrestar el costo de las abstracciones de lenguaje en algunos casos. Esta herramienta puede ayudarte a tomar decisiones fundamentadas sobre las compensaciones al diseñar e implementar una app.

En términos generales, cuanto más complejo sea el código de tu app, mayor será el uso de memoria. Por el contrario, el código más simple, o el código que simplifica R8, genera una representación más pequeña como instrucciones de CPU y bytes en el almacenamiento y la RAM.

Cómo medir el impacto del código con meminfo y showmap

Puedes usar las herramientas de memoria estándar de Android para ver cuánta memoria consume el código de tu app.

dumpsys meminfo

Cuando ejecutas adb shell dumpsys meminfo <package>, la categoría Code en la sección App Summary proporciona una vista de alto nivel de la memoria relacionada con el código:

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     3244
         Native Heap:     5412
                Code:    24512  # <--- Sum of .so, .dex, .oat, .art, etc.

showmap

Para obtener una vista más detallada, usa showmap. Revela regiones de archivos específicos que se asignan a la memoria.

adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"

Verás entradas para el código compilado de tu aplicación:

   size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
  12288     8192     8192     8192        0        0        0        0        0 /data/app/.../base.odex

Código no utilizado y R8

Dado que cada método ejecutado ocupa memoria, tener una app "hinchada" con inicializaciones innecesarias o bibliotecas sin usar puede afectar gravemente el rendimiento del inicio y el uso de memoria de referencia.

Por eso, las herramientas como R8 (ProGuard) son fundamentales. R8 analiza el código de bytes de tu aplicación y quita las clases o los métodos a los que nunca se llama ("eliminación de código no utilizado").

Ejercicio práctico: El costo de la hinchazón

Para demostrar el impacto del tamaño del código, considera un experimento que compara dos compilaciones de una aplicación que contiene 300 clases generadas (cada una con 500 métodos):

  • CodeBloat (sin optimizar): Es la compilación estándar sin optimizar que contiene todas las clases generadas y las cadenas únicas.
  • CodeBloatOptimized: Es el mismo código fuente, pero compilado con la reducción de R8 habilitada.

1. Compilación ahead-of-time (AOT)

Para maximizar el impacto de la memoria respaldada por archivos, usaremos la herramienta cmd package compile para compilar las apps con anticipación (AOT) en archivos .oat.

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized

Ten en cuenta que este es un ejemplo sintético. Por lo general, las apps usarán el modo de compilación speed-profile (consulta más abajo).

2. Lanza y compara

Para ver un inicio verdaderamente en frío en el que el sistema debe leer el código del almacenamiento, descartaremos la caché de páginas del kernel antes de iniciar cada app. Esto requiere acceso de administrador.

Inicia la app sin optimizar:

adb shell am force-stop com.android.codebloat
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5 # Wait for the background thread to load classes
adb shell dumpsys meminfo -s com.android.codebloat

Ahora, haz lo mismo con la app optimizada:

adb shell am force-stop com.android.codebloat.optimized
# Drop page cache to ensure the start is truly cold
adb shell "echo 3 > /proc/sys/vm/drop_caches"
adb shell am start -W -n com.android.codebloat.optimized/com.android.codebloat.MainActivity
sleep 5
adb shell dumpsys meminfo -s com.android.codebloat.optimized
Los resultados

Si observas la fila Code en la sección App Summary, verás una diferencia enorme:

  • Sin optimizar Code: Alrededor de 30,000 KB (30 MB)
  • Optimizado Code: Alrededor de 2,000 KB (2 MB)

Dado que R8 determinó que los 500 métodos dentro de esas clases nunca hacían nada útil (el método doSomething() solo llama a method0(), y los resultados se ignoran), quitó casi todo el código generado artificialmente del APK final.

3. Cómo ver el impacto en Perfetto

El impacto del exceso de código se puede observar claramente durante la fase de carga inicial de la aplicación. Específicamente, busca el segmento bindApplication en el subproceso principal y los segmentos anidados que comienzan con madvising, que indican que el sistema se está preparando para cargar archivos del APK y su código compilado (.odex).

En un inicio en frío interactivo, el sistema mmap() y madvise() el código y otros datos de estos archivos que son necesarios para que la app se cargue y se ejecute. El valor que se encuentra después de "size=" en los segmentos de madvising indica la cantidad de datos que se deben cargar. Esta recuperación previa del código de la app se realiza para acelerar el inicio de la app.

En la comparación, podemos ver que la cantidad de código de la app que se debió cargar del almacenamiento a la RAM fue mucho mayor en el caso de la app inflada, lo que generó duraciones más largas que contribuyeron a un inicio más lento de la app. Además, el registro del inicio de la app inflada muestra segmentos para cargar archivos DEX secundarios (classes2.dex, classes3.dex) en los que la app inflada se vio obligada a "desbordarse" porque no cabía en un solo archivo DEX.

Comparación (inicio en frío en el Pixel 10a)
Métrica Sin optimizar (CodeBloat) Optimizado (CodeBloatOptimized)
base.odex tamaño de madvise ~7.9 MB (2.0 ms) ~16 KB (0.003 ms)
base.apk tamaño de madvise Aprox. 2.4 MB (2.4 ms) ~4 KB (0.001 ms)
classes2.dex tamaño de madvise Aproximadamente 7.3 MB (8.6 ms) N/A
classes3.dex tamaño de madvise Aprox. 7.3 MB (8.0 ms) N/A
Duración total de madvising Aprox. 21 ms ~0.004 ms
Rendimiento de carga de la app no optimizado

Captura de pantalla de la IU de Perfetto que muestra el proceso com.android.codebloat con los segmentos de madvising para los archivos DEX principal y secundario

Rendimiento de carga de la app optimizado

Captura de pantalla de la IU de Perfetto que muestra el proceso com.android.codebloat.optimized con un solo segmento pequeño de madvising

El impacto del exceso de código varía según el tamaño de la app, las características del dispositivo del usuario y la carga del sistema.

PerfettoSQL para el análisis de carga

Puedes usar las siguientes consultas para extraer estas métricas de tus registros.

1. Duración del inicio de la app

Muestra el tiempo transcurrido desde que se inicia la actividad de una app hasta que esta dibuja un primer fotograma.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';

Consulta Información sobre los diferentes estados de inicio de la app.

La duración del inicio de una app depende de muchos factores, además de los que se abordan en esta guía.

2. Extrae los tamaños y las duraciones de madvising

Esta consulta se enfoca en la parte madvising que vimos anteriormente.

INCLUDE PERFETTO MODULE slices.with_context;

SELECT
  name,
  dur/1e6 AS dur_ms
FROM thread_slice
WHERE process_name LIKE 'com.android.codebloat%'
  AND name LIKE 'madvising %';
3. Desglose del estado del subproceso principal (duración total por estado)

Esta consulta muestra cuánto tiempo pasó el subproceso principal de la app en diferentes estados.

SELECT
  p.name AS process_name,
  state,
  sum(dur)/1e6 AS total_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
GROUP BY p.name, state;

Puedes definir mejor la consulta para que solo se muestren los estados del subproceso principal durante la duración del inicio de la app.

INCLUDE PERFETTO MODULE android.startup.startups;

SELECT
  p.name AS process_name,
  ts.state,
  -- Calculate only the duration that falls within the startup window
  SUM(
    MAX(0,
      MIN(ts.ts + ts.dur, s.ts + s.dur) - MAX(ts.ts, s.ts)
    )
  ) / 1e6 AS startup_dur_ms
FROM thread_state ts
JOIN thread t USING (utid)
JOIN process p USING (upid)
-- Join on the package name to align thread states with the correct startup
JOIN android_startups s ON s.package = p.name
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.is_main_thread = 1
  -- Only select thread states that overlap with the startup interval
  AND ts.ts + ts.dur > s.ts
  AND ts.ts < s.ts + s.dur
GROUP BY 1, 2
ORDER BY startup_dur_ms DESC;

Esto puede exponer algunos problemas interesantes, por ejemplo:

  • Tiempo alto dedicado a Runnable (R), pero no a Running: Esto indica que el inicio de la app se retrasó debido a la contención de la CPU, es decir, el subproceso principal de la app no se pudo ejecutar porque otros subprocesos (posiblemente de otras apps) estaban ocupando las CPUs.
  • Mucho tiempo dedicado al sueño interrumpible (D): Por lo general, esto indica una E/S lenta o una presión de memoria que detiene el inicio de la app.
  • Tiempo de inactividad (S) prolongado: Esto significa que el subproceso principal estaba esperando que otros subprocesos realizaran el trabajo. A veces, esto indica una contención de bloqueo en la ruta de inicio de la app (es decir, el subproceso principal se bloqueó en un recurso exclusivo que estaba ocupado por otro subproceso en la app).
4. Memoria respaldada por archivo máxima (archivo RSS)

Esta métrica se correlaciona bien con la cantidad de código y datos que carga la app durante el inicio. Una app más "hinchada" alcanzará un número más alto aquí, lo que provocará presión de memoria en el sistema. A su vez, esta presión puede retrasar el inicio de la app, ya que el sistema tiene dificultades para satisfacer las solicitudes de asignación o desvía el tiempo de CPU de enfocarse en iniciar la app y lo dedica a recuperar memoria de otros procesos para satisfacer las necesidades inmediatas de la app que se está iniciando.

SELECT
  p.name AS process_name,
  max(c.value)/1024.0/1024.0 AS max_rss_file_mb
FROM counter c
JOIN process_counter_track t ON c.track_id = t.id
JOIN process p USING (upid)
WHERE p.name LIKE 'com.android.codebloat%'
  AND t.name = 'mem.rss.file'
GROUP BY p.name;

Modos de compilación y memoria de ART

Android Runtime (ART) puede compilar el código de la aplicación en uno de varios modos diferentes, también conocidos como filtros del compilador. El filtro de compilador seleccionado tiene un impacto directo en el espacio en memoria de tu app.

  • verify: ART solo realiza la verificación de bytecode. No se realiza ninguna compilación AOT. El código se ejecuta a través del intérprete o se compila en el tiempo de ejecución con el compilador JIT.
    • Impacto en la memoria: Es el tamaño más pequeño en el disco. El uso de memoria del código nativo se envía a JIT Cache (memoria sucia anónima).
  • speed: ART realiza la compilación AOT completa de todos los métodos.
    • Impacto en la memoria: Es el tamaño de .odex más grande. Maximiza el uso de la memoria respaldada por archivos (limpia).
  • speed-profile: ART solo compila los métodos que se marcaron como "frecuentes" en un perfil de JIT.
    • Impacto en la memoria: Enfoque equilibrado. Solo el código más crítico se compila con AOT.

El filtro más común es speed-profile, que se usa cuando se instalan apps del usuario. Esto se configura en las propiedades del sistema pm.dexopt.install y pm.dexopt.bg-dexopt, y, por lo general, se establece en build/make/target/product/runtime_libart.mk.

Algunas apps del sistema usarán la compilación de speed y también se compilarán durante el tiempo de compilación de la imagen del sistema. Por lo general, verify solo se usa en casos de uso de desarrollo.

Caso de uso Filtro de compilador típico
Desarrollo verify
Imagen del sistema speed
Apps de usuario speed-profile

Ejercicio práctico: Modos de compilación y memoria

Podemos usar la app de CodeBloat para ver cómo afectan estos filtros a la memoria. Para reproducir estas mediciones, haz lo siguiente:

  1. Fuerza la recompilación de la app en el modo de destino.
  2. Fuerza la detención y el inicio en frío de la app.
  3. Espera a que el subproceso en segundo plano termine de tocar las clases (observa logcat o espera 5 s).
  4. Ejecuta adb shell dumpsys meminfo com.android.codebloat.

Modo: verify (sin AOT)

adb shell cmd package compile -m verify -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

En el modo verify, el resumen de la app muestra lo siguiente: * PSS de código: ~8,000 KB * Dalvik Other (JIT): ~25,000 KB

Como no se compila ningún código con AOT, el tiempo de ejecución debe compilar con JIT los métodos activos en la caché de JIT, que aparece como memoria anónima sucia (Dalvik Other).

Modo: speed (AOT completa)

adb shell cmd package compile -m speed -f com.android.codebloat
adb shell am force-stop com.android.codebloat
adb shell am start -W -n com.android.codebloat/.MainActivity
sleep 5
adb shell dumpsys meminfo com.android.codebloat

En el modo speed, los resultados cambian drásticamente: * Code PSS: ~24,000 KB * Dalvik Other (JIT): ~5,000 KB

El código de la aplicación ahora se asigna desde el archivo .odex como memoria limpia respaldada por archivos. Esto reduce la presión sobre la caché del JIT y hace que la memoria sea apta para la expulsión bajo presión, en lugar de quedar "atascada" como RAM no sincronizada.

Modo: speed-profile (AOT selectiva)

Las apps modernas pueden incluir un perfil de Baseline de baseline.prof. ART usa esta información para compilar de forma selectiva solo el código necesario para un inicio rápido y eficiente en términos de memoria.

En este ejercicio, crearemos un perfil de Baseline para enumerar las clases de inicio de la app. Sin embargo, en realidad, el compilador también puede recibir perfiles de fuentes externas, como la tienda de aplicaciones ("perfiles de nube"), que pueden proporcionar perfiles JIT obtenidos de forma colaborativa para las apps, independientemente de si el desarrollador también incluyó un perfil de referencia que generó.

Cómo generar y usar perfiles integrados en el dispositivo

Para ver el impacto de speed-profile, puedes generar tu propio perfil integrado en el dispositivo:

  1. Restablecer y comenzar:

    adb shell am force-stop com.android.codebloat
    
  2. Interact: Inicia la app y permite que ejecute su secuencia de inicio.

  3. Dump Profile:

    adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)
    

    (Esto obliga a la app a escribir su perfil actual en el disco).

  4. Install Profile:

    adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \
    /data/misc/profiles/ref/com.android.codebloat/primary.prof
    
  5. Compilar:

    adb shell cmd package compile -m speed-profile -f com.android.codebloat
    

Cuando vuelvas a iniciar la app, verás un saldo: PSS de código será inferior a speed (p.ej., ~16,000 KB) porque solo se compilaron los métodos de inicio "activos", y el resto se dejó para que el intérprete o el JIT los controlen solo si se usan.

Consulta los siguientes vínculos:

Análisis detallado del código compilado

Si quieres ver exactamente qué instrucciones genera ART, consulta art/DISASSEMBLY_GUIDE.md.

Proporciona instrucciones detalladas sobre el uso de lo siguiente:

  • oatdump: Para ver instrucciones de ARM64 dentro de un archivo .odex existente
  • dex2oat: Para simular la compilación con marcas de depuración detalladas.

Ejercicio: Inlining de código

Una de las razones por las que el código compilado puede crecer de forma inesperada es la inserción de métodos. El compilador puede decidir copiar el cuerpo de un método pequeño que se llama con frecuencia directamente en sus llamadores.

En nuestra app de CodeBloat, el método doSomething() en cada clase generada simplemente llama a method0(). Cuando se compila en el modo speed, es probable que el compilador de optimización de ART inserte method0() en doSomething().

Ejercicio: Verifica esto con oatdump en tu dispositivo:

# 1. Find the path to the application's APK and compiled .odex file
adb shell pm path com.android.codebloat
# Output: package:/data/app/~~.../base.apk

adb shell "dumpsys package com.android.codebloat | grep 'location is' | head -n 1"
# Example output: [location is /data/app/~~.../oat/arm64/base.odex]

# 2. Run oatdump (substituting the correct path to base.odex)
adb shell oatdump --oat-file=/data/app/~~.../oat/arm64/base.odex \
                  --class-filter=com.android.codebloat.GeneratedClass0

Busca el método doSomething en el resultado. Si se insertó en línea, verás las instrucciones para cargar la constante de cadena larga directamente dentro de doSomething, en lugar de una instrucción bl que apunte a method0.

Visualización de la optimización (CFG)

Para ver exactamente cuándo el compilador decidió intercalar el método, puedes generar un gráfico de flujo de control (CFG). Esto muestra el estado del código en cada etapa de la canalización de optimización, con cada transformación sobre la representación intermedia (RI) del compilador hasta que el código se reduce a la ISA objetivo (p.ej., ARM64).

  1. Ejecuta dex2oat con marcas de volcado: Usa la marca --verbose-methods para limitar el resultado a métodos específicos; de lo contrario, el archivo .cfg de una app grande puede crecer hasta alcanzar varios gigabytes.

    # Substitution of actual paths required:
    adb shell dex2oat64 --dex-file=/data/app/~~.../base.apk \
                        --oat-file=/data/local/tmp/dump.odex \
                        --compiler-filter=speed \
                        --dump-cfg=/data/local/tmp/codebloat.cfg \
                        --verbose-methods=doSomething
    
  2. Pull and View: Extrae el archivo .cfg a tu estación de trabajo y ábrelo con IR Hydra.

  3. Busca el Inliner: En IR Hydra, carga los artefactos de compilación y busca doSomething. Compara la representación antes y después del paso de Inliner. Verás que el gráfico se expande a medida que las instrucciones de method0 se combinan en el llamador.

Como alternativa, usa la herramienta Opt Pipeline en Compiler Explorer (como se describe en la sección anterior) y, luego, ingresa un código similar para ver una transformación similar que se realiza en el paso Inliner.

Ejercicio: Campos volátiles y barreras de memoria

En la app de MemoryLab, el campo mGarbageSink está marcado como volatile. Esto garantiza que el compilador no optimice nuestras asignaciones de basura.

public volatile byte[] mGarbageSink;

En el desensamblaje de ARM64, verás que cada almacenamiento en este campo se acompaña de una barrera de memoria (dmb ish) o del uso de instrucciones de carga-adquisición/almacenamiento-liberación (ldar/stlr). Esto garantiza la visibilidad del subproceso, pero agrega algunas instrucciones adicionales a cada acceso, lo que aumenta ligeramente el tamaño del código en comparación con un campo normal.

Ejercicio: Busca los accesos a campos y las barreras de memoria asociadas en el desensamblado.

Ejercicio: Verificaciones de suspensión implícitas

Si desensamblas un bucle, como el que se muestra en generateAllocationChurn, notarás una instrucción curiosa al final del cuerpo del bucle:

ldr x21, [x21]

Esta es una verificación de suspensión implícita. ART usa esto para permitir que el recolector de basura pause los subprocesos de forma segura. Por lo general, el registro x21 apunta a sí mismo. Cuando el GC necesita suspender el subproceso, "envenena" esa ubicación de memoria. La próxima vez que el subproceso ejecute ese ldr, se activará una falla que el tiempo de ejecución detectará y usará para hacer que el subproceso pase a un estado suspendido.

Este patrón se repite en cada bucle y al comienzo de cada método, lo que contribuye al tamaño total del código de tu aplicación.

Ejercicio: Busca todas las verificaciones de suspensión implícitas en el desensamblado del método y trata de correlacionarlas con el código fuente original.


← WebView | ↑ Arriba | Threads →