Blog 8 Min. Lesezeit

Effizienten C-Code schreiben: Von Linus Torvalds lernen

Share this article
Write Efficient C Code: Learn from Linus Torvalds

Jedes Projekt hat seinen eigenen Styleguide – eine Reihe von Konventionen dafür, wie Code geschrieben werden soll. Manche Verantwortliche legen grundlegende Coding-Regeln fest, andere bevorzugen anspruchsvollere Vorgaben. In vielen Projekten gibt es jedoch überhaupt keine definierten Regeln, sodass jeder Entwickler seinen eigenen Stil verwendet.

Eine große Codebasis lässt sich wesentlich leichter verstehen, wenn der gesamte Code einem einheitlichen Stil folgt.

Es gibt zahlreiche Quellen zu bewährten Programmierpraktiken. Gute Coding-Regeln können wir beispielsweise lernen durch:

  • das Lesen eines Buches oder Fachmagazins.
  • die Nutzung von Online-Ressourcen.
  • das Lernen von einem Kollegen.
  • die Teilnahme an einer Schulung.

Wir können auch einige Monate mit einem Experten zusammenarbeiten, um die Programmierfähigkeiten des Teams zu verbessern. Die richtige Person zu finden ist allerdings nicht einfach und kann für ein Unternehmen teuer werden. Doch warum nach einem Experten suchen, wenn wir von außergewöhnlichen Entwicklern wie Linus Torvalds lernen können? Schon die Untersuchung von Quellcode, den er entwickelt oder betreut hat, liefert wertvolle Erkenntnisse darüber, wie effizienter C-Code geschrieben wird.

Linus Torvalds ist weithin als Schöpfer des Linux-Kernels bekannt, dessen leitender Entwickler er viele Jahre lang war. Außerdem entwickelte er das äußerst populäre verteilte Versionskontrollsystem Git. That alone makes his code well worth studying :)

Ein Blick in den Git-Quellcode

Sehen wir uns einen Codeausschnitt aus Git an:

Zu diesem Code lassen sich einige Dinge feststellen:

  • Die Funktionen sind als static deklariert.
  • Die Funktionen geben einen Fehlercode zurück.
  • Die Funktionen haben nur wenige Parameter.
  • Die Funktion wird so früh wie möglich verlassen.
  • Die Variablen sind als static deklariert.
  • Die Variablennamen sind leicht verständlich.
  • Die Funktionen sind sehr kurz.
  • Der Code ist sauber eingerückt.
  • Keine überflüssigen Kommentare im Funktionskörper: Der Code spricht für sich.
  • Die Funktionskörper sind sauber eingerückt.
  • Die Include-Guards sind klar und verständlich.

Beim Durchsehen des Git-Quellcodes fällt auf, wie konsequent er umgesetzt ist: Dieselben Best Practices werden durchgängig angewendet. Zur Überprüfung suchen wir nach statischen Funktionen:

from m in Methods where m.IsStatic select m

Die Treemap eignet sich hervorragend, um einen Überblick über die von einer CQLinq-Abfrage gefundenen Codeelemente zu erhalten; die blauen Rechtecke stellen die Ergebnisse dar.

Fast alle Funktionen sind als static deklariert und daher nur innerhalb der Übersetzungseinheit sichtbar, in der sie deklariert wurden.

Ein Blick in den Quellcode des Linux-Kernels

Wechseln wir zum Linux-Quellcode und betrachten die folgende Funktionsimplementierung:

linux11

Der Code wirkt sehr sauber. Insbesondere die Funktion

  • besteht nur aus wenigen Codezeilen.
  • Ihre Signatur ist klar definiert.
  • Sie ist sinnvoll kommentiert.
  • Der Code ist sauber eingerückt.
  • Die Variablennamen sind sehr eindeutig.
  • Const-Correctness wird eingehalten.
  • Sie überprüft die Eingabeparameter und gibt eine Warnung aus, wenn bestimmte Bedingungen nicht erfüllt sind.

Ein anderer Entwickler könnte dieselbe Funktion beispielsweise so implementieren:

linux12

Der Coding-Stil hat großen Einfluss auf die Lesbarkeit des Quellcodes. Schon einige Stunden Entwicklerschulung und regelmäßige Code-Reviews können dazu beitragen, dass Code deutlich leichter zu warten und weiterzuentwickeln ist.

Untersuchen wir den Quellcode des Linux-Kernels mit CppDepend und entdecken wir einige grundlegende Coding-Regeln, die seine Entwickler anwenden.

Modularität

Modularität ist eine Softwaredesign-Technik, bei der Software stärker in voneinander getrennte Bestandteile gegliedert wird. Modularer Code lässt sich leichter verwalten und warten.

Bei einer prozeduralen Sprache wie C, die keine logischen Konstrukte wie Namespaces, Komponenten oder Klassen besitzt, lässt sich Modularität über Verzeichnisse und Dateien erreichen.

Dafür gibt es beispielsweise folgende Möglichkeiten:

  • Alle Quelldateien in einem einzigen Verzeichnis ablegen.
  • Dateien eines Moduls oder Untermoduls in einem eigenen Verzeichnis zusammenfassen.

Beim Linux-Kernel werden Verzeichnisse und Unterverzeichnisse verwendet, um den Kernel-Quellcode zu modularisieren.

linux15Kapselung

Bei der Kapselung werden Funktionen und Daten verborgen, die interne Details einer Implementierung darstellen. In C wird dies mit dem Schlüsselwort static erreicht. Solche Elemente werden als Funktionen bzw. Variablen mit Datei-Sichtbarkeit bezeichnet.

Suchen wir mit der folgenden CQLinq-Abfrage nach allen statischen Funktionen:

linux17

Mit der Metric View lässt sich gut erkennen, wie viele Funktionen betroffen sind. Dort wird die Codebasis als Treemap dargestellt. Treemapping visualisiert hierarchische Daten mithilfe ineinander verschachtelter Rechtecke. Eine CppDepend-Treemap verwendet die übliche Codehierarchie:

  • Projekte enthalten Verzeichnisse.
  • Verzeichnisse enthalten Dateien.
  • Dateien enthalten Structs, Funktionen und Variablen.

Die Treemap bietet eine praktische Möglichkeit, die Ergebnisse einer CQLinq-Abfrage darzustellen, sodass die betroffenen Codeelemente visuell erkennbar sind.

linux2

Wie zu sehen ist, sind viele Funktionen als static deklariert.

Suchen wir nun nach statischen Variablen:

linux3

Wie bei den Funktionen sind auch viele Variablen als static deklariert.

Im Quellcode des Linux-Kernels wird Kapselung immer dann eingesetzt, wenn Funktionen und Variablen ausschließlich innerhalb einer Datei sichtbar sein sollen.

Structs für das Datenmodell verwenden

In C verwenden Funktionen Variablen für ihre Verarbeitung. Dabei kann es sich handeln um:

  • Statische Variablen.
  • Globale Variablen.
  • Lokale Variablen.
  • Variablen aus Structs.

Jedes Projekt besitzt ein Datenmodell, das von zahlreichen Quelldateien verwendet werden kann. Globale Variablen sind eine Möglichkeit, doch in der Regel ist es besser, zusammengehörige Daten in Structs zu bündeln.

Suchen wir nach globalen Variablen mit einem primitiven Typ:

linux4

Nur wenige Variablen sind betroffen. Einige davon könnten möglicherweise in Structs zusammengefasst werden, etwa (elfcorehdr_addr und elfcorehdr_size) oder (pm_freezing und pm_nosig_freezing).

Funktionen kurz und übersichtlich halten

Auf der Webseite zum Linux-Coding-Style:

Functions should be short and sweet, and do just one thing.  They should
fit on one or two screenfuls of text (the ISO/ANSI screen size is 80x24,
as we all know), and do one thing and do that well.

The maximum length of a function is inversely proportional to the
complexity and indentation level of that function.  So, if you have a
conceptually simple function that is just one long (but simple)
case-statement, where you have to do lots of small things for a lot of
different cases, it's OK to have a longer function.

Suchen wir nach Funktionen mit mehr als 30 Codezeilen.

linux14

Nur wenige Funktionen umfassen mehr als 30 Codezeilen.

Anzahl der Funktionsparameter

Funktionen mit NbParameters > 8 können umständlich aufzurufen sein und unter Umständen die Performance beeinträchtigen. Eine Alternative besteht darin, eine eigene Struktur für die Übergabe der Argumente zu verwenden.

linux7

Nur zwei Funktionen haben mehr als acht Parameter.

Anzahl lokaler Variablen

Funktionen mit NbVariables > 8 können schwer verständlich und wartbar sein. Bei NbVariables > 15 sind sie äußerst komplex und sollten in kleinere Funktionen aufgeteilt werden, sofern sie nicht automatisch von einem Tool generiert wurden.

linux9

Nur fünf Funktionen besitzen mehr als 15 lokale Variablen.

Komplexe Funktionen vermeiden

Zur Identifizierung komplexer Funktionen stehen zahlreiche Metriken zur Verfügung. NBLinesOfCode, die Anzahl der Parameter und die Anzahl lokaler Variablen gehören zu den grundlegendsten.

Daneben gibt es weitere nützliche Metriken zur Erkennung komplexer Funktionen:

  • Die zyklomatische Komplexität ist eine verbreitete prozedurale Softwaremetrik und entspricht der Anzahl der möglichen Entscheidungspfade in einer Prozedur.
  • Nesting Depth misst die maximale Verschachtelungstiefe eines Gültigkeitsbereichs innerhalb eines Funktionskörpers.
  • Max Nested Loops entspricht der maximalen Verschachtelungstiefe von Schleifen innerhalb einer Funktion.

Welche Maximalwerte für diese Metriken akzeptabel sind, hängt weitgehend von den Präferenzen des Teams ab; universell gültige Grenzwerte gibt es nicht.

Suchen wir nach Funktionen, die Kandidaten für ein Refactoring sind:

linux8

Nur sehr wenige Funktionen können als komplex angesehen werden.

Namenskonventionen

Es gibt keine universelle Namenskonvention. Jedes Projekt kann diejenige wählen, die seinen Anforderungen am besten entspricht. Entscheidend ist vor allem, die gewählte Konvention konsequent anzuwenden.

Bei Linux müssen Struct-Namen mit einem Kleinbuchstaben beginnen. Mit der folgenden Abfrage können wir prüfen, ob dies im gesamten Kernel-Quellcode eingehalten wird:

linux5

Nur vier Structs beginnen mit „_“ statt mit einem Kleinbuchstaben.

Einrückung

Einrückung trägt wesentlich zur Lesbarkeit von Code bei. Die Gründe dafür werden auf der Webseite zum Linux-Coding-Style:

Rationale: The whole idea behind indentation is to clearly define where
a block of control starts and ends.  Especially when you've been looking
at your screen for 20 straight hours, you'll find it a lot easier to see
how the indentation works if you have large indentations.

Now, some people will claim that having 8-character indentations makes
the code move too far to the right, and makes it hard to read on a
80-character terminal screen.  The answer to that is that if you need
more than 3 levels of indentation, you're screwed anyway, and should fix
your program.
 Fazit

Die Untersuchung bekannter Open-Source-Projekte ist eine hervorragende Möglichkeit, die eigenen Programmierkenntnisse zu verbessern – insbesondere wenn diese Projekte von Experten entwickelt und gepflegt werden. Sie müssen das Projekt dafür nicht einmal herunterladen und kompilieren; es genügt, den Code auf GitHub zu durchsuchen.

Share this article