Under de senaste 5 åren har det mobila utrymmet sett en dramatisk förändring när det gäller prestanda för smarttelefoner och surfplattor. Branschen har sett en övergång från enkelkärniga till dual-core till fyrkärniga processorer till dagens heterogena 6-10 kärnor. Detta var en naturlig utveckling som liknar vad PC-utrymmet har sett under det senaste decenniet, men bara i en mycket mer accelererad takt. Även om ILP (instruktionsnivå parallellism) verkligen också har gått upp med varje ny processorarkitektur, med konstruktioner som ARMs Cortex A15 eller Apples Cyclone-processorkärnor som ger betydande entrådiga prestandahöjningar, är det ökningen av CPU-kärnor som har lett till mest enkelt sätt att öka den totala datorkraften.
Denna ökning av CPU-kärnor väckte många diskussioner om hur mycket meningsfulla sådana konstruktioner är i verklig användning. Jag kan fortfarande minnas när de första quad-cores introducerades att användare argumenterade för fördelen med 4 kärnor i mobila arbetsbelastningar och att dessa ökningar bara gjordes för marknadsföringens skull. Jag kan dra paralleller mellan de diskussionerna för några år sedan och dagens argument om 6 till 10-kärniga SoCs baserade på big.LITTLE.
Även om det har gjorts några försök att analysera debatten om kärnvärden, var jag aldrig riktigt nöjd med metodiken och resultaten av dessa bitar. De befintliga verktygen för att övervaka processorer gör det helt enkelt inte när det gäller att noggrant analysera de finkorniga händelser som dikterar hanteringen av multi-core och heterogena processorer. För att äntligen försöka få en ordentlig analys av situationen, för den här artikeln, har jag försökt att närma mig den här frågan från grunden på ett ordnat och korrekt sätt, och inte förlita mig på några tredjepartsverktyg.
Metod förklaras
Jag bör börja med en ansvarsfriskrivning att eftersom verktygen som krävs för en sådan analys i hög grad är beroende av Linux-kärnan, är denna analys begränsad till beteendet hos Android-enheter och inte nödvändigtvis representerar beteendet hos enheter på andra operativsystem, i synnerhet Apples iOS. Som sådan bör alla jämförelser mellan sådana SoCs begränsas till rent teoretiska scenarier där en given CPU-konfiguration skulle köra Android.
Grunderna: Frekvens
Traditionellt när de vill logga vad processorn gör, skulle de flesta användare tänka på att titta på frekvensen som den körs på för närvarande. Vanligtvis ger detta en ungefärlig idé för att se om det är någon belastning på CPU:n och när den sätter i hög växel. Problemet med detta är hur man fångar frekvensen: utläsningsprovet kommer alltid att vara ett enda diskret värde vid en given tidpunkt. För att exakt kunna få en bra representation av frekvensen skulle man behöva ha en samplingshastighet på minst dubbelt så snabb som CPU:ns DVFS-mekanism. Mobila SoCs kan nu byta frekvens med intervaller på ner till 10-20 ms, och har till och med oförutsägbara finkorniga switchar som kan orsakas av QoS-förfrågningar (Quality of Service).
Sampling vid allt under hälften av DVFS-växlingshastigheterna kan leda till felaktiga data. Detta kan till exempel hända i periodiska korta höga skurar. Ta en given samplingshastighet på 1s: Föreställ dig att vi läser ut frekvensen vid 0,1s och 1,1s i tiden. Frekvensen vid båda dessa avläsningar skulle vara antingen vid en hög eller låg frekvens. Vad som händer däremellan fångas inte, och på grund av att växlingshastigheten är så hög kan vi gå miste om 90 %+ av CPU:ns verkliga frekvensbeteende.
Istället för att gå vägen att logga den diskreta frekvensen i en mycket hög hastighet, kan vi göra något mycket mer exakt: Logga den kumulativa uppehållstiden för varje frekvens på varje avläsning. Eftersom Android-enheter körs på Linux-kärnan har vi enkel tillgång till denna statistik från CPUFreq-ramverket. Tid-i-tillstånd-statistiken är alltid korrekt eftersom den inkrementeras av kärndrivrutinen asynkront vid varje frekvensändring. Så genom att beräkna deltan mellan varje avläsning får vi en korrekt frekvensfördelning inom perioden mellan våra avläsningar.
Det vi slutar med är en staplad tidsfördelningsgraf som denna:
Y-axeln i grafen är en staplad procentandel av varje CPU:s frekvenstillstånd. X-axeln representerar fördelningen i tid, alltid beroende på scenariots längd. För läsbarhetens skull i den här artikeln valde jag en effektiv provperiod på ~200 ms (på grund av overhead på skript- och tidshållningsmekanismer är detta bara ett grovt mål) som borde ge tillräckligt med upplösning för en bra grafisk representation av CPU:ns frekvensbeteende.
Med detta har vi nu den första delen av våra verktyg för att noggrant analysera SoC:s beteende: frekvens.
Detaljerna: Maktstater
Även om frekvens är en av de första mätvärdena som kommer att tänka på när man försöker övervaka en CPU:s beteende, finns det ett helt annat dolt lager som sällan exponeras: CPU:s viloläge. För läsare som letar efter en mer djupgående förklaring av hur CPUIdle fungerar, jag har berört det och energihantering av moderna SoCs i allmänhet fungerar i vår djupdykning av Exynos 7420. Dessa förklaringar är giltiga för i princip alla dagens SoCs baserade på ARM CPU IP, så det gäller SoCs från MediaTek och ARM-baserade Qualcomm chipset också.
För att hålla saker kort, är en förenklad förklaring att utöver frekvens kan moderna CPU: er spara ström genom att gå in i vilolägen som antingen stänger av klockan eller strömmen till de individuella CPU-kärnorna. Vid det här laget pratar vi om kopplingstider på ~500µs till +5ms. Det är sällsynt att hitta SoC-leverantörer som exponerar API:er för liveavläsning av processorernas effekttillstånd, så detta är en statistik som man inte ens realistiskt kunde logga via diskreta avläsningar. Lyckligtvis bestäms fortfarande CPU:s vilolägen av kärnan, som återigen, på samma sätt som CPUFreq-ramverket, ger oss samlad tid-i-tillstånd-statistik för varje strömtillstånd på varje CPU.
Detta är en viktig skillnad att göra i dagens ARM CPU-kärnor eftersom (förutom Qualcomms Krait-arkitektur) alla CPU:er inom ett kluster körs på samma synkrona frekvensplan. Så även om en CPU kan rapporteras köra på en hög frekvens, berättar detta inte riktigt vad den gör och kan lika gärna vara helt power-gated när den sitter inaktiv.
Genom att använda samma metod som för frekvensloggning, slutar vi med en inaktiv effekttillstånd staplad tidsfördelningsgraf för alla kärnor inom ett kluster. Jag har märkt tillstånden som “Clock-gated”, “Power-gated” och “Active” som i tekniska termer representerar WFI (Wait-For-Interrupt) C1, power-collapse C2 vilolägen, såväl som tidsskillnad till väggklockan som representerar den “aktiva” tiden då CPU:n inte är i något energisparläge.
The Intricacies: Scheduler Run-Queue Depths
Ett mått som jag aldrig tror har diskuterats i samband med mobil är djupet på CPU:ns körkö. I Linux-kärnschemaläggaren är körkön en lista över processer (Den faktiska implementeringen involverar ett röd-svart träd) som för närvarande finns på den CPU:n. Detta är kärnan i den förebyggande schemaläggningskaraktären hos CFS (Completely Fair Scheduler) processplanerare i Linux-kärnan. När flera processer körs på samma CPU är schemaläggaren ansvarig för att fördela bearbetningstiden rättvist mellan varje tråd baserat på tidssegment och processprioritet.
Kärnan och Android kan typ avslöja information om körkön genom en av kärnans sysfs-noder. På Android kan detta aktiveras genom alternativet “Visa CPU-användning” i utvecklaralternativen. Detta ger dig tre numeriska parametrar samt en lista över de aktiva avläsningsprocesserna. Det numeriska värdet är det så kallade “belastningsmedelvärdet” för schemaläggaren. Den representerar hela systemets belastning – och den kan användas för att avläsa hur många trådar i ett system som används. De tre värdena representerar medelvärden för olika tidsfönster: 1 minut, 5 minuter och 15 minuter. Det faktiska värdet är en procentsats – så till exempel representerar 2,85 285 %. Hur detta är tänkt att tolkas är att om vi skulle konsolidera alla processer i så lite processorer som möjligt så har vi teoretiskt två processorer vars belastning är 100 % (summa upp till 200 %) samt en tredje upp till 85 % belastning.
Nu är det här väldigt konstigt, hur kan telefonen fullt ut använda nästan 3 kärnor medan jag inte gjorde något mer än att gå på tomgång på skärmen med CPU-statistiken på? Tyvärr lider kärnplaneraren av samma samplingshastighetsproblem som förklaras i vår frekvensloggningsmetod. Sanningen är att belastningsgenomsnittsstatistiken bara är en ögonblicksbild av schemaläggarens körköer som uppdateras endast i 5-sekundersintervaller och det representerade värdet är en beräknad belastning baserat på tiden mellan ögonblicksbilderna. Tyvärr är denna statistik extremt missvisande och representerar inte på något sätt den faktiska situationen för körningsköerna. På Qualcomm-enheter är denna statistik ännu mer missvisande eftersom den kan visa belastningsmedelvärden på upp till 12 i vilolägen. I slutändan betyder detta att det i princip är omöjligt att få korrekt RQ-djupstatistik på lagerenheter.
Lyckligtvis snubblade jag över samma problem för några år sedan och var medveten om en lappa som jag tidigare använde tidigare och som skrevs av Nvidia som introducerar detaljerad rq-djupsstatistik. Detta spårar körköerna exakt och atomärt varje gång en process går in i eller lämnar en körkö, vilket gör det möjligt för den att exponera ett glidande fönstergenomsnitt av körködjupet för varje CPU under en period av 134ms.
Nu har vi ett live pollable medelvärde för schemaläggarens körköer och vi kan helt logga den exakta mängden trådar som körs på systemet.
Återigen representerar X-axeln i graferna tiden i millisekunder. Den här gången representerar Y-axeln rq-djupet för varje CPU. Jag inkluderade också summan av rq-djupen för alla CPU:er i ett kluster samt summan av båda klustren för systemets totalsumma i en separat graf.
Värdena kan tolkas på samma sätt som belastningsmedelvärdena, men denna gång har vi ett separat värde för varje CPU. Ett kör-ködjup på 1 betyder att CPU:n laddas 100 % av tiden, 0,2 betyder att CPU:n är laddad med endast 20 %. Nu kommer det intressanta måttet för värden över 1: För allt över ett rq-djup på 1 betyder det att CPU:n föregriper mellan flera processer som kumulativt överskrider processorkraften för den CPU:n. Till exempel i grafen ovan har vi några toppar per CPU på ~2. Det betyder att CPU:n har minst två trådar på den CPU:n och att de delar 50 % av beräkningstiden för den CPU:n var och en, dvs de körs på halvfart.
Data och mål
På de följande sidorna kommer vi att ta en titt på cirka 20 olika användningsfall i den verkliga världen där vi övervakar CPU-frekvens, strömtillstånd och schemaläggarens körköer. Vad vi specifikt letar efter är djupspetsarna för körningskön för varje scenario för att se hur många trådar som skapas under de olika scenarierna.
Testerna körs på Samsungs Galaxy S6 med Exynos 7420 (4x Cortex A57 @ 2,1GHz + 4x Cortex A53 @ 1,5GHz) som borde fungera bra som en representation av liknande flaggskeppsenheter som såldes 2015 och framåt.
Beroende på användningsfallen kommer vi att se hur många av kärnorna på dagens många kärniga big.LITTLE-system som används. Tillsammans med att ha energihanteringsdata på båda klustren, kommer vi också att se hur mycket meningsfullt heterogen bearbetning är och hur mycket nytta man kan dra av det.