”Mechanical Sympathy” i softwareudvikling

Selvom man ikke arbejder med high-performance/low-latency softwareudvikling til daglig, kan det alligevel være nyttigt at lære den hardware at kende, som ens software kører på, og dermed blive bedre til at forstå hvad der sker på runtime. Mange softwareudviklere har i sin tid lært om cpu-arkitektur, men er denne viden up-to-date? Der er sket en stor udvikling inden for dette område. Selvom vi ikke længere kan forvente en forøgelse af hastigheden i hver enkelt cpu (clock speed), så kan vi alligevel få udført flere og flere operationer pr sekund, idet cpu’erne bliver designet anderledes; der er bl.a. parallelisering inde i hver cpu-kerne.

Jeg var til et foredrag på GOTO Aarhus konferencen i 2012, hvor high-performance/low-latency specialisten Martin Thompson gav sit foredrag om ”Mechanical sympathy”, hvor han argumenterede for at alle softwareudviklere burde sætte sig ind i den hardware der benyttes, for at kunne få softwaren til at udnytte hardwaren på bedst mulig vis. Også når man udvikler i Java!

Udtrykket Mechanical sympathy er taget fra formel1 verdenen, hvor en racerkører får mest ud af sin racerbil ved at kende motoren til punkt og prikke.

I ”gamle dage” sad man og kodede meget mere hardware-nært, end man gør i dag. Højniveau-sprog tillader programmøren at undgå at tænke over hardwaren. Men selvom man koder i Java, kan man sagtens tænke i nye baner og blive bedre til at kode mere hardware-nært.

Tilbage til cpu-arkitektur designs. De nyere cpu’er er designet til at have meget hurtige og forholdsvis store cpu-caches samt hurtig kommunikation cpu’erne imellem. At tilgå main memory er langsomt, så derfor er det vigtigt at få ens kode ned i cpu caches. Og at undgå cache-misses, hvilket man kan opnå ved at tænke mere lineært og sekventielt når man koder. Det er som regel kun en mindre del af al koden, som eksekveres oftest. Hvis man kan undgå at der bliver hoppet alt for meget rundt i koden (fx ved at undgå LinkedLists i Java), så kan den del af koden, der eksekveres oftest, ligge i cache. Undgå også at OS’et bliver indvolveret, idet dette kan medføre at alt hvad der ligger i cache bliver slettet.

Oftest er cpu’erne idle – prøv at køre et analyseværktøj (fx perfstat på Linux) på det kørende software og se hvor mange af cpu’erne, der er idle. I det hele taget er det meget gavnligt at analysere softwaren jævnligt, og at udføre fx perfomancetests ugentligt – og ikke kun lige op til en software release!

Kommunikationen imellem to eller flere cpu’er er gjort hurtigere, så derfor er det oplagt at bruge message passing paradigmet i stedet for at tilgå fælles hukommelse.  Som Martin Thompson sagde i et interview: ”Hvis jeg gerne vil vide hvad dit navn er, så kan jeg spørge dig og få et svar. Alternativt kan jeg åbne op til din hjerne og tilgå lige netop de nervesynapser, som indeholder dit navn, og på den måde få fat i navnet. Hvorfor er det at vi programmerer sådan?” Hvorfor ikke bruge message passing mellem to tråde i stedet for, specielt når de nyeste cpu-arkitektur designs netop passer til message passing: cpu cache endda i flere lag (L1, L2 og L3) samt hurtig kommunikation imellem cpu’erne

En anden optimering, og et fornuftigt alternativ til at undgå concurrency problemer, er at bruge lock-free algoritmer. Vi står nemlig i en svær situation: vi har multiple cpu’er, vi har multi-threading (endda inde i hver cpu-kerne) og vi tilgår delt hukommelse. Mange bruger locks til at løse problemet med delt hukommelse. Men locks i koden er meget langsomt, og kan medføre deadlocks, der er svære at debugge. Når man bruger locks i koden, så skal koden have fat i operativsystemet for at få løst spørgsmålet om hvilken tråd, der har lov til at tilgå den fælles hukommelse. Og det er ikke altid at operativsystemet lige har tid til at få løst konflikten mellem flere tråde, idet der kan være andre og mere vigtigere ting at lave for OS’et, før den vender tilbage. Dette tager tid! Faktisk kan det tage ca. 1000 gange længere tid for en simpel operation, hvis man har 2 tråde med én lock i forhold til 1 tråd uden locks.

Der findes en del alternativer til locks:

  • Actor model
  • CAS operationer (Compare And Swap/Set) som ikke involverer OS’et !
  • Lock-free algoritmer

Martin Thompson har været med til at udvikle et High Performance Inter-Thread Messaging Library for Java, som hedder Distruptor. Der er ingen contention imellem trådene og ingen locks, hvilket medfører at frameworket er utroligt hurtigt. Læs mere om Disruptor her:

Javas eget concurrency framework (java.util.concurrent) er med Java 7 blevet væsentligt forbedret, og nogle af implementationerne heri er også blevet ret hurtige (på niveau ned Disruptor frameworket). Det interessante med Disruptor er også at frameworket håndterer og implementerer concurrency anderledes end hvad der er gjort før.

Hvad har du af erfaringer med at skulle tænke over hvilken hardware, du kører med, selvom du udvikler i Java? Og har du måske oplevet bøvl med fx performancetests, hvor der dukker uforklarlig latency op? Og hvad var løsningen i så fald?

3 comments for “”Mechanical Sympathy” i softwareudvikling

    • Tak. Den artikel ser også spændende ud. Hvis man gerne vil vide præcis hvordan ens CPU micro-arkitektur ser ud, så søg efter fx Intel og find den model (dvs. codename) du har. Man kan finde illustrationer af hvordan én kerne i én CPU er designet, fx hvor mange addition, subtraktion, multiplikation og division-operationer der kan paralleliseres.

Skriv et svar til Helena Meyer Annuller svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *