Dette indlæg rummer egentlig ingen ny viden, men på trods af at jeg efterhånden har brugt lang tid på finurlige detaljer omkring .NETs afviklingsmiljø, er jeg først nu stødt på denne detalje, så set i det lys vil jeg tillade mig at betegne emnet for dette indlæg som esoterisk og gentage noget, der muligvis ikke er nyt for alle.
Som det forhåbentlig er kendt for læserne af denne blog, foregår dynamisk allokering af hukommelse i .NET enten i generation 0 af heapen eller på Large Object Heap (LOH) i fald den allokerede instans er på 85.000 bytes eller mere.
Derfor vil small pege på et array allokeret i generation 0 og large pege på et array allokeret på LOH i nedenstående eksempel.
var small = new byte[1000]; var large = new byte[85000];
Det kan vi verificere via WinDbg eller ved at kalde GC.GetGeneration(), der som forventet returnerer henholdsvis 0 og 2 for de to referencer (bemærk, at metoden ikke skelner mellem generation 2 og LOH, da de i forhold til garbage collection behandles i samme ombæring). Bruger vi debuggeren, får vi lidt mere nøjagtig information, og her kan vi se, at large faktisk peger på en instans på LOH.
Gentager vi øvelsen for double[], burde vi igen kunne forudsige placeringen af de enkelte instanser. En double fylder 8 bytes, så for at komme over den magiske grænse, skal vi have et array med lidt over 10.000 elementer, lad os bare sige 11.000. Med det in mente burde vi altså kunne konkludere at small og large endnu en gang peger på instanser i henholdsvis generation 0 og på LOH.
var small = new double[1000]; var large = new double[11000];
Det er bare ikke sådan det forholder sig. Begge arrays bliver allokeret på LOH!
CLRen benytter nemlig en forholdsvis esoterisk optimering i dette tilfælde. Arrays af double med 1000 eller flere elementer bliver mod forventning altid allokeret på LOH. Det er nyt for mig, men hvis man nærlæser kommentarerne til dette gamle blogindlæg, kan man se, at det er ”by design”. Det er altså ikke en fejl, det er en feature.
Argumentationen er, at objekter på LOH altid ligger på 8 bytes skel, og derfor giver bedre performance ved opslag af elementerne. Jeg har ikke kunne finde nogen forklaring på, hvorfor grænsen på 1000 elementer er valgt, men sådan forholder det sig nu engang.
Det er fristende at prøve, om vi kan eftervise effekten af denne optimering, men det er ikke let i praksis, da vi har meget begrænset kontrol over og indsigt i allokering og adresselæsninger i managed code, så derfor må vi tage Microsofts ord for pålydende her.
Konsekvenserne af denne optimering kan være mange og ikke alle nødvendigvis til vores fordel. Hvis vi antager, at optimeringen har sin berettigelse, så må vi gå ud fra, at læsning af elementerne i et double[], nyder gavn af den gunstige placering i hukommelsen.
Til gengæld er disse arrays pludselig dyrere at allokere, eftersom allokering på LOH benytter en free list, hvilket kan være mange gange langsommere end den simple pointeroperation, der skal til ved allokering i generation 0. Ligeledes vil levetiden af disse arrays stige markant. Er der tale om midlertidige arrays, går de fra hurtig oprydning i generation 0 til en lang levetid på LOH. Det kan have indflydelse på hvor stor belastning garbage collection lægger på applikationen, ligesom det kan give yderligere problemer i form af fragmentering af LOH.
Konsekvenserne afhænger af den konkrete applikations forbrug af hukommelse, men givet er det, at det kan være en særdeles vigtig detalje i nogle scenarier. Så med mange års forsinkelse gør jeg hermed mit til at udbrede kendskabet til en godt bevaret hemmelighed.
Spøjst designvalg. Og det lyder da ærligt talt lidt problematisk hvis de kun kan garantere 8-byte alignment for objekter på LOH. Det betyder man risikerer et mærkbart performance hit hver gang man bruger dynamisk allokerede doubles (class members, for eksempel)
Optimeringen er specifik i forhold til double arrays. Jeg ved ikke, hvad de gør i forhold til doubles generelt.
Nej, men deres forklaring antyder at de kun kan garantere 8-byte alignment (som er nødvendig for effektiv tilgang til doubles) i objekter allokeret på LOH.
Jeg går ud fra at de også kan aligne valuetypes på stakken, så doubles som lokale variable burde også være ok, men for doubles som medlemmer i en klasse er den eneste konklusion jeg kan se, at de ikke er garanteret korrekt alignment, da de hverken ryger på stakken eller på LOH.
Fordelen ved at de gør det sådan er vel at GC’en ikke behøver tage højde for alignment, og blot kan placere alle objekter på 4-byte boundaries. Men et interessant trade-off.