Dev Diary #4 (Teil 1 von 3) – September-Oktober 2018

Teil 1: Nicht Initialisierte Variablen

Hallo allerseits!

In diesem ‘Developer Diary’ möchten wir euch ein paar Einblicke in einige fundamentale Programmierprobleme geben, wie sich diese auf die Stabilität und Geschwindigkeit des Spiels auswirken, und welche Lösungswege wir anstreben um diese Probleme zu beseitigen. Es geht hier um relativ technische Themen, aber wir bemühen uns alles so zu umschreiben, dass auch Nicht-Programmierer folgen und einen Überblick erhalten können.
Für die Programmierer unter euch: Uns ist bewusst, dass wir hier einiges extrem vereinfachen, also habt bitte Nachsicht mit uns. Die Gilde 3 ist in C++ geschrieben, also setzt bitte eure C++-gefärbten Sonnenbrillen zum Lesen auf.

Wir haben diese Story in 3 Teile aufgeteilt. Dies ist nun der erste Teil: “Nicht initialisierte Variablen”.

Das Problem

Variablen erfüllen beim Programmieren einen ähnlichen Zwecken wie in der Mathematik (und falls ihr den Begriff kennt, dann wahrscheinlich von dort): Sie können einen bestimmten Wert aus einem Wertebereich annehmen, und in Berechnungen und genereller Verarbeitung den Zweck eines ‘Platzhalters’ für explizite Werte einnehmen.

In C++ benötigt jede Variable zwei Informationen:

  • Einen Typ. Dadurch erfährt der Compiler (das ist das Programm, welches den Text den wir als Quelltext eintippen, in tatsächlich ausführbare Dateien übersetzt) welchen Wertebereich eine Variable repräsentieren kann. Handelt es sich um eine Ganzzahl? Oder ein Textfragment? Oder vielleicht um einen ganzen Spielcharakter?
  • Einen Namen. Dieser Name wird dann verwendet um auf den tatsächlich abgelegten Wert zuzugreifen. Meistens wird etwas Kurzes, ähnlich wie in der Mathematik, gewählt (etwa ‘x’, ‘i’, etc…), oder ein beschreibender Name, wie etwa ‘spielCharakter’ oder ‘listeGeheimerGesellschaften’.<(li>

Wenn man nun die richtigen Anweisungen in den Quelltext eintippt kann man den Compiler dazu bewegen, ein kleines Stück Speicher zu reservieren und unter dem gewählten Namen verfügbar zu machen.

Wie ihr oben erkennen könnt, befindet sich in C++ ein Initialwert NICHT unter den zwingend benötigten Information. Man kann ohne weiteres eine Anweisung erstellen, um eine Ganzzahl unter dem Namen ‘meineGanzzahl’ zu erhalten, ohne ihr einen Wert zuzuweisen. Immerhin könnte es ja sein, dass es egal ist welchen Wert die Variable zu Beginn hat, da sie später ohnehin überschrieben wird. Man kann jedoch jederzeit den Wert einer Variablen auslesen, auch wenn sie noch keinen zugewiesen bekommen hat. Doch was erhält man dann? Manche Programmiersprachen weisen allen Variablen einen Standardwert zu – für Zahlen im Normalfall null. In C++ geschieht das jedoch nicht. Der Wert den man hier erhält wenn man von einer nicht initialisierten Variablen liest, ist was auch immer vorher im Speicher an der entsprechenden Stelle stand – effektiv ein zufälliger Wert. Ziemlich häufig wird man null erhalten, wodurch man sich in einem falschen Gefühl der Korrektheit wiegen kann.

Es gibt durchaus valide Gründe, wieso das in C++ so umgesetzt wurde; im Endeffekt gibt es in C++ ein Mantra: “Du bezahlst nicht für etwas, was du nicht verwendest.”, oder anders gesagt, die Programmiersprache wird keinen Extra-Code generieren, der die Laufzeit negativ beeinflusst.

Die Auswirkungen

Ultimativ bedeutet das, dass man in C++ sehr gut darauf aufpassen muss, alle Variablen zum richtigen Zeitpunkt zu initialisieren. Vergisst man das, führt dies zu extrem zufällig auftretenden Fehlern, die nur schwer aufzudecken sind. Beispiele aus Gilde 3 wären der “Disko”-Effekt, der vor einigen Patches im Winter aufgetreten ist, welcher durch eine nicht initialisierte Farbe für die Sonnenbeleuchtung verursacht wurde. Wir hatten deswegen auch mehrere Bugs die wir intern abfangen konnten, bevor wir einen Patch veröffentlicht haben; das Spiel dachte beispielsweise einmal, dass es keine männlichen Kinder mehr gibt.

Die Lösung

Was können wir gegen dieses Problem ausrichten? Zuerst einmal sollte man natürlich bei der Entwicklung großen Wert darauf legen, dass es gar nicht erst zu nicht initialisierten Variablen kommt. Es gibt Richtlinien und Regeln, die man befolgen sollte, und die dabei helfen so etwas zu vermeiden.

Um existierende Probleme aufzudecken gibt es hauptsächlich drei Optionen:

  • Manuelles Überprüfen. Immer wenn wir neuen Quelltext untersuchen, stellen wir sicher, dass alles richtig initialisiert wird. Das ist natürlich keine gute Option um flächendeckend Probleme im gesamten Code aufzudecken, da der Quelltext für Gilde 3 sehr umfangreich ist, und es daher zu lange dauern würde, alles von Hand zu überprüfen.
  • Statische Code Analyse. Das bedeutet, dass wir spezielle Hilfswerkzeuge einsetzen, die den Quelltext auf Probleme wie nicht initialisierte Variablen überprüfen (diese Hilfsprogramme sind auch nützlich um andere Fehler aufzudecken). Für die unter euch, die an diesem Thema mehr interessiert sind: Wir haben mit “clang tidy” gute Erfahrungen gemacht, es hat bereits einiges aufgedeckt (ihr könnt hier mehr über dieses Tool erfahren: http://clang.llvm.org/extra/clang-tidy/ )
  • “Memory fuzzing”. Diese Hilfsprogramme füllen den Speicher absichtlich mit zufallswerten bevor sie ein zu untersuchendes Programm darauf zugreifen lassen, wodurch der sehr häufige Fall aufgedeckt werden kann, bei dem der Speicher schon von Haus mit ‘0’ gefüllt ist. Valgrind ( http://valgrind.org/) wäre ein Beispiel für so ein Hilfsprogramm. Diesen Schritt haben wir (noch) nicht unternommen.

Falls euch unsere Erklärung mehr verwirrt als geholfen hat, dann können wir uns auf diesen Wikipedia Artikel verweisen, der sich dieser Thematik sehr verständlich annimmt: https://de.wikipedia.org/wiki/Variable_(Programmierung)#Initialisierung (ausführlicher in der Englischen Wikipedia: https://en.wikipedia.org/wiki/Uninitialized_variable )

Wir werden diese Story aus dem Entwicklertagebuch bald mit „Speicherlecks (Memory Leaks)“ fortsetzen. Bleibt dran.