Wann ist Code bewundernswert?



Das Thema idealer Code wird von erfahrenen Programmierern häufig kontrovers diskutiert. Umso interessanter war es, die Meinung von Parallels RAS-Entwicklungsdirektor Igor Marnat einzuholen. Unter dem Schnitt die Meinung seines Autors zum erklärten Thema. Viel Spaß!



Als Einführung möchte ich auf die Frage eingehen, warum ich beschlossen habe, diesen kurzen Artikel zu schreiben. Bevor ich es schrieb, stellte ich mehreren Entwicklern eine Frage aus dem Titel. Ich habe mehr als fünf Jahre mit den meisten Jungs gearbeitet, mit einigen etwas weniger, aber ich vertraue ihrer Professionalität und Erfahrung bedingungslos. Alle Erfahrung in der industriellen Entwicklung seit mehr als zehn Jahren, jeder arbeitet in russischen und internationalen Unternehmen, Softwareherstellern.

Einige Kollegen fanden es schwierig zu antworten (manche denken immer noch), andere nannten ein oder zwei Beispiele gleichzeitig. Für diejenigen, die Beispiele gaben, stellte ich eine klärende Frage: "Was hat diese Bewunderung tatsächlich verursacht?" Die Antworten stimmten mit den Ergebnissen der nächsten Phase meiner kleinen Forschung überein. Ich suchte im Internet nach Antworten auf diese Frage in verschiedenen Formulierungen nahe dem Titel des Artikels. Alle Artikel antworteten ungefähr so, wie meine Kameraden geantwortet hatten.

Die Antworten der Entwickler sowie der Wortlaut der gefundenen Artikel bezogen sich auf die Lesbarkeit und Struktur des Codes, die Eleganz logischer Konstruktionen, die Verwendung aller Merkmale moderner Programmiersprachen und die Befolgung eines bestimmten Designstils.

Als ich mir die Frage nach dem "göttlichen Code" stellte, tauchte die Antwort sofort aus dem Unterbewusstsein auf. Ich dachte sofort an zwei Codebeispiele, mit denen ich lange Zeit (vor mehr als zehn Jahren) gearbeitet hatte, aber ich fühle immer noch ein Gefühl der Bewunderung und Ehrfurcht. Nachdem ich die Gründe für die Bewunderung für jeden von ihnen berücksichtigt hatte, formulierte ich mehrere Kriterien, die unten diskutiert werden. Ich werde nebenbei auf das erste Beispiel eingehen, aber ich möchte das zweite genauer analysieren. Übrigens werden in unterschiedlichem Maße alle diese Kriterien im Entwicklerhandbuch " Perfect Code " von Steve McConnell berücksichtigt, aber dieser Artikel ist merklich kürzer.

90er Beispiel


Das erste Beispiel, das ich erwähnen werde, bezieht sich auf die Implementierung des v42bis-Modemprotokolls. Dieses Protokoll wurde in den späten 80ern - frühen 90ern entwickelt. Eine interessante Idee, die von den Entwicklern des Protokolls verkörpert wird, ist die Implementierung der Stromkomprimierung von Informationen während der Übertragung über eine instabile (Telefon-) Kommunikationsleitung. Der Unterschied zwischen Stream-Komprimierung und Dateikomprimierung ist grundlegend. Beim Komprimieren von Dateien kann der Archivierer den Datensatz vollständig analysieren, den optimalen Ansatz für die Datenkomprimierung und -codierung bestimmen und die gesamten Daten in die Datei schreiben, ohne sich um mögliche Daten- und Metadatenverluste sorgen zu müssen. Beim Entpacken ist der Datensatz wiederum wieder vollständig zugänglich, die Integrität wird durch eine Prüfsumme sichergestellt. Bei der Inline-Komprimierung kann der Archivierer nur auf ein kleines Datenfenster zugreifen. Es gibt keine Garantie für keinen Datenverlust. Die Notwendigkeit, die Verbindung neu zu installieren und den Komprimierungsprozess zu initialisieren, ist häufig.

Die Autoren des Algorithmus fanden eine elegante Lösung, deren Beschreibung buchstäblich mehrere Seiten umfasst . Viele Jahre sind vergangen, aber ich bin immer noch beeindruckt von der Schönheit und Anmut des von den Entwicklern des Algorithmus vorgeschlagenen Ansatzes.

Dieses Beispiel bezieht sich immer noch nicht auf den Code als solchen, sondern auf den Algorithmus, daher werden wir nicht näher darauf eingehen.

Linux ist der Kopf von allem!


Ich möchte das zweite Beispiel eines perfekten Codes genauer analysieren. Dies ist der Linux-Kernel-Code. Der Code, der zum Zeitpunkt des Schreibens den Betrieb von 500 Supercomputern aus den Top 500 steuert, der Code, der auf jedem zweiten Telefon der Welt ausgeführt wird und der die meisten Server im Internet steuert.

Betrachten Sie beispielsweise die Datei memory.c aus dem Linux-Kernel , die zum Speicherverwaltungssubsystem gehört.

1. Quellen sind leicht zu lesen. Sie sind in einem sehr einfachen Stil geschrieben, der leicht zu befolgen und schwer zu verwechseln ist. Großbuchstaben werden nur für Präprozessoranweisungen und Makros verwendet, alles andere wird in kleinen Buchstaben geschrieben, Wörter in den Namen werden durch Unterstriche getrennt. Dies ist vielleicht der einfachste Codierungsstil, der möglich ist, außer dass überhaupt kein Stil vorhanden ist. Gleichzeitig ist der Code perfekt lesbar. Der Ansatz zum Einrücken und Kommentieren ist von jedem Teil einer Kernel-Datei aus sichtbar, zum Beispiel:

static void tlb_remove_table_one(void *table) {     /*      * This isn't an RCU grace period and hence the page-tables cannot be      * assumed to be actually RCU-freed.      *      * It is however sufficient for software page-table walkers that rely on      * IRQ disabling. See the comment near struct mmu_table_batch.      */     smp_call_function(tlb_remove_table_smp_sync, NULL, 1);     __tlb_remove_table(table); } 


2. Der Code enthält nicht zu viele Kommentare, aber diejenigen, die normalerweise nützlich sind. Sie beschreiben in der Regel nicht eine Aktion, die bereits aus dem Code ersichtlich ist (ein klassisches Beispiel für einen nutzlosen Kommentar ist „cnt ++; // Inkrementzähler“), sondern den Kontext dieser Aktion - warum hier getan wird, was getan wird, warum es getan wird, warum hier, mit welchen Annahmen es verwendet wird, mit welchen anderen Stellen im Code es verbunden ist. Z.B:

 /** * tlb_gather_mmu - initialize an mmu_gather structure for page-table tear-down * @tlb: the mmu_gather structure to initialize * @mm: the mm_struct of the target address space * @start: start of the region that will be removed from the page-table * @end: end of the region that will be removed from the page-table * * Called to initialize an (on-stack) mmu_gather structure for page-table * tear-down from @mm. The @start and @end are set to 0 and -1 * respectively when @mm is without users and we're going to destroy * the full address space (exit/execve). */ void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,            unsigned long start, unsigned long end) 


Eine andere Verwendung von Kommentaren im Kernel besteht darin, den Verlauf von Änderungen zu beschreiben, normalerweise am Anfang einer Datei. Die Geschichte des Kernels gibt es seit fast dreißig Jahren und es ist nur interessant, einige Orte zu lesen. Sie fühlen sich als Teil der Geschichte:

 /* * demand-loading started 01.12.91 - seems it is high on the list of * things wanted, and it should be easy to implement. - Linus */ /* * Ok, demand-loading was easy, shared pages a little bit tricker. Shared * pages started 02.12.91, seems to work. - Linus. * * Tested sharing by executing about 30 /bin/sh: under the old kernel it * would have taken more than the 6M I have free, but it worked well as * far as I could see. * * Also corrected some "invalidate()"s - I wasn't doing enough of them. */ 


3. Der Kernel-Code verwendet spezielle Makros, um Daten zu überprüfen. Sie werden auch verwendet, um den Kontext zu überprüfen, in dem der Code funktioniert. Die Funktionalität dieser Makros ähnelt der Standardzusicherung, mit dem Unterschied, dass der Entwickler die ausgeführte Aktion überschreiben kann, wenn die Bedingung erfüllt ist. Ein allgemeiner Ansatz zur Datenverarbeitung im Kernel: Alles, was aus dem Benutzerbereich kommt, wird überprüft. Bei fehlerhaften Daten wird der entsprechende Wert zurückgegeben. In diesem Fall kann WARN_ON verwendet werden, um einen Datensatz an das Kernelprotokoll auszugeben. BUG_ON ist normalerweise sehr nützlich, wenn Sie neuen Code debuggen und den Kernel auf neuen Architekturen starten.

Das BUG_ON-Makro bewirkt normalerweise, dass der Inhalt der Register und des Stapels gedruckt wird, und stoppt entweder das gesamte System oder den Prozess, in dessen Kontext der entsprechende Aufruf stattgefunden hat. Das WARN_ON-Makro gibt einfach eine Nachricht an das Kernel-Protokoll aus, wenn die Bedingung erfüllt ist. Es gibt auch Makros WARN_ON_ONCE und eine Reihe anderer, deren Funktionalität sich aus dem Namen ergibt.

 void unmap_page_range(struct mmu_gather *tlb, ….     unsigned long next;    BUG_ON(addr >= end);    tlb_start_vma(tlb, vma); int apply_to_page_range(struct mm_struct *mm, unsigned long addr, …    unsigned long end = addr + size;    int err;    if (WARN_ON(addr >= end))        return -EINVAL; 


Der Ansatz, bei dem Daten aus unzuverlässigen Quellen vor der Verwendung überprüft werden und die Reaktion des Systems auf „unmögliche“ Situationen vorgesehen und bestimmt wird, vereinfacht das Debuggen und den Betrieb des Systems erheblich. Sie können diesen Ansatz als Implementierung des Fail-Early- und Laute-Prinzips betrachten.

4. Alle Hauptkomponenten des Kernels versorgen Benutzer über eine einfache Schnittstelle, das virtuelle Dateisystem / proc /, mit Informationen über ihren Status.

Beispielsweise sind Speicherstatusinformationen in der Datei / proc / meminfo verfügbar

 user@parallels-vm:/home/user$ cat /proc/meminfo MemTotal:    2041480 kB MemFree:      65508 kB MemAvailable:   187600 kB Buffers:      14040 kB Cached:      246260 kB SwapCached:    19688 kB Active:     1348656 kB Inactive:     477244 kB Active(anon):  1201124 kB Inactive(anon):  387600 kB Active(file):   147532 kB Inactive(file):  89644 kB …. 


Die obigen Informationen werden in mehreren Quelldateien des Speicherverwaltungssubsystems gesammelt und verarbeitet. Das erste MemTotal-Feld ist also der Wert des Totalram-Felds der Sysinfo-Struktur, das mit der Funktion si_meminfo der Datei page_alloc.c gefüllt ist.

Das Organisieren der Erfassung, Speicherung und der Zugriff des Benutzers auf solche Informationen erfordert natürlich den Aufwand des Entwicklers und einen gewissen Aufwand für das System. Gleichzeitig sind die Vorteile eines bequemen und einfachen Zugriffs auf solche Daten sowohl im Entwicklungsprozess als auch im Betrieb des Codes von unschätzbarem Wert.

Die Entwicklung fast jedes Systems sollte mit einem System zum Sammeln und Bereitstellen von Informationen über den internen Status Ihres Codes und Ihrer Daten beginnen. Dies wird im Entwicklungs- und Testprozess und später im Betrieb sehr hilfreich sein.

Wie Linus sagte : „Schlechte Programmierer sorgen sich um den Code. Gute Programmierer sorgen sich um Datenstrukturen und ihre Beziehungen. "

5. Der gesamte Code wird vor dem Festschreiben von mehreren Entwicklern gelesen und diskutiert. Ein Verlauf der Quellcodeänderungen wird aufgezeichnet und ist verfügbar. Änderungen an einer Zeile können auf ihr Auftreten zurückgeführt werden - was hat sich geändert, von wem, wann, warum, welche Probleme wurden von den Entwicklern besprochen. Zum Beispiel wird die Änderung in https://github.com/torvalds/linux/commit/1b2de5d039c883c9d44ae5b2b6eca4ff9bd82dac#diff-983ac52fa16631c1e1dfa28fc593d2ef im Code memory.c von httbb. Eine kleine Optimierung des Codes wurde vorgenommen (ein Aufruf zum Aktivieren des Speicherschreibschutzes erfolgt nicht, wenn der Speicher bereits schreibgeschützt ist).

Für einen Entwickler, der mit Code arbeitet, ist es immer wichtig, den Kontext um diesen Code herum zu verstehen, mit welchen Annahmen der Code erstellt wurde, was und wann er geändert wurde, um zu verstehen, welche Szenarien von den Änderungen betroffen sein könnten, die er vornehmen wird.

6. Alle wichtigen Elemente des Kernel-Code-Lebenszyklus sind dokumentiert und zugänglich , beginnend mit dem Codierungsstil und endend mit dem Inhalt und dem Zeitplan für die Veröffentlichung stabiler Kernel-Versionen . Jeder Entwickler und Benutzer, der in der einen oder anderen Funktion mit dem Kernel-Code arbeiten möchte, verfügt über alle erforderlichen Informationen.

Diese Momente schienen mir wichtig zu sein, sie bestimmten meine Begeisterung für den Kerncode. Offensichtlich ist die Liste sehr kurz und kann erweitert werden. Die oben aufgeführten Punkte beziehen sich meiner Meinung nach jedoch auf Schlüsselaspekte des Lebenszyklus eines Quellcodes aus Sicht des Entwicklers, der mit diesem Code arbeitet.

Was ich abschließend sagen möchte. Die Kernentwickler sind klug und erfahren, sie haben es geschafft. Von Milliarden von Linux-Geräten bewiesen

Seien Sie als Kernel-Entwickler, verwenden Sie Best Practices und lesen Sie Code Complete!

Z.Y. Welche Kriterien für einen idealen Code haben Sie übrigens persönlich? Teilen Sie Ihre Gedanken in den Kommentaren.

Source: https://habr.com/ru/post/de426679/


All Articles