Introduktion till programmering, del 13- Optimering av kod

Ooooooooch så var det onsdag! 😀

Introduktion till programmering

Förra veckan tittade vi på X-mas lights by m.nu och hur man skapar ett nytt läge för denna, men denna veckan går vi vidare på ett nytt ämne: att optimera kod!
Varför skulle man vilja göra det, måntro? Jo, än idag är beräkningskapacitet en flaskhals hos många datorer. En typisk, stationär dator är som regel så pass kraftfull att man inte märker om ett program är dåligt optimerat, men på långsammare datorer (som till exempel Raspberry Pi) blir det betydligt mer uppenbart då sysslor kan ta betydligt längre än väntat att exekvera.
Så som programmerare är det bra att alltid försöka skriva effektiv kod. Detta då det alltid är lättare att göra rätt från början, än att försöka rätta misstagen i efterhand. Ta inga genvägar (inte ens till det perfekta ljudet)!
För att illustrera hur ineffektiv kod kan vara dels svår att upptäcka, men också slöa ner även långsammare datorer, har jag här några anekdoter från spelprogrammering:

Spelprogrammering i Java

När jag läste Datavetenskap vid Linköpings Tekniska Högskola gick jag bland annat en kurs i Java, där vi som avslutningsprojekt gjorde ett plattformsspel.
När man programmerar ett spel har man, förutom med vissa undantag, alltid en ”main”-loop. Denna main-loop körs oftast åtskilliga gånger per sekund och har i uppgift att avläsa om någon inmatning kommit från användaren, kolla om t.ex. kollisioner hänt i spelvärlden och sedan rita ut bilden baserat på detta.
Därmed skapade vi en main-loop, och spelvärlden byggde vi upp av block som fylldes med bilder som var 64×64 pixlar stora. Spelfönstrets storlek var 800×600, vilket innebar att 12,5×9,4 sådana rutor alltid skulle ritas ut på skärmen.
Nivåerna var klart bredare än spelets fönster, kanske 2500×600, så när man gick åt höger följde nivån med. Vi märkte snart att spelet ”laggade” ganska mycket, det vill säga inte gick så fort som man kunde förvänta sig.
Vi tänkte inte så mycket på det, utan slutförde kursen med toppbetyg. 😉
Först efteråt, när vi inspekterade spelets kod, insåg vi varför spelet laggade så mycket. Vi hade helt glömt optimera utritningen av nivåerna, vilket innebar att varje steg i loopen ritade spelet ut hela nivån (även det som inte syntes i fönstret). Genom att lägga in ett villkor i utritningsdelen av loopen som i princip sa ”rita bara ut om rutan visas i fönstret” minskade mängden arbete för funktionen med 2/3 och spelet flöt på avsevärt mycket bättre!

Age of Empires 2

Den som växt upp med datorer och datorspel, som jag, kan knappast ha missat strategispelet Age of Empires 2. Omåttligt populärt på sin tid, och fortfarande är det många som spelar det i nätverk. Men i en artikel berättade en av utvecklarna bakom spelet om ett allvarligt prestandaproblem som gjorde spelet nästan obrukbart. I denna artikel går ehan igenom olika steg som togs för att optimera spelet så att det skulle fungera även på sämre datorer.

Man upptäckte vid testning att spelet laggade extremt mycket, men kunde helt enkelt inte förstå varför. Med hjälp av felsökningsverktyg lyckades man till slut komma fram till att det var ”pathfinding”-beräkningarna som tog upp en väldigt stor del av beräkningarna varje varv i main-loopen. Denna funktion beräknade, för varje varv, den kortaste vägen för varje enhet för att ta sig till sitt slutmål. Stod enheten stilla kontrollerade funktionen ändå huruvida den var på väg någonstans, men det är såklart nödvändigt för att försäkra sig om att enheten ska eller inte ska röra sig. Det första problemet som uppdagades var att många enheter inte lyckades hitta en väg till sitt mål, och därför försökte igen väldigt många gånger. Att lösa detta problemet minskade antalet pathing-beräkningar med ~30%, men det räckte inte för att möta systemkraven.

Andra prestandaproblem var relaterade till enheter. Alla enheter i Age of Empires 2, var nedärvda från samma objekt, låt oss kalla detta objekt ”Unit”. Vi gick kort igenom arv i ett av de första inläggen, men kort sagt kan man säga att alla enheter har samma grundläggande funktionalitet. Och det är ju bra. Fast det är det egentligen inte.
Hur gör man om man har en flygande enhet? Lätt, ärv Unit och skapa en ”FlyingUnit” med den extra funktionaliteten. En enhet som rör sig på vatten? ”NavalUnit”, inga problem! Men hur gör man senare, när man inser att man vill ha en enhet som ska kunna röra sig både i luften och på vatten? Det går bara ärva ett objekt.
Då får man tillämpa ett anti-designmönster, ”The Blob”, som är rent bedrövligt. Man flyttar helt enkelt funktionaliteten från FlyingUnit och NavalUnit och lägger dessa i Unit, så att man kan använda båda till sin nya hybridenhet!
Problemet är att fortsätter man jobba så här tillräckligt länge kommer klassen Unit vara extremt svårläst och full med kod som i många enheters fall inte behövs, men som kanske anropas ändå.

I fallet med Age of Empires 2 fanns dels kod för att fylla på träden för att dess resurser inte skulle ta slut. Denna slutade man använda för att det bedömdes förlänga Multiplayer-matcher för mycket. Men man tog inte bort all koden, och märkte så småningom att denna fortfarande kördes trots att den inte användes, och tog upp en hel del prestanda. Ett annat problem med ärvda enheter nämnda ovan var att alla träd kontrollerades efter deras ”line of sight”, vilket var totalt onödigt eftersom träden inte kontrolleras av en spelare och därför inte behöver se någonting alls.

Hur skulle man då ha kodat Age of Empires 2 för att undvika detta? Förslagsvis, med komponenter. Man skapar objekt, och har sedan ett antal attribut som går att tilldela dessa, exempelvis för att låta dem röra sig, attackera eller vad man nu kan tänkas vilja. Denna artikel går igenom ämnet och diskuterar hur det går om man inte använder det!

Ingen kod alls denna vecka? Nej det verkar inte bättre! Men det är såklart viktigt att ta lite teori ibland också! 😉
Nu nämnde jag ”anti-designmönster” och ”komponenter” ovan, vad är det för något? Det, och vad ”designmönster” är ska vi gå igenom nästa vecka! Ses då 😉

Kommentera