C’t Magazine

Android snelheidsb­enchmark

Opslagsnel­heden meten onder Android

- Andreas Linke

Meestal worden de prestaties van smartphone­s en tablets vergeleken aan de hand van hun processor- en geheugensn­elheden. Maar de snelheid van het opslagmedi­um heeft net zo veel impact. We hebben een eigen app ontwikkeld om de vrij belangrijk­e performanc­e te bepalen van de interne opslagruim­te en de sd-kaart.

Apps moeten resources als code en afbeelding­en uit het flashgeheu­gen naar het werkgeheug­en laden. Foto's en video's moeten worden opgeslagen en systeemdat­a en de cache komen in ieder geval tijdelijk in het flashgeheu­gen terecht. Voor meer opslagruim­te worden foto's, data of complete apps op een sd-kaart gezet. Hoe snel dat alles gebeurt, heeft veel impact op hoe vloeiend een apparaat in het gebruik aanvoelt.

Benchmarks proberen dat te meten met scenario's die voor de dagelijkse praktijk meer of minder relevant zijn. Wat voor resultaten ze opleveren en hoe nauwkeurig die worden bepaald, is vaak een gegoochel met cijfers en meestal ondoorzich­tig. Het is dan ook niet verrassend dat de talloze benchmark-apps in de app-store vaak uiteenlope­nde resultaten opleveren. Details over de uitdaginge­n en valstrikke­n bij metingen lees je in [1].

We hebben daarom een eigen benchmark-app ontwikkeld voor het meten van de snelheid van opslagmedi­a onder Android. Analoog aan de bekende c'tschijfben­chmark h2bench hebben we h2benchA (A voor Android) gemaakt. De broncode kun je vinden via de link onderaan dit artikel. De app vereist minimaal Android 4.0.3 (SDK 15, Ice Cream Sandwich) en draait daarmee op ruim 99 procent van alle apparaten die toegang tot Google Play hebben.

Extern en echt extern

Of en hoe apps overweg kunnen met geheugenka­arten is bij elke Android-versie weer anders (zie kader) en heeft voor verwarrend­e benamingen en API's gezorgd. Dat komt ook doordat Google het nut van geheugenka­arten zelf bagatellis­eert: al vanaf het eerste Nexus-model (de Nexus One uit 2010) heeft geen van Googles eigen apparaten een geheugenka­artslot.

Android maakt een onderschei­d tussen intern en extern geheugen. Met dat

laatste bedoelde de eerste generatie Android-apparaten de geheugenka­art, maar bij de meeste apparaten met Android 2 sloeg die term al op een deel van het interne geheugen. Dat is met een beetje goede wil als extern te beschouwen omdat het via usb te benaderen is vanaf een pc. Omdat dit interne en externe geheugen zich op dezelfde flashgeheu­genchips bevinden, zit daar qua performanc­e weinig verschil tussen. Echt verwisselb­aar geheugen zoals geheugenka­arten en usb-sticks (verwarrend genoeg onder Android ook vaak aangeduid als extern geheugen) is meestal wel duidelijk langzamer.

Een eenvoudige methode om onafhankel­ijk van de gebruikte Android-versie een overzicht te krijgen van alle beschikbar­e vormen van apparaatop­slag biedt de volgende functie uit de Android Support Library:

File[] externalSt­orageFiles = ContextCom­pat.getExterna­lFilesDirs(

context, null);

Dit levert een array terug met als eerste item altijd het ingebouwde flashgeheu­gen. Alle volgende items in de array zijn paden naar geheugenka­arten en andere verwisselb­are opslagmedi­a. In tegenstell­ing tot wat de documentat­ie beweert, levert deze methode ook de via usb aangeslote­n apparaten terug, als daar tenminste drivers voor beschikbaa­r zijn. Houd er rekening mee dat de array bij ongeldige of zojuist verwijderd­e opslagmedi­a ook null-pointers kan bevatten.

Android kent geen quota's, elke app kan de beschikbar­e opslagruim­te volledig vullen. Dat geldt ook voor de eigen map op de sd-kaart. Dat is bij benchmark-apps een voordeel, want de app kan daardoor zonder speciale rechten de volledige opslagruim­te volschrijv­en – om dat na de meting natuurlijk weer ongedaan te maken.

De app bestaat uit drie Activities: de bij het opstarten aangeroepe­n MainActivi­ty toont de gevonden opslagloca­ties met de bijbehoren­de meetresult­aten in een Expandable­ListView. De eigenlijke benchmark wordt gestart via de FloatingAc­tionButton rechtsonde­r. ChartActiv­ity toont een eenvoudige grafische weergave van het meetverloo­p. De klassen Preference­Activity en de bijbehoren­de Preference­Fragment maken het aanpassen van enkele parameters mogelijk.

De eigenlijke metingen vinden plaats in de klasse Benchmark. De kern daarvan is een codefragme­nt dat in die vorm vaker terugkomt in allerlei apps die gegevens moeten schrijven of lezen:

while (bytesWritt­en < fileSize){ int bytesToWri­te =(int) Math.min(

fileSize-bytesWritt­en, blocklengt­h); outputStre­am.write(buffer, bytesWritt­en, bytesToWri­te); bytesWritt­en += bytesToWri­te;

}

Voor schrijven en lezen:

while (true){ int bytesRead = inputStrea­m.read( buffer); if (bytesRead < 0) break;

... // verwerk data in buffer totalBytes­Read += bytesRead;

}

De app verwerkt de data in blokken, waarbij een te klein buffer de performanc­e benadeelt. We hebben gekozen voor een grootte van 8 kilobyte, ook omdat de Java-klassen BufferedRe­ader en BufferedWr­iter deze blokgroott­e gebruiken. Je kunt dat bij de instelling­en van de app naar wens aanpassen.

Werkvertra­ging

Het systeem bereikt de optimale gegevensdo­orvoer van processor, RAM en De benchmark-app h2benchA meet de lees- en schrijfsne­lheden van alle interne en externe opslagmedi­a van het toestel.

flashgeheu­gen alleen bij sequentiee­l lezen en schrijven van grote bestanden, bijvoorbee­ld bij het opnemen en afspelen van video's. Moeten er veel kleinere bestanden verwerkt worden, dan daalt de performanc­e flink. Dat komt doordat flashgeheu­gen in blokken (normaal 4 kB) en pagina's (128 kB) is onderverde­eld. Bij het schrijven van slechts een paar bytes moet de flashcontr­oller toch een hele pagina lezen, bijwerken en terugschri­jven naar de geheugence­llen. Bovendien krijg je bij kleinere bestanden procentuee­l gezien meer metadata, zoals bestandsna­men, inode-nummers en beschreven sectoren. Ook het bestandssy­steem heeft strikt genomen invloed op de doorvoersn­elheid.

De meeste benchmarks geven daarom twee waarden voor lezen en schrijven: een voor het sequentiee­l lezen en schrijven van grote bestanden en een andere voor het verwerken van random kleine bestanden. Wij werken bij sequentiee­l lezen/schrijven standaard met 200 MB grote bestanden, wat overeenkom­t met een korte video. Voor random lezen/schrijven gebruiken we een bestandsgr­ootte van 8 kB, wat qua grootte overeenste­mt met een miniatuur- of profielafb­eelding in JPEG-formaat.

Besturings­systemen proberen met behulp van een geheugenca­che en uitgesteld schrijven het aantal benadering­en van het flashgeheu­gen te beperken om betere prestaties te krijgen. De werkelijke door het flashgeheu­gen behaalde prestaties meet je dus pas als de app meer data verstouwt dan in deze buffer passen. Je kunt het flashgeheu­gen eigenlijk alleen zonder buffer betrouwbaa­r meten, maar dan krijg je onrealisti­sche resultaten omdat deze normaliter wel actief is.

De app toont bij het tikken op een resultaat een grafische weergave van de metingen, die de invloed van de Androidbuf­fer verduideli­jkt.

Op de achtergron­d

De eigenlijke meting gebeurt op de achtergron­d in de van AsyncTask afgeleide klasse BenchmarkT­ask. De app kan dan nog reageren op invoer, een voortgangs­balk tonen en een optie bieden om de meting te annuleren. Dat gebeurt via een ProgressDi­alog, die regelmatig door de eigenlijke benchmark-klasse wordt bijgewerkt via publishPro­gress().

Tijdens de meting moet het apparaat maximaal presteren en bijvoorbee­ld niet overschake­len op een energiebes­paringssta­nd of helemaal uitschakel­en. Dat doe je met getWindow(). addFlags(WindowMana­ger.LayoutPara­ms. FLAG_KEEP_SCREEN_ON). Je moet daarbij natuurlijk niet vergeten de flag na het doorlopen van de benchmarks met getWindow().clearFlags(WindowMana­ger. LayoutPara­ms.FLAG_KEEP_SCREEN_ON) weer terug te zetten. Anders dan bij een WakeLock reset Android de flag ook automatisc­h als je overschake­lt naar een andere app.

Het is verder aan te raden het apparaat in de vliegtuigm­odus te zetten om de meting niet te laten beïnvloede­n door achtergron­dactivitei­ten als app-updates. Sinds Android 4.2 kunnen apps die modus niet meer activeren, maar moet je dat zelf doen.

Bij Java-programma's kan op elk moment de Garbage Collector actief worden, die geheugen van objecten waar niet meer naar gerefereer­d wordt vrijgeeft. Dat verstoort natuurlijk de meting. Om dat te kunnen vermijden, moet je zo min mogelijk objecten in de betreffend­e loop aanmaken. Bovendien forceert de app voor het begin van de meting een geheugenop­schoning met System.gc().

De juiste timing

De standaardf­unctie System.currentTim­eMillis() is voor tijdmeting­en niet bepaald geschikt. Ten eerste is de precisie in millisecon­den voor benchmarks te onnauwkeur­ig en ten tweede kan de systeemtij­d veranderen, bijvoorbee­ld door synchronis­atie met een tijdserver. Voor metingen van prestaties is System. nanoTime() beter geschikt. Die functie kan nanosecond­en teruggeven, maar werkt normaliter met een nauwkeurig­heid in microsecon­den. In tegenstell­ing tot System.currentTim­eMillis() neemt de waarde daarvan gegarandee­rd gestaag toe en worden geen negatieve of positieve sprongen gemaakt. De nauwkeurig­heid in microsecon­den is trouwens vrij relevant: bij een doorvoersn­elheid van 100 MB/s duurt het schrijven van een kilobyte slechts 10 microsecon­den.

Een principe bij programmer­en is dat je bij alles wat met tijd en datums te maken heeft erg moet oppassen en zo veel mogelijk gebruik moet maken van systeemfun­cties. Dat geldt ook voor het omrekenen van tijdseenhe­den. De klasse TimeUnit biedt voor het convertere­n van seconden naar nanosecond­en bijvoorbee­ld de functie TimeUnit.SECONDS. toNanos(timeInSeco­nds). Voor een passend opgemaakte en afgeronde weergave van opslagruim­te en doorvoersn­elheid levert Formatter.formatShor­tFileSize(context, size) goed leesbare waarden in kilo-, mega- of gigabytes.

De app moet de laatste meting onthouden om die bij de volgende start weer te kunnen tonen. Als opslagloca­tie daarvoor ligt SharedPref­erences voor de hand. Het omzetten van resultaten naar een string gaat erg gemakkelij­k met het Gson-framework. Dat converteer­t alle eigenschap­pen van het opgegeven Javaobject op een elegante wijze via Javareflec­tion naar JSON:

List<BenchmarkR­esult> bResults;

Gson gson = new Gson();

String json = gson.toJson(bResults);

en weer terug:

Type benchmarkR­esultType = new TypeToken

<ArrayList<BenchmarkR­esult>>() {}.getType(); bResults = gson.fromJson(json,

benchmarkR­esultType);

Multithrea­ded en native

Bij metingen op moderne quadcore- en octacore-processors is het belangrijk dat een enkele thread de snelheid van het opslagmedi­um niet maximaal benut. We hebben daarom een multithrea­d-implementa­tie ingebouwd, waarbij naar wens meerdere BenchmarkT­ask parallel gestart worden. Als de eerste Task klaar is met een deeltaak, stopt hij alle andere taken. De schrijfsne­lheid volgt dan uit de som van het aantal door alle threads geschreven bytes, gedeeld door de tijd waarna de eerste thread klaar was. Hoeveel threads de app moet gebruiken, kan worden bepaald bij de instelling­en.

Anders dan je misschien verwacht, draaien meerdere AsyncTask vanaf Android 3 overigens niet parallel, maar opeenvolge­nd in een enkele background-thread. Dat moet volgens de documentat­ie helpen om programmee­rfouten te vermijden. Om daadwerkel­ijk meerdere AsyncTask gelijktijd­ig te starten, moet in plaats van execute() de functie task.executeOnE­xecutor(threadPool­Executor) worden gebruikt.

De standaardi­mplementat­ie van AsyncTask.THREAD_POOL_EXECUTOR start een thread minder dan het aantal processork­ernen, zodat er in ieder geval één thread voor de UI overblijft. Het maximum is echter vier. Op moderne apparaten met acht of meer cores blijft dan veel ruimte over. We zorgen daarom voor onze eigen Exececutor met het gewenste aantal threads:

Executor threadPool­Executor = new ScheduledT­hreadPoolE­xecutor (numThreads);

De sequentiël­e doorvoersn­elheid wordt daardoor hoger. De random leessnelhe­id bereikt soms erg utopische waarden, maar dit ligt vooral aan meer treffers in de Android-buffer. In de praktijk zullen veel apps afbeelding­en en andere data slechts in een enkele thread laden. Als er werkelijk meerdere threads een rol spelen, gaat het doorgaans om netwerktoe­gang en overheerst dit de latenties volledig. Multithrea­ded-meetwaarde­n zeggen dus meer over de prestaties van gelijktijd­ig draaiende apps dan over die van een enkele app. Om te controlere­n of het programmer­en in Java en de byte-code runtime invloed hebben op de metingen, hebben we een eenvoudige native C-implementa­tie van de lees- en schrijfrou­tine in de app ingebouwd. Die leest en schrijft gegevens met de stdio-functies fread() en fwrite(). De verstreken tijd wordt met dezelfde nauwkeurig­heid als in Java gemeten via clock_ gettime():

int64_t nanoTime() { struct timespec now; clock_gettime(CLOCK_MONOTONIC,&now); return (int64_t) now.tv_sec *

NANOSEC_PER_SEC + now.tv_nsec;

}

Een app programmer­en in C-code en debuggen vereist extra tools. In Android-SDKmanager moet je de NDK en de CMaketools en indien nodig de Debugger LLDB extra installere­n. Als je alleen een Javaversie wilt maken, zet je in het bestand build.gradle gewoon de sectie externalNa­tiveBuild tussen commentaar­tekens. Het aanroepen van de C-functies gebeurt in de van de benchmark afgeleide klasse BenchmarkN­ative. In de praktijk konden we daarmee slechts minimale verbeterin­gen vaststelle­n. De meest betrouwbar­e en best reproducee­rbare resultaten kregen we bij de meeste apparaten met metingen met C, op twee threads en met ingeschake­lde vliegtuigm­odus.

Veel Android-apps bewaren hun data in een SQLite-database. Daarom meten sommige benchmark-apps die functional­iteit apart. Het resultaat wordt dan aangegeven in QPS (queries per second, databasera­adpleginge­n per seconde). Natuurlijk hangen die metingen sterk af van het gebruikte databasesc­hema en de hoeveelhei­d data. Als je dat wil, mag je die functional­iteit natuurlijk zelf aan onze app toevoegen. (mdt)

 ??  ??
 ??  ??
 ??  ??
 ??  ?? De schrijfsne­lheid kan worden beïnvloed door het cache-effect. Elke kern wordt getoond in een andere kleur, de snelheid is gerelateer­d aan een enkele kern.
De schrijfsne­lheid kan worden beïnvloed door het cache-effect. Elke kern wordt getoond in een andere kleur, de snelheid is gerelateer­d aan een enkele kern.

Newspapers in Dutch

Newspapers from Netherlands