Il codice che scrivi è una forma di utilizzo della memoria. Ogni classe, metodo e costante stringa nella tua applicazione deve essere caricata nella RAM durante l'esecuzione. Più grande è la base di codice della tua applicazione, più memoria consumerà solo per esistere.
Memoria basata su file e paging su richiesta
Android carica codice eseguibile dal tuo .apk (ad esempio file .oat o .so)
utilizzando mmap. Ciò significa che il codice è basato su file.
Fondamentalmente, Android utilizza il paging su richiesta. Quando l'app viene avviata, il kernel non carica immediatamente l'intero APK nella RAM. ma esegue solo la mappatura del file nello spazio di indirizzi virtuale del processo. Quando l'app viene eseguita e la CPU passa a una nuova funzione, viene attivato un "errore di pagina". Il kernel mette in pausa il thread, legge quella specifica pagina di codice di 4 KB dallo spazio di archiviazione nella RAM fisica e riprende l'esecuzione.

Ciò significa che il codice che pacchettizzi ma che non esegui non utilizza la memoria fisica per le pagine di codice. Tuttavia, le librerie inutilizzate aumentano comunque le dimensioni complessive dell'APK e possono aumentare significativamente la memoria utilizzata dai metadati interni del sistema (come gli indici DEX e i descrittori di classe), che devono essere letti anche solo per sapere che il codice esiste. Inoltre, molte librerie contengono inizializzatori statici o vengono toccate dai framework di inserimento delle dipendenze durante l'avvio dell'app, causando comunque il loro paging nella RAM.
Eviction e rallentamenti delle pagine
Poiché la memoria basata su file può sempre essere riletta dallo spazio di archiviazione, il kernel considera queste pagine "pulite". Quando il sistema subisce una pressione della memoria, il kernel espelle (elimina) queste pagine di codice pulito dalla RAM per fare spazio ad altre operazioni.
Se in un secondo momento l'app deve eseguire di nuovo il codice, la CPU genererà un errore e il kernel dovrà rileggere la pagina dallo spazio di archiviazione. Più codice contiene la tua app, più è vulnerabile all'eliminazione del codice. Quando un utente torna alla tua app gonfia dopo aver utilizzato altre app, si verificano rallentamenti e scatti casuali perché la CPU si blocca costantemente in attesa che il codice venga riportato dalla memoria.
Il costo di un errore di pagina:sebbene vari notevolmente in base alla velocità di archiviazione del dispositivo (UFS vs. eMMC) e allo stato del kernel, un errore di pagina grave (lettura di 4 KB dall'archiviazione) può costare da 0,5 ms a 5 ms. Se il percorso di avvio tocca 500 pagine diverse di codice non ottimizzato, potresti facilmente introdurre diverse centinaia di millisecondi di latenza I/O pura nel tempo di avvio dell'app.
Esplorare le dimensioni del codice con Compiler Explorer
Per farti un'idea di come il tuo codice Java o Kotlin si traduce in codice macchina nativo (e quindi in byte di memoria), puoi utilizzare Compiler Explorer.
Il supporto di Android è integrato direttamente in Godbolt. Consente di vedere come le diverse parti della toolchain Android (D8, R8 e dex2oat) trasformano il codice sorgente.
Come utilizzare Compiler Explorer con Android
- Vai alla pagina godbolt.org.
- Seleziona Android Java o Android Kotlin dal menu a discesa della lingua (in alto a sinistra).
- Nel menu a discesa del compilatore (in alto a destra nel riquadro del codice), puoi scegliere
tra diversi strumenti:
d8: mostra il bytecode Dalvik (.dex). Questa è la rappresentazione più vicina al codice originale ed è più facile da leggere.r8: mostra come lo strumento di ottimizzazione R8 riduce e ottimizza il bytecode.dex2oat: mostra il codice macchina ARM64 finale che viene effettivamente eseguito sul dispositivo. Qui puoi vedere l'impatto reale sulla memoria (4 byte per istruzione).dex2oatpuò scegliere come target diverse ISA, ma ARM64 è la più comune per i cellulari.
- Evidenziazione sorgente<>output: se passi il mouse sopra una riga di codice, verranno evidenziate le istruzioni corrispondenti di bytecode o codice macchina, in modo da poter tracciare facilmente l'impatto di istruzioni specifiche.
- Pipeline di ottimizzazione: nella visualizzazione di disassemblaggio, puoi fare clic su Aggiungi nuovo… -> Opt Pipeline. In questo modo puoi visualizzare i passaggi interni eseguiti dal compilatore. Puoi esaminare come viene trasformata la rappresentazione interna (IR) in ogni fase (ad esempio, tra i passaggi "Inliner (prima)" e "Inliner (dopo)") prima che venga convertita nel codice macchina ARM64 finale.

Perché è importante per la memoria
Ogni istruzione visualizzata nell'output dex2oat che ha come target l'ISA ARM64 occupa
4 byte nel file eseguibile (.odex o .oat) della tua app.
Prova a inserire un codice che utilizzi funzionalità di linguaggio diverse e studia l'output del compilatore:
- Accesso agli array e iteratori di elenchi:
- Un semplice ciclo di array su
int[]potrebbe essere compilato in circa 10 istruzioni (circa 40 byte). - Un ciclo foreach su un
Listutilizza implicitamente unIterator. Ciò può comportare 30-40 istruzioni (~160 byte) a causa delle chiamate di metodi aggiuntive (hasNext(),next()) e dell'allocazione dell'oggetto iteratore stesso. - Ottimizzazione R8: in determinate condizioni (ad es. quando
Listsi dimostra essere unArrayList), l'ottimizzatore R8 può trasformare un ciclo foreach in un semplice ciclo indicizzato, eliminando l'overhead dell'iteratore e riducendo sia le dimensioni del codice che la saturazione della memoria di runtime.
- Un semplice ciclo di array su
- Chiamate di metodi virtuali: comportano il caricamento della classe dell'oggetto, la ricerca del metodo in
vtablee poi la ramificazione. In genere, questa operazione richiede 4-5 istruzioni (~20 byte). - Chiamate dirette/statiche: spesso si traducono in una singola istruzione
bl(Branch with Link) (4 byte). - Lambda Kotlin: possono generare intere classi anonime e metodi bridge aggiuntivi, aggiungendo centinaia di byte di codice e overhead di metadati per un semplice blocco funzionale.
Utilizzando Compiler Explorer, puoi vedere come le funzionalità avanzate del linguaggio (come le espressioni lambda Kotlin, le API di stream o l'uso intensivo di generici) influiscono sulle dimensioni finali compilate della tua applicazione e come gli ottimizzatori come R8 possono contrastare il costo delle astrazioni del linguaggio in alcuni casi. Questo strumento può aiutarti a fare scelte informate nella progettazione e nell'implementazione di un'app.
In generale, una maggiore complessità nel codice della tua app comporta un maggiore utilizzo della memoria. Al contrario, un codice più semplice o semplificato da R8 genera una rappresentazione più piccola come istruzioni della CPU e byte in memoria e RAM.
Misurare l'impatto del codice con meminfo e showmap
Puoi utilizzare gli strumenti di memoria Android standard per vedere quanta memoria consuma il codice della tua app.
dumpsys meminfo
Quando esegui adb shell dumpsys meminfo <package>, la categoria Codice nella sezione
Riepilogo app fornisce una panoramica generale della memoria correlata al codice:
App Summary
Pss(KB)
------
Java Heap: 3244
Native Heap: 5412
Code: 24512 # <--- Sum of .so, .dex, .oat, .art, etc.
showmap
Per una visualizzazione più granulare, utilizza showmap. Mostra le regioni al di fuori di file specifici
che vengono mappati alla memoria.
adb shell showmap $(pidof <package>) | grep -E "\.oat|\.odex|\.dex|\.apk"
Vedrai le voci relative al codice compilato della tua applicazione:
size RSS PSS clean dirty clean dirty swap swapPSS object
------- -------- -------- -------- -------- -------- -------- -------- -------- ----------------
12288 8192 8192 8192 0 0 0 0 0 /data/app/.../base.odex
Codice inutilizzato e R8
Poiché ogni metodo eseguito occupa memoria, un'app "gonfia" con inizializzazioni non necessarie o librerie inutilizzate può influire gravemente sulle prestazioni di avvio e sulla memoria utilizzata di base.
Per questo motivo, strumenti come R8 (ProGuard) sono fondamentali. R8 analizza il bytecode dell'applicazione e rimuove tutte le classi o i metodi che non vengono mai chiamati ("rimozione del codice inutilizzato").
Esercizio pratico: il costo del bloat
Per dimostrare l'impatto delle dimensioni del codice, considera un esperimento che confronta due build di un'applicazione contenente 300 classi generate (ciascuna con 500 metodi):
- CodeBloat (non ottimizzato): la build standard non ottimizzata contenente tutte le classi generate e le stringhe uniche.
- CodeBloatOptimized: lo stesso codice sorgente, ma compilato con la riduzione R8 attivata.
1. Compilazione ahead-of-time (AOT)
Per massimizzare l'impatto della memoria basata su file, utilizzeremo lo strumento cmd package compile
per compilare in anticipo (AOT) le app in file .oat.
adb shell cmd package compile -m speed -f com.android.codebloat
adb shell cmd package compile -m speed -f com.android.codebloat.optimized
Tieni presente che questo è un esempio sintetico. In genere, le app utilizzano la modalità di compilazione
speed-profile (vedi più avanti).
2. Avvia e confronta
Per vedere un avvio veramente freddo in cui il sistema deve leggere il codice dalla memoria, elimineremo la cache delle pagine del kernel prima di avviare ogni app. Questa operazione richiede l'accesso root.
Avvia l'app non ottimizzata:
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
Ora esegui la stessa operazione per l'app ottimizzata:
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
I risultati
Se esamini la riga Codice nella sezione App Summary, noterai una
differenza enorme:
- Non ottimizzata
Code: circa 30.000 KB (30 MB) - Ottimizzata
Code: circa 2000 KB (2 MB)
Poiché R8 ha stabilito che i 500 metodi all'interno di queste classi non stavano
facendo nulla di utile (il metodo doSomething() chiama solo
method0() e i risultati vengono ignorati), ha rimosso quasi tutto il
codice generato artificialmente dall'APK finale.
3. Visualizzare l'impatto in Perfetto
L'impatto del codice bloat è chiaramente visibile durante la fase di caricamento iniziale dell'applicazione. In particolare, cerca la sezione bindApplication nel
thread principale e le sezioni nidificate che iniziano con madvising, che indicano che il
sistema si sta preparando a caricare i file dall'APK e il relativo codice compilato (.odex).
In un avvio a freddo interattivo, il sistema mmap() e madvise() il codice e
altri dati di questi file necessari per il caricamento e l'esecuzione dell'app. Il valore dopo "size=" nelle sezioni madvising indica la quantità di dati da caricare. Il precaricamento del codice dell'app viene eseguito per accelerare l'avvio dell'app.
Dal confronto possiamo notare che la quantità di codice dell'app che doveva essere
caricata dallo spazio di archiviazione alla RAM era molto maggiore nel caso dell'app bloated, con conseguente
durate più lunghe che hanno contribuito a un avvio più lento dell'app. Inoltre, la traccia dell'avvio dell'app bloated mostra segmenti per il caricamento dei file DEX secondari
(classes2.dex, classes3.dex) che l'app bloated è stata costretta a "riversare" in
perché non rientravano in un unico file DEX.
A confronto (avvio a freddo su Pixel 10a)
| Metrica | Non ottimizzato (CodeBloat) | Ottimizzata (CodeBloatOptimized) |
|---|---|---|
base.odex madvise size |
~7,9 MB (2 ms) | ~16 KB (0,003 ms) |
base.apk madvise size |
~2,4 MB (2,4 ms) | ~4 KB (0,001 ms) |
classes2.dex madvise size |
~7,3 MB (8,6 ms) | N/D |
classes3.dex madvise size |
~7,3 MB (8 ms) | N/D |
Durata totale madvising |
~21 ms | ~0,004 ms |
Prestazioni di caricamento delle app non ottimizzate

Prestazioni di caricamento delle app ottimizzate

L'impatto del codice ridondante varia in base alle dimensioni dell'app, alle caratteristiche del dispositivo dell'utente e al carico del sistema.
PerfettoSQL per l'analisi del caricamento
Puoi utilizzare le seguenti query per estrarre queste metriche dalle tue tracce.
1. Durata dell'avvio dell'app
Mostra il tempo che intercorre tra l'avvio di un'attività dell'app e il disegno del primo frame dell'attività.
INCLUDE PERFETTO MODULE android.startup.startups;
SELECT package, dur, startup_type
FROM android_startups
WHERE package LIKE 'com.android.codebloat%';
Vedi: Informazioni sui diversi stati di avvio dell'app
La durata di avvio di un'app è sensibile a molti fattori diversi da quelli trattati in questa guida.
2. Estrarre le dimensioni e le durate di madvising
Questa query si concentra sulla parte madvising che abbiamo visto sopra.
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. Suddivisione dello stato del thread principale (durata totale per stato)
Questa query mostra quanto tempo ha trascorso il thread principale dell'app in diversi stati.
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;
Puoi perfezionare la query per esaminare solo gli stati del thread principale durante la durata dell'avvio dell'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;
In questo modo, possono emergere alcuni problemi interessanti, ad esempio:
- Runnable (R) con tempo di esecuzione elevato, ma non in esecuzione: indica che l'avvio dell'app è stato ritardato a causa della contesa della CPU, ovvero il thread principale dell'app non è stato eseguito perché altri thread (probabilmente di altre app) occupavano le CPU.
- Tempo elevato trascorso in modalità Sospensione interrompibile (D): in genere indica I/O lento o utilizzo intensivo della memoria che blocca l'avvio dell'app.
- Tempo elevato trascorso in stato di sospensione (S): significa che il thread principale era in attesa che altri thread svolgessero il lavoro. A volte questo indica una contesa di blocchi nel percorso di avvio dell'app (ovvero il thread principale è stato bloccato su una risorsa esclusiva occupata da un altro thread nell'app).
4. Memoria supportata da file massima (file RSS)
Questa metrica è strettamente correlata alla quantità di codice e dati caricati dall'app all'avvio. Un'app più "gonfia" raggiungerà un numero più alto qui, causando una pressione sulla memoria del sistema. Questa pressione può a sua volta ritardare l'avvio dell'app, in quanto il sistema fatica a soddisfare le richieste di allocazione o devia il tempo della CPU dall'avvio dell'app al recupero di memoria da altri processi per soddisfare le esigenze immediate dell'app di avvio.
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;
Modalità di compilazione ART e memoria
Android Runtime (ART) può compilare il codice dell'applicazione in una delle diverse modalità, note anche come filtri del compilatore. Il filtro del compilatore selezionato ha un impatto diretto sul footprint della memoria dell'app.
verify: ART esegue solo la verifica del bytecode. Non viene eseguita la compilazione AOT. Il codice viene eseguito tramite l'interprete o compilato in fase di runtime dal compilatore JIT.- Impatto sulla memoria: dimensioni più ridotte su disco. L'utilizzo della memoria del codice nativo viene
inserito in
JIT Cache(memoria sporca anonima).
- Impatto sulla memoria: dimensioni più ridotte su disco. L'utilizzo della memoria del codice nativo viene
inserito in
speed: ART esegue la compilazione AOT completa di tutti i metodi.- Impatto sulla memoria: dimensioni
.odexmaggiori. Massimizza l'utilizzo della memoria basata su file (pulita).
- Impatto sulla memoria: dimensioni
speed-profile: ART compila solo i metodi contrassegnati come "hot" in un profilo JIT.- Impatto sulla memoria: approccio bilanciato. Viene compilato AOT solo il codice più critico.
Il filtro più comune è speed-profile, che viene utilizzato durante l'installazione delle app utente. Questa impostazione è configurata nelle proprietà di sistema pm.dexopt.install e
pm.dexopt.bg-dexopt e in genere viene impostata in
build/make/target/product/runtime_libart.mk.
Alcune app di sistema utilizzeranno la compilazione speed e verranno compilate anche al tempo di compilazione dell'immagine di sistema. verify viene in genere utilizzato solo nei casi d'uso di sviluppo.
| Caso d'uso | Filtro del compilatore tipico |
|---|---|
| Sviluppo | verify |
| Immagine di sistema | speed |
| App utente | speed-profile |
Esercizio pratico: modalità di compilazione e memoria
Possiamo utilizzare l'app CodeBloat per vedere in che modo questi filtri influiscono sulla memoria. Per
riprodurre queste misurazioni:
- Forza la ricompilazione dell'app nella modalità di destinazione.
- Forza l'interruzione e l'avvio a freddo dell'app.
- Attendi il completamento dell'accesso alle classi da parte del thread in background (guarda logcat o attendi 5 secondi).
- Esegui
adb shell dumpsys meminfo com.android.codebloat.
Modalità: verify (nessun 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
In modalità verify, il riepilogo dell'app mostra: * Code PSS: circa 8000 KB * Dalvik
Other (JIT): circa 25.000 KB
Poiché non viene compilato alcun codice AOT, il runtime deve compilare JIT i metodi hot nella
cache JIT, che viene visualizzata come memoria anonima modificata (Dalvik
Other).
Modalità: speed (AOT completo)
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
In modalità speed, i risultati cambiano drasticamente: * Code PSS: circa 24.000 kB *
Dalvik Other (JIT): circa 5000 kB
Il codice dell'applicazione ora viene mappato dal file .odex come memoria pulita
supportata da file. Ciò riduce la pressione sulla cache JIT e rende la
memoria idonea all'eliminazione in caso di pressione, anziché rimanere "bloccata" come RAM
modificata.
Modalità: speed-profile (AOT selettivo)
Le app moderne possono includere un profilo di base baseline.prof. ART lo utilizza per
compilare selettivamente solo il codice necessario per un avvio rapido ed efficiente in termini di memoria.
In questo esercizio creeremo un profilo di base per elencare le classi di avvio dell'app. Tuttavia, in realtà il compilatore potrebbe ricevere profili anche da fonti esterne come lo store di applicazioni ("profili cloud"), che possono fornire profili JIT crowdsourcing per le app indipendentemente dal fatto che lo sviluppatore abbia anche incluso un profilo di base generato.
Generare e utilizzare profili sul dispositivo
Per vedere l'impatto di speed-profile, puoi generare il tuo profilo
sul dispositivo:
Reimposta e avvia:
adb shell am force-stop com.android.codebloatInteragisci: avvia l'app e lascia che esegua la sequenza di avvio.
Dump Profile:
adb shell kill -s SIGUSR1 $(pidof com.android.codebloat)In questo modo l'app scrive il profilo attuale sul disco.
Installa profilo:
adb shell cp /data/misc/profiles/cur/0/com.android.codebloat/primary.prof \ /data/misc/profiles/ref/com.android.codebloat/primary.profCompila:
adb shell cmd package compile -m speed-profile -f com.android.codebloat
Al riavvio, vedrai un bilanciamento: Code PSS sarà inferiore a
speed (ad es.~16.000 KB) perché sono stati compilati solo i metodi di avvio "hot ",
mentre il resto viene gestito dall'interprete o JIT solo se vengono effettivamente
utilizzati.
Vedi:
Analisi approfondita del codice compilato
Se vuoi vedere esattamente quali istruzioni genera ART, consulta
art/DISASSEMBLY_GUIDE.md.
Fornisce istruzioni dettagliate sull'utilizzo di:
oatdump: per visualizzare le istruzioni ARM64 all'interno di un file.odexesistente.dex2oat: per simulare la compilazione con flag di debug dettagliati.
Esercizio: incorporamento del codice
Uno dei motivi per cui il codice compilato può aumentare in modo imprevisto è l'inlining dei metodi. Il compilatore può decidere di copiare il corpo di un metodo piccolo e chiamato di frequente direttamente nei chiamanti.
Nella nostra app CodeBloat, il metodo doSomething() in ogni classe generata
chiama semplicemente method0(). Quando viene compilato in modalità speed, il compilatore di ottimizzazione di ART probabilmente inserirà method0() in doSomething().
Allenamento:verifica questa informazione utilizzando oatdump sul tuo 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
Cerca il metodo doSomething nell'output. Se è stato incorporato, vedrai
le istruzioni per caricare la costante stringa lunga direttamente all'interno di doSomething,
anziché un'istruzione bl che ha come target method0.
Visualizzare l'ottimizzazione (CFG)
Per vedere esattamente quando il compilatore ha deciso di incorporare il metodo, puoi produrre un grafico del flusso di controllo (CFG). Mostra lo stato del codice in ogni fase della pipeline di ottimizzazione, con ogni trasformazione nella rappresentazione intermedia (IR) del compilatore fino a quando il codice non viene ridotto all'ISA di destinazione (ad es. ARM64).
Esegui
dex2oatcon i flag di dump: utilizza il flag--verbose-methodsper limitare l'output a metodi specifici; in caso contrario, il file.cfgper un'app di grandi dimensioni può raggiungere diversi gigabyte.# 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=doSomethingEstrai e visualizza: estrai il file
.cfgsulla workstation e aprilo con IR Hydra.Trova l'inliner: in IR Hydra, carica gli artefatti di compilazione e cerca
doSomething. Confronta la rappresentazione prima e dopo il passaggio dell'Inliner. Vedrai il grafico espandersi man mano che le istruzioni dimethod0vengono unite al chiamante.
In alternativa, utilizza lo strumento Opt Pipeline in Compiler Explorer (come descritto nella sezione precedente) e inserisci un codice simile per visualizzare una trasformazione simile eseguita nel passaggio Inliner.
Esercizio: campi volatili e barriere di memoria
Nell'app MemoryLab, il campo mGarbageSink è contrassegnato come volatile. In questo modo, il compilatore non ottimizza le nostre allocazioni di spazzatura.
public volatile byte[] mGarbageSink;
Nel disassemblaggio ARM64, vedrai che ogni memorizzazione in questo campo è
accompagnata da una barriera di memoria (dmb ish) o dall'uso di
istruzioni Load-Acquire/Store-Release (ldar/stlr). Ciò garantisce la visibilità dei thread, ma aggiunge alcune istruzioni aggiuntive a ogni accesso, aumentando leggermente le dimensioni del codice rispetto a un campo normale.
Esercizio: trova gli accessi ai campi e le barriere di memoria associate nel disassemblaggio.
Esercizio: controlli di sospensione implicita
Se smonti un ciclo, come quello in generateAllocationChurn, noterai un'istruzione curiosa alla fine del corpo del ciclo:
ldr x21, [x21]
Questo è un controllo di sospensione implicita. ART lo utilizza per consentire al Garbage
Collector di sospendere i thread in modo sicuro. Il registro x21 in genere punta a se stesso.
Quando il GC deve sospendere il thread, "avvelena" la posizione di memoria. La
prossima volta che il thread esegue ldr, verrà attivato un errore che
il runtime rileva e utilizza per far passare il thread a uno stato sospeso.
Questo pattern viene ripetuto in ogni ciclo e all'inizio di ogni metodo, contribuendo alle dimensioni totali del codice dell'applicazione.
Esercizio:trova tutti i controlli di sospensione implicita nel disassembly del metodo e prova a metterli in correlazione con il codice sorgente originale.