Warum programmiert ihr in C und was?
-
Andromeda schrieb:
Eine C-konformer Heap
Nee, schlussaus, pfui. Nix C-konform.
Ich will ein Objekt haben, dem ich angeben kann:
- ob das jetzt eine Stack- oder eine Mapping-Allokation ist - und ich will die Möglichkeit haben, dass ab einer bestimmten Größe (z.B. eine Stanardpage) automatisch nur noch Mappings erlaubt sind.
- wie groß die Pages sein sollen, um weniger Einträge im TLB zu haben.
- dass ich jetzt neuen Speicher brauche und dass es mir komplett egal ist dabei, wo der neue Speicherbereich liegt, weil ich mit relativen Indizes arbeite.Was ich NICHT will, ist mit Zeigern rumhantieren, wofür jedes Mal erst mal eine Liste gelockt werden muss, um dann nach dem Eintrag für den Zeiger zu suchen. Ich habe hier mein Mapping-Objekt auf dem Stack, das Mapping selbst gibt mir der Kernel, und große, zusammenhängende Daten kann ich einfach direkt reinpacken.
Dieser ganze C-konforme Anspruch mag für ein paar Systeme Sinn ergeben, aber lange nicht für alle. Selbst wenn alles gut läuft bei einem
malloc
- sprich, Daten müssen nicht kopiert werden, neuer Speicher muss nicht angefordert werden, also Zero-Copy und ohne Kontextwechsel - so muss doch noch immer in der globalen Liste eingetragen werden, dass Adresse X bis Y jetzt belegt sind.Für kleine Allokationen kann man direkt den Stack benutzen, und derzeit teste ich Code, mit dem man den auch wieder zurücksetzen kann. Für größere Allokationen ruft man das Betriebssystem an oder verwendet optionale Mapping-Objekte bei Funktionsaufrufen. Wenn man keine hat, kümmert sich die Funktion selbst um ihren Speicher. Alles eine Frage des Frameworks, nicht der Sprache. Der zusätzliche Code, damit eine Funktion so ein Objekt nutzen kann, ist ein zusätzlicher Parameter, ein Variablenblockmakro, ein Funktionsaufruf, dem die Standardargumente über ein Makro mitgegeben werden (sprich, im normalen Code werden dann keine Argumente übergeben), und entweder das Zurücksetzen oder das Freigeben des Mapping-Objekts. Insgesamt vielleicht 5 Zeilen mehr Code pro Funktion, im Gegenzug kann dann reservierter Speicher mehrmals verwendet und auch erweitert werden.
Und interessant wird es, wenn man in dem gleichen Mapping, in dem man seine Input-Daten hat, auch seine Output-Daten speichern will, nur halt am Ende. Bonuspunkte, wenn man 64-Bit-Längen verarbeitet haben will, die ranzige Bibliothek aber nur direkt mit 32 Bit arbeiten kann (du brauchst gar nicht so unschuldig zu gucken, zlib). Dann muss man konsequent relative Indizes verwenden. Die Belohnung? Man hat nicht mehr diesen Verwaltungskram im Hintergrund, alles läuft direkt für deinen Thread und muss nicht gelockt werden, für kleinere Allokationen wird erst Speicher auf dem Stack verwendet, bevor das Betriebssystem angefragt wird, und man kann direkt bestimmen, wie der Speicher reserviert werden soll.
(EDIT): Und ich will dabei noch den Code, den ich dann hinterher schreiben muss, reduziert haben. Wenn ich am Ende mehr Code schreiben muss, um Kinkerlitzchen zu verwalten, dann habe ich das Ziel verfehlt. Das soll gleichzeitig intelligent UND einfach UND zentral sein, damit man es im Zweifelsfall auch an seine Situation anpassen kann. Ich gebe einen Pagesize von 1 GiB an, das kann der Prozessor aber nicht, dann soll das Ding automatisch runtergehen. bis es klappt.
SOWAS will ich. Gab es nicht. Also musste ich es selbst machen. Und plötzlich dauert ein Funktionsaufruf nur noch 500 Cycles statt 200.000, sowohl in C als auch in C++.
Es wird nicht besser, indem man versucht, "C-konform" Algorithmen zu tweaken. Aber schlecht programmieren kann man in jeder Programmiersprache, wa?
-
dachschaden schrieb:
SOWAS will ich. Gab es nicht. Also musste ich es selbst machen. Und plötzlich dauert ein Funktionsaufruf nur noch 500 Cycles statt 200.000, sowohl in C als auch in C++.
Du willst die eierlegende Wollmilchsau-Speicherverwaltung. Prinzipiell sowas wie die Quadratur des Kreises. Da das leider unmöglich ist, kommen in vielen größeren MCU-Projekten mehrere Allokatoren zum Einsatz. Das Thema hat Potential für eine Doktorarbeit: "Efficient use of RAM in memory-constraint systems running multiple applications on a preemptive time-shared basis", oder so.
-
Andromeda schrieb:
Du willst die eierlegende Wollmilchsau-Speicherverwaltung. Prinzipiell sowas wie die Quadratur des Kreises. Da das leider unmöglich ist,
Quatsch mit Soße, ich hab' eine Speicherverwaltung geschrieben, die genau das macht, was ich will. Da ist nichts unmöglich.
Ähh, nein, stimmt nicht ganz. Zero-Copy auf Windows ist unmöglich, weil es kein
VirtualReAlloc
oder dergleichen gibt. In der Basic-Implementierung macht der Allokator dann halt auf Linux und Windows eine Kopie der Daten. In dem etwas abstrakterien Layer wird unter Linux aber stattdessenmremap
verwendet, das kann das ohne Kopie.Andromeda schrieb:
Das Thema hat Potential für eine Doktorarbeit: "Efficient use of RAM in memory-constraint systems running multiple applications on a preemptive time-shared basis", oder so.
Find ich nicht. Aber ich habe, glaube ich, auch einfach andere Ansprüche. Wenn bei mir was nicht das tut, was es soll, dann sage ich "nicht mir mir" und gehe dem auf den Grund.
-
dachschaden schrieb:
Quatsch mit Soße, ich hab' eine Speicherverwaltung geschrieben, die genau das macht, was ich will. Da ist nichts unmöglich.
Okay, nehmen wir mal an, dass einige Anwendungen zusammen etwa bis zu 80% des Speichers belegen, aber niemals eine Ablehnung einer Speicheranforderung sehen dürfen, da sonst die Funktion des Gesamtsystems gefährdet wäre. Wie würdest du das handlen?
-
Andromeda schrieb:
Okay, nehmen wir mal an, dass einige Anwendungen zusammen etwa bis zu 80% des Speichers belegen, aber niemals eine Ablehnung einer Speicheranforderung sehen dürfen, da sonst die Funktion des Gesamtsystems gefährdet wäre. Wie würdest du das handlen?
Was hat das mit den Anforderungen zu tun, die ich aufgezählt habe? Dein Kommentar der Wollmilchsau bezog sich (wie ich das verstanden habe) auf meine Anforderungen, und die habe ich erfüllt. Deswegen ist es Quatsch mit Soße, wenn du sagst, dass ich meine Speicherverwaltung, so wie ich sie haben will, nicht implementieren kann, wenn ich sie so implementiert habe, wie ich es will. Meine Anforderung ist nicht, dass Allokationen immer erfolgreich sind, selbst wenn der Platz knapp wird. Und das wird durch Hugepages nur noch schwerer, weil die größere freie Speicherblöcke benötigen, die mit der Fragmentierung des (physikalischen) Speichers immer weniger werden. Und selbst wenn es meine Anforderung wäre, dann ist das immer noch ein Problem des Kernels (Stichwort "Overcommit").
Es gäbe einige Techniken auf Systemen, die Paging unterstützen, um eine derartige Situation zu verhindern - swappen oder overcommiten. Irgendwann sind auch die ausgereizt. Wenn der Speicher dann knapp wird, muss die Anwendung selbst halt ordentlich handeln, dass sie jetzt keinen Speicher bekommt.
Und Systeme, die kein Paging können, habe ich schon in vorherigen Posts exkludiert.
-
dachschaden schrieb:
Wenn der Speicher dann knapp wird, muss die Anwendung selbst halt ordentlich handeln, dass sie jetzt keinen Speicher bekommt.
Und genau das ist z.B. in echtzeitfähigen Systemen ein Riesenproblem. Deshalb bin ich überzeugt davon, dass maximale Verfügbarkeit letztlich nur durch direkten Speicherzugriff zu erreichen ist und nicht durch irgendwelche Zwischensoftware, die Pages umsortiert und Festplatte/Flash als Buffer für RAM benutzt.
Große OS (zum Beispiel Windows) treiben es auf die Spitze, indem sie sogar Code einer Anwendung auf die Festplatte auslagern. Das hat dann zur Folge, dass ein selten genutzter Befehl bzw. Menupunkt zu mehreren Sekunden Wartezeit führt. Sogar das OS selbst friert manchmal einfach mal kurz ein, wenn du nur 4GB RAM hast.
-
dachschaden schrieb:
Ich habe hier gerade ein C++-Binary rumfliegen, welches so an das halbe Mebibyte groß ist. Verwendet Boost. Da sind so vielleicht 2 Funktionen von 100, die tatsächlich vom Programmierer kommen, der Rest kommt von Boost oder iostream oder macht string-Konvertierungen oder weiß der Geier. Würde mal so schätzen, mindestens 80% sind nur ctor/dtor-Aufrufe und ein bisschen Stack Unwinding.
Ok...
Das gleiche Problem haben wir bei C auch. Dieses ganze Konstrukt um malloc mag für Embedded-Systeme (wo z.B. alle Programme in Ring 0 laufen und überall lesen und schreiben können und sich alle Programme einen zentralen Heap teilen) vielleicht noch Sinn ergeben. Aber für einen großen Teil der Programme gilt das nicht.
Nur mal eine Vermutung:
Kann es sein, dass irgentein Java Jüngling ein Projekt in C programmiert hat, sein Code mit malloc() verseucht hat, du das Projekt übernommen hast, festgestellt hast, dass das malloc() viel Zeit frisst und deswegen eine neue malloc() Implementierung gemacht hat?
dachschaden schrieb:
Und wenn ich all diesen Scheiß wegnehme und mir mein eigenes Interface zusammenbastel, in dem ich auf Feature-Creep-Scheiß verzichte und mir lieber die Möglichkeit gebe, Page-Walks zu vermeiden, und dabei noch auf 32 KiB-Binaries komme, mit einem Verhältnis von 35/1 LOC (eine LOC in der Anwendung kommt auf 35 LOC in der Bibliothek), dann weiß ich, dass ich was richtig gemacht habe.
Entschuldigung dass ich das mal sage, aber für meinen Geschmack solltest du dein aufbrausendes Temperament mal zügeln. Gerade bei der Programmierung steckt der Teufel im Detail, die so manche voreilige Lösung scheitern lässt.
Was juckt mich ehrlich gesagt die Binary Größe? Was? Wenn es dir um Performance geht, dann suche dir die Bottlenecks mittels Profiler oder anderem und optimiere diese. Und wenn jemand O(n^2) mal malloc() auffruft, dann würde ich erst mal überprüfen ob dies überhaupt notwendig ist!
Und wenn 80% von 512 kByte Konstruktor, Destruktor Aufrufe sind, dann würde ich das erst einmal mittels der Map File überprüfen. Und wenn das stimmt, dann muss der Quellcode vor new() Aufrufen überquellen. Also würde mir den Verantwortlichen schnappen und ihm gehörig den Marsch blasen. Denn das komplette Programm-Design ist kaputt. Und dann hilft nur eins: neuprogrammieren!
-
Bitte ein Bit schrieb:
Kann es sein, dass irgentein Java Jüngling ein Projekt in C programmiert hat, sein Code mit malloc() verseucht hat, du das Projekt übernommen hast, festgestellt hast, dass das malloc() viel Zeit frisst und deswegen eine neue malloc() Implementierung gemacht hat?
Wie kommst du denn auf diese Idee?
Nein, das hat nix mit Java zu tun, eher mit Perl.
Vor vielen Monden habe ich mal einen Webcrawler geschrieben. Wer Perl kennt, weiß, dass der Threading-Support echt grausam ist (oder zumindest war, als ich den geschrieben habe), und so nach 25 "Threads" und vier Stunden Laufzeit war der Arbeitsspeicher ohne Not komplett voll. Das war damals noch eine 16-GiB-Maschine ... ich weiß bis heute nicht, warum Perl so viel Speicher verbraucht hat, und mir ist es auch egal.Und als ich das in C nachgeschrieben habe, wurde das Laufzeitverhalten zwar immer besser, aber mit der Zeit tauchten weitere Flaschenhälse auf, wenn richtig viele Threads laufen und Speicher anfordern.
Irgendwann hatte ich das dann auch gefixt. Und dann sollte der Code noch in zig anderen Programmen einfließen. Und dann musste ich intelligent abstrahieren.
Bitte ein Bit schrieb:
Entschuldigung dass ich das mal sage, aber für meinen Geschmack solltest du dein aufbrausendes Temperament mal zügeln. Gerade bei der Programmierung steckt der Teufel im Detail, die so manche voreilige Lösung scheitern lässt.
Da du nicht wissen kannst, wie das Interface aussieht, dass ich geschrieben habe, kannst du gar nicht beurteilen, ob ich eventuelle Details außer Acht gelassen habe.
Ich bin bei einigen Sachen fett auf's Maul geflogen, aber habe auch etliche Erkenntnisse daraus gezogen.
Bitte ein Bit schrieb:
Was juckt mich ehrlich gesagt die Binary Größe? Was?
Noch nie ein Programm disassembliert, weil der Quellcode nicht (mehr) vorhanden war?
Wenn du halt keine Programme reverse-engeineerst, dann braucht dich das nicht kümmern, stimmt schon. Genau genommen brauchen dich dann auch Optimierungen nicht zu kümmern, solange es nur schnell genug ist.
Nur wenn du eine zentrale Speicherverwaltungslib hast, über die du tausend tolle Sachen machen willst, dann kümmert es dich vielleicht. Weil der Code dann nicht nur Programm A berührt, sondern auch Programm B - Z.
Bitte ein Bit schrieb:
Wenn es dir um Performance geht, dann suche dir die Bottlenecks mittels Profiler oder anderem und optimiere diese. Und wenn jemand O(n^2) mal malloc() auffruft, dann würde ich erst mal überprüfen ob dies überhaupt notwendig ist!
Das Überprüfen, ob es notwendig ist, ist doch gar nicht das Problem!
Vor kurzem habe ich mal einen Blick in XFE geworfen, ein "leichtgewichtiger" Dateimanager auf Linux, der halt seine Display-Verbindungen nicht richtig geschlossen hat, wenn ein Subprozess gestartet werden sollte, und irgendwann hat X dann gestreikt und keine weiteren Verbindungen aufgemacht, und weil der Code darauf nicht prüfte, ist der Manager dann halt gestorben.Warum ich das erzähle? Weil das Herrausfinden des Fehlers gar nicht mal schwer war - beim Starten des Programms holt sich die Anwendung bereits eine X-Verbindung, die kann dann überall durchgereicht werden. Aber wir reden hier von einem Zeiger auf eine Struct, die eine X-Verbindung exportiert. Bei meinem Interface ist es möglich, dass TCP-Payload, entchunkter HTTP-Content und decodierter HTTP-Content im gleichen Speicherblock liegen, und dann soll noch was in den freien Speicher geschrieben werden, den wir am Ende haben. Und wenn wir ein
realloc
machen, zerstört das absolute Pointer auf bestimmte Datensektionen.Und sowas taucht dann bis zu 35 Mal in verschiedenen Programmen auf - von denen ich weiß. Das Problem sind dann nicht nur die Flaschenhälse, sondern auch, den Code wartbar zu halten. Und dafür möchtest du so viel Code wie zentral haben.
Bitte ein Bit schrieb:
Und wenn 80% von 512 kByte Konstruktor, Destruktor Aufrufe sind, dann würde ich das erst einmal mittels der Map File überprüfen. Und wenn das stimmt, dann muss der Quellcode vor new() Aufrufen überquellen. Also würde mir den Verantwortlichen schnappen und ihm gehörig den Marsch blasen. Denn das komplette Programm-Design ist kaputt. Und dann hilft nur eins: neuprogrammieren!
Ich weiß nicht, in welcher Welt du lebst, aber in meiner Welt hat man diesen Luxus nicht unbedingt. Weil der Programmierer längst das Weite gesucht hat, oder eh nie zur Verfügung stand. Und warum sollte man überhaupt in Betracht ziehen, das Programm neuzuschreiben? Vom Design her ist doch nichts zu beanstanden hier!
Und die Programmierer, die ich kenne, lassen das dann auch so stehen.
-
das mit deiner selbstgeschriebenen speicherverwaltung klingt sehr interessant. hast du da zufällig eine doku (für dich) geschrieben die ich lesen könnte?
ich würd gern die internen unterschiede und vorteile deiner variante gegenüber malloc verstehen. um ehrlich zu sein, weiß ich selbst nicht wie malloc intern arbeitet (es tut halt), so das ich da auch noch was lernen konnte.wenn du den code/doku für dich behalten willst kann ich das auch verstehen, kein problem.
-
tenim schrieb:
um ehrlich zu sein, weiß ich selbst nicht wie malloc intern arbeitet (es tut halt), so das ich da auch noch was lernen konnte.
Im Prinzip arbeitet malloc auf Basis verketteter Listen. Es zerstückelt einen größeren Speicherblock in kleine Schnipsel. Hier ein paar Beispiele: http://www.rtos.be/2014/05/open-source-implementations-of-malloc/