Måle kodens ydeevne og oprette grundlinjer

Fuldført

Måling af kodeydelse og etablering af baselines er et kritisk trin i optimeringsprocessen. Det giver et referencepunkt til at evaluere resultaterne af eventuelle ændringer, du foretager.

Etablere en ydeevnebaseline

En oprindelig ydeevne er et sæt målinger, der registrerer, hvordan koden klarer sig, før du foretager ændringer. Det er dit referencepunkt. Uden en baseline til sammenligning ved du ikke, om dine opdateringer forbedrer kodens ydeevne eller gør tingene værre.

Her er nogle vigtige aspekter, der skal måles i en baseline:

  • Udførelsestid (ventetid): Hvor lang tid tager det at udføre en bestemt operation eller proces? Denne værdi kan måles for en lille funktion (det tager f.eks. 5 millisekunder at sortere 10.000 elementer) eller et komplet scenarie (tjenesten svarer f.eks. på en anmodning på 250 millisekunder).

  • Dataoverførselshastighed: Hvor mange operationer kan udføres på et givet tidspunkt? Denne værdi er relevant for serverapplikationer (anmodninger pr. sekund), batchbehandling (poster behandlet pr. minut) osv.

  • Ressourceforbrug: Hukommelsesforbrug (arbejdssæt, allokeringer, skraldindsamlingsfrekvens), CPU-forbrug (fastlægger den en kerne på 100%? hvor længe?), disk-I/O, netværks-I/O osv., afhængigt af hvad koden gør.

  • Skalerbarhed egenskaber: Hvordan ændres ovenstående målinger, efterhånden som inputstørrelsen vokser, eller når belastningen (samtidige brugere) øges? Du kan måle, at en funktion tager 0,1 millisekunder for 100 elementer og 10 millisekunder for 10.000 elementer. Det angiver, hvordan den skalerer (i dette tilfælde nogenlunde lineær med antallet af elementer). Denne form for baseline hjælper dig med at forstå skalerbarhed og sætte mål. For eksempel vil du måske have 100k varer til at sortere på under 200 millisekunder, hvilket kan betyde, at du har brug for en mere effektiv algoritme eller parallel sortering.

Når du etablerer et referencescenarie:

  • Gør miljøet så ensartet som muligt. Kør test på den samme maskine under lignende forhold (luk baggrundsprogrammer, brug release build osv.). Ydeevnen kan variere fra løb til løb; Minimering af støj giver mere pålidelige data.

  • Brug udgivelsesbuilds og realistiske indstillinger. Fejlfindingsbuilds kører ofte langsommere på grund af ekstra kontroller og manglende optimeringer. Hvis du måler algoritmehastighed, skal du kompilere i udløsertilstand med optimeringer slået til. Hvis din kode er flertrådet eller asynkron, skal du på samme måde teste den i et scenarie, der ligner produktion (f.eks. det samme antal tråde eller den samme størrelse på datasættet).

  • Automatiser eller script målingen. Du bør køre din kode flere gange. Det hjælper at have en simpel benchmark-sele eller bruge værktøjer, der automatiserer den. For en lille funktion kan du f.eks. skrive en løkke, der kalder den, måle tid og beregne et gennemsnit.

  • Optag tallene. Hold styr på de grundlæggende målinger (tid, hukommelse osv.). Hvis du har en enhedstest eller et logoutput, der udskriver oplysninger om ydeevne, skal du gemme det. I ikke-trivielle tilfælde er det nyttigt at bruge et regneark eller en simpel tabel til at sammenligne "før vs efter".

Baseline-måling ved hjælp af stopur

C# tilbyder System.Diagnostics.Stopwatch klassen, som er nyttig til at måle udførelsestiden for kodeblokke.

Antag f.eks., at du har følgende ProcessData() metode, som du mistænker for at være langsom:

var sw = Stopwatch.StartNew();
ProcessData();
sw.Stop();
Console.WriteLine($"ProcessData() completed in {sw.ElapsedMilliseconds} milliseconds");

Forestil dig, at denne kode konsekvent udskriver en værdi på ca. 120 millisekunder. Denne gennemsnitsværdi er din baseline for ProcessData() under de testbetingelser, du brugte.

Baseline for hukommelsesforbrug

Metoden GC.GetTotalMemory er et .NET metodekald, der henter den aktuelle mængde hukommelse, der er allokeret af garbage collector (GC). Denne GetTotalMemory metode registrerer den aktuelle hukommelsesallokeringstilstand, når den kaldes.

Du kan registrere hukommelsesforbrug ved at kontrollere GC.GetTotalMemory(false) før og efter en handling.

Antag f.eks., at du har følgende kode:

long memoryBefore = GC.GetTotalMemory(forceFullCollection: true);
ProcessData();
long memoryAfter = GC.GetTotalMemory(forceFullCollection: true);
Console.WriteLine($"Memory used by ProcessData(): {memoryAfter - memoryBefore} bytes");

Hvis denne kode udskriver "Hukommelse brugt af ProcessData(): 50.000 byte", er det hvor meget hukommelse operationen allokerede (netto). Denne værdi kan være en del af din baseline, især hvis du bekymrer dig om hukommelse eller affaldsindsamling.

Kontekstafhængige grundlinjer

Udgangspunktet er ofte ikke et enkelt tal. Du kan have en baseline på en bestemt skala og derefter teste i en større skala. For eksempel:

  • Sortering af 10k elementer tager 50 millisekunder (baseline ved 10k).
  • Sortering af 100k varer tager 800 millisekunder.

Dette resultat indikerer, at ydeevnen forringes, efterhånden som inputtet vokser, hvilket er forventeligt. Det tyder også på, at algoritmen kan være dårligere end O(n log n), fordi en forøgelse af elementerne med en faktor 10 ser ud til at øge tiden med en faktor 16. Denne type baseline hjælper dig med at forstå skalerbarhed og sætte mål.

Værktøjer og teknikker til måling af ydeevne

Forståelse af ydeevne kræver både måling og analyse af algoritmiske egenskaber. Før du dykker ned i måleværktøjer, er det vigtigt at forstå nøglebegreber:

Forståelse af algoritmisk kompleksitet

Tidskompleksitet beskriver, hvordan en algoritmes runtime vokser, efterhånden som inputstørrelsen øges, typisk udtrykt ved hjælp af Big O-notation:

  • O(1) – Konstant tid: Ydeevnen ændres ikke med inputstørrelsen (f.eks. opslag i ordbogen).
  • O(n) – Lineær tid: Ydeevnen vokser proportionalt med input (f.eks. gentagelse gennem en liste én gang).
  • O(n²) - Kvadratisk tid: Ydeevnen vokser med kvadratet af inputstørrelsen (f.eks. indlejrede løkker).
  • O(log n) – Logaritmisk tid: Ydeevnen vokser langsomt, når inputtet øges (f.eks. binær søgning).

Rumkompleksitet måler, hvordan hukommelsesforbruget vokser med inputstørrelsen ved hjælp af den samme Big O-notation. Forståelse af disse begreber hjælper med at identificere, hvorfor visse kodemønstre bliver flaskehalse i stor skala.

Genkendelse af præstationsmønstre og anti-mønstre

Almindelige anti-mønstre for ydeevne omfatter:

  • N+1-forespørgselsproblemer: Oprettelse af en forespørgsel for at få en liste og derefter N flere forespørgsler efter relaterede data.
  • Ineffektive datastrukturer: Brug af lister til hyppige opslag i stedet for ordbøger eller hash-sæt.
  • For tidlig strengsammenkædning: Opbygning af store strenge ved hjælp af += i stedet for StringBuilder.
  • Synkrone operationer: Blokering af tråde med synkron I/O, når asynkrone operationer ville være bedre.

Caching-strategier kan forbedre ydeevnen dramatisk ved at gemme ofte anvendte data i hukommelsen og undgå dyre genberegninger eller I/O-operationer.

Fra enkle til sofistikerede, her er værktøjer og metoder til at måle ydeevne:

Manuel timing med stopur

Manuel instrumentering af kode med Stopwatch eller endda DateTime.UtcNow forskelle kan give hurtig indsigt. Denne tilgang er ad hoc, men tilgængelig og ofte tilstrækkelig til en første undersøgelse. Du kan drysse timing-logfiler omkring kodesektioner (f.eks. logføre, hvor lang tid en databaseforespørgsel tog, hvor lang tid filparsing tog osv.).

Logning og tællere

Logning er ad hoc, men tilgængelig og ofte tilstrækkelig til en første undersøgelse. Du kan drysse timing-logfiler omkring kodesektioner (f.eks. logføre, hvor lang tid en databaseforespørgsel tog, hvor lang tid filparsing tog osv.).

Hændelseslogning og ydeevnemålinger

Tilføjelse af strategisk logning kan afsløre præstationsmønstre og hjælpe med at identificere flaskehalse:

  • Antal handlinger: Logfør, hvor mange elementer der blev behandlet, databaseforespørgsler blev udført, eller der opstod cachehits/-fejl.
  • Tidsopdelinger: Mål forskellige faser af handlinger (f.eks. "Databaseforespørgsel: 50 millisekunder, Behandling: 20 millisekunder, Serialisering: 10 millisekunder").
  • Ressourceudnyttelse: Spor hukommelsesallokeringer, brug af trådpuljer eller målepunkter for forbindelsespuljer.
  • Målepunkter for ydeevne: Overvåg ventetid (svartid), gennemløb (handlinger pr. sekund) og skalerbarhedsegenskaber.

Hvis din log f.eks. viser "Hentede 1000 poster fra databasen", når du forventede at se 100 poster, kan uoverensstemmelsen indikere et N+1-forespørgselsproblem eller ineffektiv forespørgselslogik. På samme måde kan logning af cache-hitrater hjælpe med at evaluere cachingstrategiens effektivitet.

Indbyggede profiler (Visual Studio Diagnostics)

Visual Studio (Enterprise-udgaven og til en vis grad Community) har profileringsværktøjer (CPU-brug, hukommelsesforbrug, Performance Profiler). Du kan køre din applikation under profileren, og den viser dig for eksempel en opdeling af CPU-tid efter funktion eller en liste over objekter på heapen, og hvem der har tildelt dem.

  • CPU-profileren opretter typisk et kaldetræ, hvor du kan se, hvilke metoder der brugte mest CPU-tid.
  • Hukommelsesprofilen kan tage snapshots for at vise, hvordan hukommelsesforbruget vokser, og hvilke typer objekter der optager plads.

Brug af en profil er ofte så simpelt som at klikke på "Start diagnosticering" og vælge typen af profil.

.NET CLI-værktøjer

Til .NET Core og .NET 5+ leverer Microsoft kommandolinjeværktøjer som dotnet-counters, dotnet-trace, dotnet-dump, dotnet-gcdump.

  • dotnet-counters kan vise ydeevnemålinger i realtid for en kørende app (GC-samlinger, undtagelser, brug af trådpuljer osv.).
  • dotnet-trace kan indsamle et spor af en apps udførelse, som man kan analysere for at se, hvilke metoder der kørte.

Disse værktøjer er mere avancerede, men uvurderlige til dybe dyk eller produktionsprofilering, hvor du ikke kan vedhæfte en GUI-profiler.

Mikrobenchmarking-biblioteker

Hvis du vil sammenligne to implementeringer af en funktion (f.eks. din originale vs. en foreslået optimeret version) på en streng måde, er BenchmarkDotNet et populært bibliotek. Den kører funktionerne mange gange, varmer just-in-time-compileren op, måler præcist og giver statistik (som middelværdi, standardafvigelse). Disse data bruges til mikrobenchmarks (små isolerede kodestier) med høj præcision.

Test af ydeevne/belastning

I større scenarier (webprogrammer, tjenester) kan du skrive en belastningstest eller bruge et værktøj (f.eks. JMeter, k6 eller Visual Studio belastningstest) til at simulere mange anmodninger eller store input. Denne tilgang kan afsløre gennemløb og stabilitet under stress og hjælpe med at identificere flaskehalse, der kun opstår i stor skala.

Systemovervågning

Overvåg den overordnede systemadfærd for at identificere ressourceflaskehalse og skalerbarhedsproblemer:

  • CPU-brugsmønstre: Højt CPU-forbrug kan indikere algoritmisk ineffektivitet eller beregningsmæssige flaskehalse.
  • Hukommelsesforbrug: Voksende hukommelsesforbrug kan tyde på hukommelseslækager, ineffektive datastrukturer eller overdreven objektallokering.
  • I/O-målinger: Høj disk eller netværks-I/O kan indikere ineffektive dataadgangsmønstre eller dårlig cachelagring.
  • Affaldsindsamling: Hyppig affaldsindsamling kan påvirke ydeevnen, især i applikationer med høj kapacitet.

Værktøjer som Task Manager dotnet-counterseller PerfMon på Windows kan give indsigt på systemniveau. Forståelse af disse målepunkter hjælper med at korrelere ydeevne på kodeniveau med systemressourceudnyttelse.

Overvåg forbedringer og undgå regressioner

Når du har etableret en baseline, kan du begynde at implementere kodeforbedringer. Kodeforbedringer bør foretages i en cyklus af kodeopdateringer efterfulgt af måling for at se, om ændringen havde den ønskede effekt.

Når du foretager en ændring, skal du altid måle igen ved hjælp af den samme metode som din oprindelige plan. Sammenlign de nye målinger med baseline.

Her er nogle anbefalede retningslinjer for denne proces:

  • Skift én ting ad gangen (hvis det er muligt): Så du ved, hvad der forårsagede enhver forbedring eller regression.

  • Kør den samme test/måling som baseline: Brug den samme procedure, og sammenlign målepunkter direkte.

  • Hvis der opnås forbedring: Fantastisk, overvej, om det opfylder målet. Hvis du havde brug for en 2x hastighed, og du fik 1,5x, kan du gentage yderligere.

  • Hvis ingen forbedring eller dårligere ydeevne: Undersøg hvorfor. Det kan være, at din ændring ikke adresserede den virkelige flaskehals eller byttede en omkostning for en anden.

  • Tjek for bivirkninger: Bivirkninger kan omfatte uventet korrekthed eller problemer med ydeevnen. For eksempel kan du optimere CPU-ydeevnen, men være vidne til en stor stigning i hukommelsesforbruget. Er den bivirkning acceptabel?

  • Automatiser regressionsregistrering: Skrivning af test af ydeevneenheder eller benchmarks, der kører før og efter ændringer, er en almindelig teknik til regressionsregistrering. Selvom ikke alle teams skriver præstationstests, er det ikke en dårlig idé at have en lille pakke af præstationstests (især for kritiske veje), der sikrer, at nye ændringer ikke bremser tingene drastisk.

Brug GitHub Copilot til at hjælpe med målinger

GitHub Copilot kan hjælpe dig med at konfigurere teknikker til måling af ydeevne:

  • Spørg efter timing-eksempler: "Hvordan kan jeg måle udførelsestiden for en C#-metode?" GitHub Copilot kan foreslå Stopwatch med eksempelkode.
  • Få vejledning til profilering: "Hvilke værktøjer kan jeg bruge til at profilere et . GitHub Copilot kan angive profileringsværktøjer og deres use cases.
  • Generer benchmarkkode: GitHub Copilot kan hjælpe med at oprette BenchmarkDotNet-klasser eller andre måleseler.

GitHub Copilot fungerer som en hurtig reference til korrekt implementering af måleteknikker.

Resumé

Måling af kodeydelse og etablering af baselines er et kritisk trin i optimeringsprocessen. Ved systematisk at registrere udførelsestid, ressourceforbrug og skalerbarhedsegenskaber opretter du et referencepunkt til at evaluere virkningen af dine ændringer. Brug af forskellige måleteknikker – fra simpel timing til Stopwatch avancerede profileringsværktøjer – giver dig mulighed for at identificere flaskehalse nøjagtigt. Sørg altid for, at forbedringer af ydeevnen ikke kompromitterer korrektheden eller introducerer nye problemer. Med en disciplineret tilgang til måling og iteration kan du effektivt forbedre din kodes ydeevne og samtidig bevare dens integritet.