Pointer wegen möglichen Stackoverflow?
-
Hallo zusammen,
auf der Arbeit habe ich einen neuen Kollegen bekommen. Dieser lehrt sogar als Dozent an einer Uni. Ich hab mich mit ihm etwas unterhalten und er hat mir erzählt, dass er fast alle Objekte/Instanzen immer mit Pointer macht. Natürlich mit Smart Pointern damit kein Speicherleck entsteht. Er hat es hauptsächlich damit begründet, dass man damit einen Stackoverflow verhindert. Wenn eine Klasse mehrere Member hat oder andere Klassen als Member hat, wäre das schon groß aus seiner Sicht. Außerdem macht man ja in anderen Programmiersprachen auch Objekte per new. An manchen Stelle in seinem Code existieren Objekte sogar nur in einer Funktion für mehrere Zeilen und werden trotzdem per new und Smart Pointer erzeugt.
Ich hab mal vor Ewigkeiten den Stack herausgefordert und einen kleinen Test gemacht. Damals habe ich einen vektor erstellt und viele Objekte einer von einer unseren Klassen gemacht, die extrem viel Vererbung und Member hat. Ich glaube bei ca. 10 Millionen Objekte im Vektor hatte ich einen Stackoverflow.
Jetzt ist meine Frage was an seiner These wirklich dran ist. Ich hab gelesen und halte daran fest, dass ich Objekte nur mache, wenn ich die Lebensdauer länger haben muss, weil unser alten Compiler auf der Arbeit die neuen Features nicht unterstützt und ich auf Zeiger ausweichen muss oder weil die Klassen kein Move erlauben und/oder ein Kopieren nicht möglich/sinnvoll ist. In meinem Code werden ca. 99% aller Objekte nicht durch new erzeugt.
-
Es ist nicht Aufgabe des Benutzers, sich darum zu kümmern. Ein Objekt, das große Datenmengen hält, hat sich selber darum zu kümmern, dass diese nicht direkt im Objekt liegen. Ist aber auch gar nicht so schwer, sich darum zu kümmern, denn alles was im Objekt selbst liegt, muss ja als Membervariable im Objekt so definiert sein. Jedem sollten die Alarmglocken schrillen, wenn man ein
int my_array[9999999];
als Membervariable hat. Natürlich ebenso, wenn man ein solches Konstrukt auf Ebene eines Codeblocks hat.
Dein Kollege erreicht also wenig, außer seinen Code ineffizient zu machen. Der Code ist zugegebenermaßen robuster gegenüber Designfehlern in den benutzten Objekten, aber will man das? Will man nicht eher keinen schlechten Code benutzen?
Der Stack kann auch überlaufen, wenn man eine sehr tiefe Rekursion hat, aber dann helfen auch Pointerindirektionen nicht. Und wieder gilt, wenn man im echten Leben außerhalb einer Übungsaufgabe eine tiefe Rekursion hat, macht man schon was anderes falsch.
Es gibt Grenzfälle bei irgendwelchen C-VLAs, die zwar selbst in C ausgemustert wurden, aber die ein C++ Compiler als Erweiterung zulassen könnte. Damit kann man allerlei Dummheiten treiben. Aber da ist es erst recht Aufgabe des VLA Nutzers, korrekt mit diesen umzugehen. Denn Nutzung als Stackvariable ist zwar möglich, aber nicht wofür die gedacht sind/waren. In C gab/gibt es auch einen (später standardisierten) Hack, namens Flexible Array Members, aber auch da ist das erst einmal kein C++, und es gilt erst Recht, dass derjenige der diese im Struct benutzt in der Verantwortung ist, und nicht derjenige, der das Struct benutzt.
Außerdem macht man ja in anderen Programmiersprachen auch Objekte per new.
Es ist meiner Meinung nach DER große Grund, C++ zu nutzen, dass man da ein vernünftiges Konzept für deterministisches Verhalten lokaler Ressourcen hat, und somit kein new und/oder Garbage Collector braucht. Wenn man das nicht nutzt, hat man quasi Java, und keinen Grund, nicht direkt Java zu schreiben.
-
Hallo @KK27 ,
Ob Pointer oder nicht und Stack versus Heap hat mMn verschiedene Gründe.Warum ich Objekte auf dem Heap anlege (vorzugsweise mit std::shared_ptr):
- Wenn ich aus eine Methode/Funktion ein Objekt liefer und über den weiteren Verbleib nichts weiß.
- Wenn eine Methode/Funktion eine unbekannte Anzahl von Daten ermittelt.
- Bei Objekten wie z.B. Bildern oder ähnlichen "Blob"-Objekten.
Wann ich Pointer nutze:
Aus obigen Gründen und Pointer als Parameter, wenn eine Methode/Funktion ein Objekt als Parameter optional erwartet. Das übergebene Objekt muss natürlich nicht unbedingt im Heap angelegt werden.Habe bestimmt was vergessen
-
warum darf denn ein Objekt, also eine Klasseninstanz keine großen Datenmengen bzw. Referenzen auf große Datenmengen als Eigenschaft haben? Und was heißt große Datenmengen, 1 MB, 1GB?
MFG
-
@Helmut-Jakoby sagte in Pointer wegen möglichen Stackoverflow?:
- Bei Objekten wie z.B. Bildern oder ähnlichen "Blob"-Objekten.
Das ist doch gerade ein Paradebeispiel dafür, wo der Blob sich darum gefälligst selber zu kümmern hat. Er hat ja auch kaum eine andere Wahl, denn entweder hält er seine Nutzdaten dynamisch, dann ist es sowieso auf dem Heap. Oder er hat ein
char[1000000000]
als Member in der Hoffnung, dass das für alle Blobgößen reicht, und dann gehört der Code weggeworfen und der Autor ins Gefängnis.
-
@SeppJ
Ich meinte, wenn ich so was wie ein Binary Large Object selbst bauen muss.
-
@KK27 ich schließe mich da @SeppJ an. Objekte dynamischer Größe gehören nicht auf den Stack. VLAs,
alloca
und all soche Sachen sollten besser nicht verwendet werden und es fällt mir schwer, ein Problem zu konstruieren, das man nicht auch anders lösen könnte.Da bleibt eigentlich nur Rekursion, die die Stack überlaufen lassen kann. Hierbei sollte man natürlich schon darauf achten, dass man den Algorithmus so formuliert, dass dieser sparsam mit dem Stack umgeht (wenige und kleine Variablen, Endrekursion oder in einen iterativen Algorithmus umschreiben). Auch hier fällt mir auf Anhieb kein Problem ein, bei dem das bei auf ausgewachsenen PCs üblichen Stack-Größen zum Problem wird (Mikrocontroller mit 1KiB Speicher mal außen vor gelassen, da muss man vielleicht hin und wieder mal auf andere Algorithmen ausweichen). Bei Rekursionen, die ich in meinen Programmen verwende, wächst die Rekursionstiefe auch meistens logarithmisch zur Problemgröße und ein so großes Problem, das den Stack zum überlaufen bringt, würde im Normalfall gar nicht auftreten - sofern es überhaupt in den Gesamtspeicher des Systems passt - vermutlich bekäme ich ein "Out of Memory" oder das System schiesst sich mit Auslagerungs-Thrashing ab, bevor es zu einem Stack Overflow kommt.
Ich will nicht behaupten, dass so ein Stack Overflow unmöglich wäre, aber ich erachte das Problem nicht als häufig genug um zu rechtfertigen im gesamten Code ausschliesslich Heap-Objekte zu verwenden.
Auch ist die Performance ein gewichtiges Gegenargument. Der Stack ist üblicherweise ziemlich "heiß" im CPU-Cache und für jedes Objekt auf eine andere Stelle im Speicher zugreifen zu müssen macht das Programm gewiss nicht schneller.
-
@Finnegan sagte in Pointer wegen möglichen Stackoverflow?:
Auch hier fällt mir auf Anhieb kein Problem ein, bei dem das bei auf ausgewachsenen PCs üblichen Stack-Größen zum Problem wird
Ich bin einmal an die Stackgrenze gestoßen, als ich eine Dreiecksvermaschung aufbaute und an einen Speziallfall stieß. Ich meine die Dreiecksvermaschung bestand aus über 100000 Dreicke und 99% der Dreiecke lagen in 1% der Fläche. Die Rekursionstiefe dürfte so bei cirka 30000 betragen haben.
Da der Algo aber soweit optimiert war, habe ich einfach die Stackgröße auf 2 MByte gesetzt.
-
@Quiche-Lorraine sagte in Pointer wegen möglichen Stackoverflow?:
@Finnegan sagte in Pointer wegen möglichen Stackoverflow?:
Auch hier fällt mir auf Anhieb kein Problem ein, bei dem das bei auf ausgewachsenen PCs üblichen Stack-Größen zum Problem wird
Ich bin einmal an die Stackgrenze gestoßen, als ich eine Dreiecksvermaschung aufbaute und an einen Speziallfall stieß. Ich meine die Dreiecksvermaschung bestand aus über 100000 Dreicke und 99% der Dreiecke lagen in 1% der Fläche. Die Rekursionstiefe dürfte so bei cirka 30000 betragen haben.
Interessant. Ich weiss schon warum ich es erstmal vorsichtig mit "fällt mir auf Anhieb keins ein" formuliert habe. Jetzt wo ich etwas mehr drüber nachdenke, kann ich mir doch ein paar Fälle vorstellen. Besonders auf Graphen. Man stelle sich z.B. einen Binären Baum vor, den man rekursiv abschreiten will, aber der Baum ist derart unbalanciert, dass er de facto eine verkettete Liste ist. Dann wäre die Rekursionstiefe gleich der Anzahl der Knoten - das kann dann schon schnell zu viel werden.
Oder der Input ist fehlerhaft. Jetzt erinnere ich mich noch an einen Algo, der nur auf zyklenfreien Graphen funktionierte, weil er ansonsten zu einer Endlosrekursion führte. Da habe ich dann eine Zyklenerkennung mit ordentlicher Fehlermeldung eingebaut, da natürlich ein Crash wegen Stack Overflow inakzeptabel war.
Ich nehme das also zurück, auch wenn ich denke, dass sowas doch eher selten vorkommt. Jedenfalls nicht oft genug, um die Heap-Allokationen für alles zu rechtfertigen (die bei der o.g. Endlosrekursion dann auch vor die Wand gelaufen wären, wenn auch vielleicht mit Out of Memory).
-
@Finnegan Listen oder Bäume aus
unique_ptr
sind auch so eine Sache. Die können einem im Destruktor schnell um die Ohren fliegen, weil die einzelnen Pointer da sich rekursive freigeben.Aber, wo ich gerade
unique_ptr
schreibe, @Helmut-Jakoby warumshared_ptr
. Ich bin ein großer Freund von klarer Ownership und weiß tatsächlich nicht, wann ich das letzte mal eine shared ownership gebraucht habe.@KK27 In meinem Verständniss von "modernem" C++, möchte man Value Semantics haben, die STL bietet dafür jede Menge Helfer, wie
std::variant
,std::optional
und so weiter. Das macht, meiner Meinung nach, Code einfacher zu lesen und zu verstehen.
Alles in einen Smart Pointer zu packen, widerspricht dem Verständnis.Als Anregung zu modernem C++ und Value Semantics vlt ein Vortrag von Klaus Iglberger von der CppCon 2022: https://www.youtube.com/watch?v=G9MxNwUoSt0
-
@Finnegan sagte in Pointer wegen möglichen Stackoverflow?:
Auch hier fällt mir auf Anhieb kein Problem ein, bei dem das bei auf ausgewachsenen PCs üblichen Stack-Größen zum Problem wird
Dito. Für gewöhnlich wird dann eher die Laufzeit zum Problem.
Wie groß ist denn ein "Element", und wie viele gibt es?
-
Erst mal vielen Dank für die vielen Rückmeldungen.
@Schlangenmensch sagte in Pointer wegen möglichen Stackoverflow?:
@KK27 In meinem Verständniss von "modernem" C++, möchte man Value Semantics haben, die STL bietet dafür jede Menge Helfer, wie std::variant, std::optional und so weiter. Das macht, meiner Meinung nach, Code einfacher zu lesen und zu verstehen.
Alles in einen Smart Pointer zu packen, widerspricht dem Verständnis.std::variant und std::optional kann ich leider nicht verwenden. Wenn ich mich nicht täusche ist unser Compiler ein 98er Compiler mit experimentiellen Featues wie Smart Pointern. Nicht alles was in C++ 11 verfügbar ist, können wir verwenden. Aber vieles wie Smart Pointer. Wir wollten unseren Compiler mal updaten, aber da gibt es Probleme mit uralten Bibliotheken und natürlich mit der Zeit. Nur mal so am Rande erwähnt.
Zum Beispiel habe ich Klasse(n) für ein Projekt geschrieben, die Daten aus einer SQL Datenbank laden. Daten die in der Datenbank den Wert NULL haben können, sind bei mir unique_ptr. Entweder haben sie einen Wert oder wenn sie NULL sind, dann ist es ein nullptr. Gerne hätte ich std::optional in diesem Kontext verwendet. Etwas besseres ist mir damals nicht eingefallen.
In meinem ganzen Leben als Programmierer ist mir einmal ein Stackoverflow passiert. Es war eine Endlosrekursion.
@Finnegan sagte in Pointer wegen möglichen Stackoverflow?:
Ich nehme das also zurück, auch wenn ich denke, dass sowas doch eher selten vorkommt. Jedenfalls nicht oft genug um, die Heap-Allokationen für alles zu rechtfertigen (die bei der o.g. Endlosrekursion dann auch vor die Wand gelaufen wären, wenn auch vielleicht mit Out of Memory).
Das sehe ich genauso. Das ist ein Sonderfall und deswegen sollte man nicht seinen ganzen Code umbauen.
@SeppJ sagte in Pointer wegen möglichen Stackoverflow?:
Das ist doch gerade ein Paradebeispiel dafür, wo der Blob sich darum gefälligst selber zu kümmern hat. Er hat ja auch kaum eine andere Wahl, denn entweder hält er seine Nutzdaten dynamisch, dann ist es sowieso auf dem Heap. Oder er hat ein char[1000000000] als Member in der Hoffnung, dass das für alle Blobgößen reicht, und dann gehört der Code weggeworfen und der Autor ins Gefängnis.
Naja. Einer unserer Senjor Programmierer verwenden immer noch solche Konstrukte und meinte bei Smart Pointer hat man keine Kontrolle wann es gelöscht wird. Egal wie gut man ihm Smart Pointer erklärt hat.
-
@KK27 sagte in Pointer wegen möglichen Stackoverflow?:
Naja. Einer unserer Senjor Programmierer verwenden immer noch solche Konstrukte und meinte bei Smart Pointer hat man keine Kontrolle wann es gelöscht wird. Egal wie gut man ihm Smart Pointer erklärt hat.
Wenn der Zeitpunkt wirklich wichtig ist, dann kann man diesen anhand des Codes oder bei komplizierteren
shared_ptr
-Strukturen im Debugger exakt bestimmen und ggfs. "verschieben" indem man einfach etwas länger den letzten Pointer hält. Ich wage aber zu behaupten, dass das "wann" eher selten relavant ist, sondern vielmehr, dass der Speicher überhaupt freigegeben wirdVielleicht verwechselt der Kollege das "wann" auch mit der Gültigkeit des Objekts hinter dem Pointer. Die garantiert man aber nicht, indem man die Speicherfreigabe selbst kontrolliert, sondern indem man sicherstellt, dass man einen gültigen (Smart-) Pointer auf das Objekt hält, während man darauf zugreift. Da wird nichts freigegeben, solange der existiert (und nicht mit sowas wie
ptr.release()
explizit freigegeben oder irgendwie gemoved wurde).
-
@Helmut-Jakoby sagte in Pointer wegen möglichen Stackoverflow?:
Wann ich Pointer nutze:
Aus obigen Gründen und Pointer als Parameter, wenn eine Methode/Funktion ein Objekt als Parameter optional erwartet. Das übergebene Objekt muss natürlich nicht unbedingt im Heap angelegt werden.Solche Zeiger verwende ich auch häufiger bzw. als Referenzen wenn die Übergabe nicht optional ist oder das Objekt einen leeren Zustand hat.
@Finnegan sagte in Pointer wegen möglichen Stackoverflow?:
Wenn der Zeitpunkt wirklich wichtig ist, dann kann man diesen anhand des Codes oder bei komplizierteren shared_ptr-Strukturen im Debugger exakt bestimmen und ggfs. "verschieben" indem man einfach etwas länger den letzten Pointer hält. Ich wage aber zu behaupten, dass das "wann" eher selten relavant ist, sondern vielmehr, dass der Speicher überhaupt freigegeben wird
Vielleicht verwechselt der Kollege das "wann" auch mit der Gültigkeit des Objekts hinter dem Pointer. Die garantiert man aber nicht, indem man die Speicherfreigabe selbst kontrolliert, sondern indem man sicherstellt, dass man einen gültigen (Smart-) Pointer auf das Objekt hält, während man darauf zugreift. Da wird nichts freigegeben, solange der existiert (und nicht mit sowas wie ptr.release() explizit freigegeben oder irgendwie gemoved wurde).Kann gut sein, dass dies mein Kollege meinte. Mit ein bisschen Übung meistert man das schnell. Am Anfang musste ich mich auch erst mit Smart Pointern anfreunden und lernen damit umzugehen.
Ich hab jetzt meine Antworten zu dem Thema. Vielen Dank.
-
@KK27 sagte in Pointer wegen möglichen Stackoverflow?:
Die Frage ist: Wie alt bist Du und wie alt ist der Kollege?
Es gab früher CPUs am Markt, die nicht den kompletten Adressraum als Stack adressieren konnten. Daher wurde natürlich extrem sparsam mit dem Stack umgegangen, und die Standardeinstellung auf anderen CPUs war auch extrem gering für den Stack. D.h. nicht triviale Allokationen mussten immer auf dem Heap erfolgen. Das setzt sich halt im Kopf fest.
-
Na ja, ich kann zumindest für Java sprechen, dass Optionals eher Fluch denn Segen sind. An für sich war das Feature nicht nötig. Aber, in C war's In, also brauchte es Java auch... Wen es interessiert, hier mehr darüber.
Ansonsten möchte ich mich an dem Kollegen-Bashing nicht beteiligen. Denn jeder weiß ja, dass studierte Leute in der Programmierung nichts taugen usw. Weg mit dem Abschaum.
-
@john-0 sagte in Pointer wegen möglichen Stackoverflow?:
Die Frage ist: Wie alt bist Du und wie alt ist der Kollege?
Ich bin 29 Jahre alt. Der Kollege der alles mit Pointern macht und der Kollege der nicht vom unique_ptr überzeugt ist sind beide über 60 Jahren.
-
@KK27 sagte in Pointer wegen möglichen Stackoverflow?:
Ich bin 29 Jahre alt. Der Kollege der alles mit Pointern macht und der Kollege der nicht vom unique_ptr überzeugt ist sind beide über 60 Jahren.
Ok, für Dich etwas historischen Kontext, um das besser einordnen zu können, wie man früher Programmieren gelernt hat, da man in der Regel privat keinen Zugriff auf ein VMS System, UNIX System, IBM Mainframe o.ä. hatte.
Die CPU von Apple II (außer IIGS), Commodore C16/C64/C128 und Atari 8Bit Rechner war entweder direkt ein MOS6502 oder ein Derivat davon. Diese CPU Familie war damals erschwinglich und sehr verbreitet, nur erlaubte diese CPU lediglich 255 Bytes als Stack für alle Routinen die gleichen 255 Bytes. D.h. trotz möglicher 1MB RAM Erweiterung (wahnsinnig teuer und nicht am Stück nutzbar, üblich waren 64kB im II/II+, und 128kB im IIe/IIc) für einen AppleII konnte man nur einen winzigen Stack nutzen. D.h. man nutzte ihn üblicherweise gar nicht.
Bei anderen 8Bit CPUs (Z80, 8085, …) war der Stack auf 16Bits (64kB) limitiert, selbst wenn per Bank Switching bedeutend mehr RAM verbaut war.
Damals sehr weit verbreitet waren DOS PCs. Die Intel 8088 CPU war ein Frickelfestival der Extraklasse, somit waren auch DOS und Windows (alles vor NT ist damit gemeint) ein Frickelfestival der Extraklasse. Ein 8086/8088 hat einen Adressraum von 1MB, aber nur wenn man das Indexregister (4Bit) zusätzlich zum Adressregister (16Bit) genutzt wird. Deshalb gibt es unterschiedliche Programmiermodelle mit unterschiedlichen Zeigerngrößen, die unter bestimmten Umständen zusammen genutzt werden. Beim 80286 kommen weitere Modi dazu, die aber unter DOS/Windos nicht genutzt wurden, weil sonst die alten Programme nicht mehr liefen.
-
@john-0 sagte in Pointer wegen möglichen Stackoverflow?:
Ok, für Dich etwas historischen Kontext, um das besser einordnen zu können, wie man früher Programmieren gelernt hat, da man in der Regel privat keinen Zugriff auf ein VMS System, UNIX System, IBM Mainframe o.ä. hatte.
Und heute schreiben wir Software für Funkchips in C, deren Programmgröße kleiner ist als 34 kByte, maximal 24 kByte RAM nutzt, mittels Bluetooth LE mit Handy kommuniziert, die Kommunikation zusätzlich Elliptic Curve Digital Signature Algorithm (ECDSA) nutzt, hierbei mehrere Crypto-Libs (Oberon, OPTIGA,..) unterstützt,...
-
ja solche alten sturköpfe, die "das schon immer so gemacht" haben sind echt furchtbar.
andererseits kann man software, die ja seit 50 jahren "läuft", auch nicht mal eben so neu machen und da sollte man diese alten techniken dann schon beherrschen.