Archive for the ‘Performance’ Category

Julekonkurrence: Performance

Tuesday, December 2nd, 2008

 

Microsoft har været så flinke at stille en række præmier til rådighed til lidt julespas, så derfor afholder jeg i løbet af december et par små kodekonkurrencer her på siden. Vi lægger ud med en lille konkurrence omkring performance. 

Betragt nedenstående metoder, SlowSum() og FastSum()

      private const int total = 1000;
      private static readonly int[,] numbers = new int[total, total];

      static private int SlowSum() {
         var sum = 0;
         for (var first = 0;  first < total; first++) {
            for (var second = 0; second < total; second++) {
               sum += numbers[second, first];
            }
         }
         return sum;
      }

      static private int FastSum() {
         var sum = 0;
         for (var first = 0; first < total; first++) {
            for (var second = 0; second < total; second++) {
               sum += numbers[first, second];
            }
         }
         return sum;
      }

De gør begge nøjagtig det samme og burde således i teorien afvikle lige hurtig, men i praksis vil SlowSum() typisk være markant langsommere end FastSum(). Hvis total f.eks. sættes til 1000 som i eksemplet, vil en simpel måling på min maskine vise, at SlowSum() er ca. dobbelt så lang tid om opgaven som FastSum().

Spørgsmålet er derfor: Hvorfor er dette tilfældet?

Send dit svar i en mail til brian@kodehoved.dk. Deadline er på fredag kl. 20.

Jeg trækker lod blandt de rigtige besvarelser om den fedeste af mine præmier. Jeg trækker også lod blandt alle besvarelser (det vil sige, noget der ligner et svar, uanset om det er korrekt eller ej) om en eller anden trøstepræmie. 

Svaret, vinderne og de indkomne forslag offentliggøres i løbet af weekenden. 

Spørgsmål til konkurrencen: smid en kommentar.

Performance-målinger i C#

Monday, April 14th, 2008

Der er en glimrende artikel af Vance Morrison i april-nummeret af MSDN magazine vedrørende måling af performance i C#, og en af forfatterens gode pointer er, at man skal være sikker på, at man måler det, man tror, man måler. Det lyder simpelt, men når vi tænker på, at der er ikke en men to optimerende compilere i spil på vejen fra kildetekst til kørende kode, er der gode muligheder for, at den kode, der faktisk afvikles, ikke stemmer fuldstændig overens med den oprindelige kildetekst. Dertil kommer naturligvis de effekter, et komplekst afviklingsmiljø har på den slags målinger.

Et eksempel på dette så jeg for nylig, da en kollega fremlagde nogle performance-målinger, han havde lavet i forbindelse med en diskussion af forskellen på at caste med as-operatoren eller via den traditionelle (type)-konstruktion. Han havde læst i Bill Wagners Effective C#, at as er mere effektiv end (type), og det ville han gerne eftervise, så han lavede et lille program, der skulle tage de relevante målinger. I pseudokode så hans program nogenlunde ud som følger (det hele er indeholdt i en metode, så der er ikke overhead fra kald og JIT-oversættelse foregår inden tidtagning):

  1. opret en instans og en ”tom” reference
  2. start tidtagning
  3. lav et antal casts med (type) så referencen peger på instansen
  4. rapporter tid for operation
  5. lav det samme antal casts med as-operatoren
  6. rapporter tid for operation

Jeg har forsimplet det lidt, men ovenstående er tro over for hans implementering, og som sådan ser det jo fornuftig ud. Formuleringen ”et antal” dækker over en for-løkke, hvilket kan være en god ide for at undgå støj i måleresultaterne. Brug lige et par sekunder på at tænke over hvilke faldgruber, der kan være i denne fremgangsmåde.

Som nævnt har vi både en C#-compiler og en JIT-compiler i spil, inden vores kode rent faktisk kører. De har lidt forskellige råderum og muligheder, men deres fælles mål er at producere effektiv kode, der er funktionelt identisk med den oprindelige kildetekst. Læg mærke til at dette sjældent vil resultere i en direkte afbildning af kildeteksten. En af de væsentligste motivationer for højniveausprog og compilere er jo netop, at vi kan udtrykke os på et højere abstraktionsniveau og lade arbejdet med at få vores kode til at køre hurtig være op til compileren.

Compilere har således et helt katalog af tricks, de benytter i oversættelsen af vores kode. Et af disse er: lad være med at generere kode, der ikke har indflydelse på tilstanden af applikationen. I ovenstående eksempel viste det sig, at C#-compileren var er smart nok til at konstatere, at der ikke var grund til at lave alle de identiske casts.

Testprogrammet opretter en instans og en reference og caster derefter instansen et antal gange til den samme reference. Det vil sige, at referencen bliver sat til at pege på den samme instans adskillige gange i begge løkker. Derfor kan compileren uden videre droppe tildeling i anden løkke, hvilket vi let kan konstatere ved at inspicere den generede IL-kode. Det havde min kollega ikke gjort, så derfor endte han med at konkludere, at den ene type cast er hurtigere end den anden til trods for, at det, han i realiteten havde målt, var, at det var hurtigere at lave ingenting end at caste – hvilket forhåbentlig ikke kommer bag på nogen.

Da jeg påpegede miseren, stoppede den reelle diskusion der, men vi kunne have taget et skridt mere i retning af at finde ud af hvilken kode, der bliver genereret. For man skal jo ikke være særlig opfindsom for at tænke sig til, at der heller ikke er noget behov lave det samme cast et antal gange. C#-compileren lavede ikke denne optimering, men der er gode chancer for, at JIT-compileren kan konstatere, at der ikke er grund til at lave mere end et cast og derfor flytter det først cast ud af løkken. I så fald ender vi med kode, der funktionelt gør som angivet i kildeteksten, men hvor de overflødige gentagelser er barberet væk, og derfor er målingerne ubrugelige.

I dette tilfælde var det ikke nødvendig at grave dybere end til IL-laget for at konstatere, at den opstillede test ikke tjente formålet. I andre tilfælde er vi nødt til at se på den JIT-oversatte kode, for at kunne udtale os om, hvad der faktisk afvikles inden, vi begynder at måle på det og konkludere, at en metode er hurtigere end en anden.

List<T> under overfladen

Wednesday, December 5th, 2007

Med introduktionen af .NET 2.0 fandt ArrayList en afløser i List<T>. Skønt ordet ”array” ikke længere indgår i navnet, er List<T> faktisk stadig baseret på Array, hvilket vi kan konstatere ved hjælp af Reflector.

De største fordele ved List<T> i forhold til ArrayList er typefastheden og en ganske betydelig performanceforbedring i forbindelse med håndtering af værdityper (value types). Typefastheden leder til simplere kode, da vi undgår at skulle undersøge type på og om nødvendig caste elementer, når vi piller dem ud af listen. Performanceforbedringen kommer af, at vi undgår at box/unboxe i forbindelse med værdityper.

Så for at gøre en lang historie kort: List<T> er langt at foretrække frem for ArrayList, så for at få mest ud af den, er det en god ide at se på, hvordan den er implementeret. Som nævnt er List<T> internt baseret på en Array. Det betyder, at skønt List<T> præsenterer en datastruktur uden fast størrelse, er dette blot en illusion. Når man opretter en List<T>, har den en størrelse, og når der er behov for mere plads, er typen således nødsaget til at oprette en ny, større struktur og flytte alle elementer til denne. Det er selvsagt ikke en billig operation.

Som udgangspunkt afsættes der faktisk ikke plads i en List<T>, hvilket man kan verificere ved at konstatere at Capacity indledningsvis sættes til 0. Så snart der tilføjes et element, afsættes der plads til 4 elementer, og herefter fordobles kapaciteten, hver gang behovet opstår. Det vil sige, at det femte element (og her tænker jeg ikke på filmen) resulterer i at Capacity stiger til 8, det niende element får Capacity til at stige til 16 og så fremdeles. Det betyder, at der for små lister kan spildes meget tid på disse interne omrokeringer, og derfor er det en god ide, at oprette sine List<T>-instanser med en passende kapacitet via den tilhørende constructor.

For meget store lister er dette overhead mindre betydende, men her kan man optimere pladsforbruget ved at sætte en passende kapacitet, så man kan undgå, at der afsættes for meget plads til referencer. Har man f.eks. en liste med 1,1 million navne, vil standardopførelsen gøre, at der afsættes plads til 2^21 eller 2.097.152 elementer. Så uanset hvad er det en god ide at sætte en indledende kapacitet, hvis man på nogen måde har mulighed for at komme med et kvalificeret gæt på den nødvendige kapacitet.

Mange implementeringer af en List-type er baseret på det, man kalder en hægtet listet (linked list), i hvilken brugeren har en reference til det første element i listen, og hvert element desuden har en reference til det efterfølgende element (eller null for sidste element i listen). En sådan implementering vil typisk tilbyde en Insert()- og en Add()-metode ganske som tilfældet er for List<T>. I dette tilfælde vil Insert() almindeligvis være særdeles effektiv, idet det kun kræver justering af to referencer at indsætte et element forrest i listen. Med mindre listen også gemmer en reference til det sidste element, vil det til gengæld være dyrt at indsætte elementer sidst i listen, da dette kræver, at vi løber hele listen igennem via referencerne i de enkelte elementer.

Sådan er det ikke med List<T>. Her forholder det sig faktisk lige omvendt. At tilføje et element via Add() er særdeles effektivt, men vil dog medføre et overhead, hvis kapaciteten skal udvides. Faktisk fremgår det af dokumentationen, at Add() som udgangspunkt er en O(1) operation. Skal listen udvides bliver det dog en O(n) operation.

Insert() er derimod en O(n) operation ifølge dokumentationen, men hvis vi tager et kig på implementeringen ved hjælp af Reflector (se nedenfor), ser vi, at dette faktisk ikke gælder, hvis Insert() resulterer i at elementet indsættes i slutningen af listen. I det tilfælde er Insert() stort set lige så hurtig som Add() – og i realiteten er der tale om O(1).

public void Insert(int index, T item)
{
   if (index > this._size) {
      ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index,
         ExceptionResource.ArgumentOutOfRange_ListInsert);
   }
   if (this._size == this._items.Length) {
      this.EnsureCapacity(this._size + 1);
   }
   if (index < this._size) {
      Array.Copy(this._items, index, this._items, index + 1, this._size - index);
   }
   this._items[index] = item;
   this._size++;
   this._version++;
}

Som det også fremgår af ovenstående, ligger Insert() naturligvis under for de samme restriktioner som Add() i det tilfælde, at kapaciteten skal udvides.

Så hvad kan vi lære af at se på implementeringen af List<T>? For det første, vil det som sagt være en god ide at vælge en passende kapacitet ved oprettelsen af sin liste for at undgå de tunge omallokeringer og det voldsomme overhead ved store lister. Dernæst kan vi konstatere, at i og med at List<T> ikke er implementeret som en hægtet liste, er Add() langt at foretrække frem for Insert(). Faktisk er omkostningerne ved sidstnævnte så store, at man virkelig kun skal bruge den som sidste udvej. Og sidst men ikke mindst, er der ingen grund til fortsat at bruge ArrayList.

Kompakt og hurtig kode med delegates

Thursday, November 22nd, 2007

System definerer et antal delegates, heriblandt to generiske delegates: Predicate<T> og Action<T> til brug for en del af List<T>s metoder. Predicate<T> bruges til at specificere en delegate, der udvælger elementer fra en liste baseret på et eller andet kriterium. Action<T> bruges til at specificere en delegate, der skal udføres på en eller flere af elementerne i en liste.

Predicate<T> kan f.eks. bruges på en List<T> til at udvælge et antal elementer via en anonym delegate. Hvis vi eksempelvis har en liste af ord, kan vi finde alle ord med tre eller flere tegn ved hjælp af FindAll() og en passende anonym Predicate<T> delegate.

List<string> longerthanthree =
   strings.FindAll(delegate(string current) { return current.Length > 3; });

I C# 3.0 kan det tilmed gøres endnu mere elegant ved hjælp af lambda-udtryk:

List<string> longerthanthree =
   strings.FindAll(current => current.Length > 3);

Personligt finder jeg disse former langt mere elegante end den tilsvarende implementering med foreach.

foreach (string s in strings) {
   if (s.Length > 3) {
      longerthanthree.Add(s);
   }
}

Vi kan altså bruge delegates som ovenstående til at skrive meget kompakt og præcis kode. Hvis det giver mening for den pågældende klasse, kan vi tilmed definere delegates til hyppige operationer. Hvis vi eksempelvis har en klasse, der repræsenterer overvågning af forskellige services, kunne vi have en ServiceWatcher-klasse, der definerer en Predicate<T> og en Action<T>, der gør det muligt at skrive kode som nedenstående.

watchers.FindAll(ServiceWatcher.AreRunning).ForEach(ServiceWatcher.StopIt);

watchers er en List<T> af ServiceWatchers, AreRunning er en Predicate<T> og StopIt er en Action<T>. De to delegates er implementeret således:

public static Predicate<ServiceWatcher> AreRunning =
   delegate(ServiceWatcher w) { return w.IsRunning; };
public static Action<ServiceWatcher> StopIt =
   delegate(ServiceWatcher w) { w.Stop(); };

Da de begge udelukkende arbejder på deres argument, kan de med fordel laves static.

(Note: Ovenstående kunne naturligvis også implementeres via en StopAllRunning()-metode, der indkapslede de nødvendige operationer. Har man flere udvælgelseskriterier og flere handlinger, er delegates dog at foretrække, da brugeren derved let kan sammensætte de relevante kombinationer.)

Hvis ovenstående konstruktion skal skrives ved hjælp af et loop, ser det ud som følger:

foreach (ServiceWatcher watcher in list) {
   if (watcher.IsRunning) {
      watcher.Stop();
   }
}

Det er måske en smagssag, hvilken variant man foretrækker, så hvis det bare var et spørgsmål om syntaks, kunne indlægget passende slutte her, men hvis vi ser lidt på performance af de forskellige teknikker, bliver det interessant.

Hvis jeg kører ovenstående igennem i Release mode på en liste med 100 elementer, kører delegate-versionen ca. dobbelt så stærkt som loop-versionen. Ved 10.000 elementer kører delegate-versionen ca. 6 gange så stærkt og ved en million elementer ca. 10 gange så stærkt! Jeg kan ikke på nuværende tidspunkt redegøre for hvilke interne optimeringer, der er årsag til denne forskel, men jeg kan blot konstatere, at dette er et af de få tilfælde, hvor den mest elegante kode også resulterer i den mest effektive ydelse. Derfor er der god grund til at se på disse delegates, når man arbejder med List<T>.

Indeksering af arrays

Saturday, November 10th, 2007

Jeg læste for nylig i en ældre C#-bog, at 1.x compileren optimerede løkker med arrays, hvis man anvendte Length af det pågældende array som grænseværdi for gennemløbet. Det gjorde mig nysgerrig, og jeg satte mig derfor for at undersøge forskellen på nedenstående tre implementeringer i håb om at finde nogle retningslinjer for valg af metode til gennemløb af arrays. Læg mærke til at der er tale om gennemløb, hvor vi har brug for at indeksere os frem til de enkelte elementer. For gennemløb uden dette behov, er syntaksen ved foreach klart at foretrække efter min mening.

public void Length() {
   int[] numbers = new int[1000000];

   for (int i = 0; i < numbers.Length; i++) {
      numbers[i] = i;
   }
}

public void Constant() {
   int[] numbers = new int[1000000];

   for (int i = 0; i < 1000000; i++) {
      numbers[i] = i;
   }
}

public void Argument(int length) {
   int[] numbers = new int[length];

   for (int i = 0; i < length; i++) {
      numbers[i] = i;
   }
}

Det første sted at undersøge er naturligvis den resulterende IL-kode, men her er absolut intet at hente. I Release mode genererer compileren stort set identisk kode for de tre varianter. Afvigelserne er udelukkende i forhold til måden at finde værdierne for længden. Der er altså ikke noget, der tyder på at den ene implementering vil være mere effektiv end den anden.

Næste skridt er derfor at måle den faktiske performance, og til det formål lavede jeg et instrumenteringsprojekt med det indbyggede Performance Tool i Visual Studio. Det er måske ikke så fancy som visse dedikerede performanceværktøjer, men det er let at bruge, og det løser opgaven i mange tilfælde.

Målingerne på min maskine viste, at jeg skulle helt op på en million elementer i min array, før Length-varianten var marginalt hurtigere end de andre. For lavere antal var den faktisk den langsomste, men i alle tilfælde lå målingerne tæt på hinanden. Ved meget lave antal elementer er prisen for et metodekald (properties er jo bare metoder, med smart syntaks) signifikant, og derfor bliver Length-varianten taberen her. (rettet: det er noget vrøvl – CLRen har support for arrays, så Length er ikke en almindelig metode, men er direkte understøttet af IL-kommandoen ldlen, men under normale omstændigheder ville metoden blive kaldt ved hvert gennemløb.)

Jeg har således ikke kunne finde argumentation for at favorisere den ene fremgangsmåde frem for den anden, så derfor vil min anbefaling være, at man anvender den, der giver det mest robuste kode, og det vil være Length-varianten, da den undgå dublering af information.

Tråde og omkostninger

Monday, August 20th, 2007

Som nævnt var højdepunktet ved Devscovery Jeffrey Richters heldagsseance omkring threading, og derfor vil jeg i de næste par indlæg gennemgå hans væsentligste pointer. Meget af det, han fremlagde, havde jeg set før, men han fik skabt en kontekst, der fik undertegnede til at se på threading med helt andre øjne.

Jeg har efterfølgende genlæst hans kapitler om threading i CLR via C#, og skønt han fremlægger mange af de samme pointer, fremstår de i mine øjne langt fra så tydelige som de gjorde på konferencen. Problemet omkring threading er i en nøddeskal, at tråde er dyre, og derfor bør man undgå dem. På den anden side, er tråde en nødvendighed for at udnytte flere CPUer samt et middel til at lave robuste applikationer. Udfordringen er således at finde den til enhver tid korrekte afvejning. I dette indlæg vil jeg se på omkostningerne ved tråde.

Tråde er dyre. De er billigere end processer, men stadig meget dyre. I praksis kan det let tage op til et par sekunder at starte en ny tråd. Det lyder selvfølgelig ikke af meget, men det svarer til grotesk mange clock cycles, så for en applikation er det en høj omkostning. Lad os se lidt på, hvorfor tråde er så dyre.

Tråde er både et Windows- og et CLR-begreb. I C# er tråde repræsenteret ved klassen Thread i System.Threading. Dette er blot en letvægtsstruktur, der repræsenterer en tråd i Windows-regi. Det er stort set omkostningsfrit at oprette en Thread-instans i C# (det er blot et objekt på heapen). Det er først, når man kalder Start() på instansen, at CLRen beder Windows om at oprette en rigtig tråd, og det er her prisen betales. Når der oprettes en tråd sker følgende:

  1. Hver tråd har en såkaldt kontekst, der bruges til at opbevare de aktuelle registerværdier for tråden. Størrelsen af denne er processorafhængig. For x86 drejer det sig om ca. 700 bytes. For IA64 ca. 2500 bytes.
  2. Hver tråd har desuden en user mode-stak og en kernel mode-stak på henholdsvis 1 MB og 12 KB (24 KB i 64 bit Windows). Disse afsættes som commited memory, så snart tråden oprettes i operativsystemet.
  3. Når en tråd oprettes, sender Windows et signal om dette til samtlige DLLer i den aktuelle proces. Det kan let betyde op til et par hundrede funktionskald, og hvad værre er, vil flere af disse ofte medføre paging for at få de nødvendige kodestumper indlæst. Det samme sker, når tråden nedlægges.

Det vil altså sige, at hver tråd optager lidt over en megabyte, uanset om tråden laver noget eller ej. Med minimum 512 MB i de fleste maskiner i dag er det naturligvis ikke det store, men givet at mange applikationer i den grad sviner med antallet af tråde, er det ikke ualmindeligt at have flere hundrede tråde kørende på en gang (efter en mindre oprydning er jeg nede på 443 tråde, mens jeg skriver dette). Det er meget hukommelse for ingenting. Det kan f.eks. være svært at forstå, hvorfor winlogon.exe har ikke mindre end 18 tråde i spil (under XP – i Vista er antallet dalet til tre hvilket må siges at være langt bedre). Alle disse tråde bidrager til et øget memoryforbrug, og dermed en dårligere ydelse for hele systemet. Når vi tilmed tager i betragtning, at CPUen ofte er ledig i 90-95% af tiden på en almindelig brugermaskine, betyder det, at størstedelen af trådene ikke laver noget som helst (udover at optage en masse hukommelse). Da hverken applikationerne eller systemet som helhed har glæde af inaktive tråde, ville alle være bedre tjent med færre tråde.

Hvis der er mere end en tråd per CPU, sørger Windows for at der hyppigt skiftes imellem disse, så det ser ud som om, at de alle kører samtidig. Dette skift kaldes et context switch, og det er ligeledes en dyr operation. Ca. hvert 20 millisekund afbryder Windows den kørende tråd for at lade en anden komme til. For at dette kan lade sig gøre, skal Windows i kernel mode og den kørende tråds tilstand skal gemmes. Det vil sige, at CPU-registre gemmes i den tilhørende kontekst. Derefter skal Windows finde ud af hvilken tråd, der nu skal have lov at køre og efterfølgende hente dens kontekst ind i CPU-registrene. Til slut forlades kernel mode igen. Alt dette sker ca. hvert 20 millisekund, hvis der er mere end en tråd per CPU. Det burde være indlysende, at dette overhead ikke gavner applikationens ydelse. Det optimale er således, at der kun er en tråd per CPU, idet det sikrer den største mulige parallelle afvikling.

Så for at opsummere får vi altså den optimale udnyttelse af tråde, hvis vi begrænser os til kun at have en tråd per CPU, da det giver det mindste hukommelsesforbrug i forhold til udnyttelsesgraden, idet vi derved slipper for context switches i den aktuelle proces (Windows skifter naturligvis også mellem tråde i forskellige processer, men den del kan vi ikke kontrollere i en given applikation).

I næste indlæg vil jeg se på, hvordan man sikrer sig det optimale antal tråde i en applikation.

Hurtigere structs

Thursday, July 19th, 2007

Alle .NET typer implementerer en lille håndfuld metoder, som de får via object. En af disse er den virtuelle metode Equals(object). objects implementering laver en referencesammenligning og returnerer derfor kun true hvis this og object peger på samme instans.

Værdityper arver fra System.ValueType (der arver fra object), og denne overskriver objects implementering af Equals(object), så der i stedet laves en sammenligning af de enkelte værdier i den pågældende værditype. Det vil sige, at hvis vi eksempelvis har nedenstående struct (structs er værdityper), implementerer System.ValueType en brugbar Equals(object) for os, og derfor kan vi uden videre sammenligne to Locations.

public struct Location {
   private int X, Y, Z;

   public Location(int x, int y, int z) {
      X = x; Y = y; Z = z;
   }
}

ValueType implementerer Equals(object) ved hjælp af Reflection, hvilket forklarer, hvordan en enkelt implementering kan varetage sammenligning af alle tænkelige værdityper. Desværre har denne fleksibilitet en betragtelig effekt på afviklingshastigheden.

En tur gennem CLR-profileren viser, at et enkelt kald af Location.Equals(object) tager
0.072856 millisekunder i Debug-mode og 0.073524 millisekunder i Release-mode. Det synes jo ikke af meget, men hvis vi ulejliger os med at lave vores egen udgave af Equals(object), kan det gøres meget bedre. Bemærk, at vi helst skal undgå at object-argumentet skal unboxes inden sammenligningen, da unbox-operationen medfører et betragtelig overhead. Derfor er implementeringen nedenfor lavet ved brug af en hjælpemetode, der kun kaldes med Location-instanser. Derved undgår vi unboxing.

public struct Location {
   private int X, Y, Z;

   public Location(int x, int y, int z) {
      X = x; Y = y; Z = z;
   }

   public override bool Equals(object obj) {
      if (obj is Location) {
         return Equals((Location)obj);
      }
      return false;
   }

   private bool Equals(Location loc) {
      return X == loc.X && Y == loc.Y && Z == loc.Z;
   }
}

Her viser CLR-profileren, at der virkelig er sket noget. Et enkelt kald i Debug-mode tager nu 0.000249 millisekunder og i Release-mode afvikles kaldet på 0.000152! Det er sjældent, at så lidt kode kan gøre så stor forskel, så hvis man arbejder med structs og har brug for at sammenligne dem, er det en særdeles god ide at implementere sin egen version af Equals(object). Resultaterne påvirkes naturligvis af den konkrete struct, men som det fremgår er forskellen så stor, at denne teknik ikke bør overses.

Retfærdigvis skal det siges, at compileren kommer med en advarsel, hvis man ikke implementerer GetHashCode(), når man implementerer Equals(object). Det har jeg dog undladt i dette eksempel af hensyn til overskueligheden.