Hallo Community!
Ich habe derzeit zu viel Zeit und deshalb eine eigene C Standard
Bibliothek als größeres Projekt gestartet. Es fehlt noch einiges in der
Umsetzung, dennoch hätte ich zu mindestens eine Frage was die
Optimierung von Standard Funktionen angeht. Will jetzt nicht einen
Flame-Thread C vs. Assembler anfangen, aber obwohl meine Kenntnisse zu
als Beispiel x86_64 Assembler noch bescheiden sind, hab ich doch
festgestellt, dass ich bis jetzt immer eine schnellere und kleinere
Variante von Grundfunktionen als GCC "heraus gezaubert" habe. Zu
mindestens OHNE Optimierungsoptionen von GCC.
Wie auch immer habe mal folgende C Standard Funktionen direkt in
Assembler (derzeit mal nur x86_64):
- memcpy()
- memset()
- strlen()
Würdet ihr noch andere Funktionen empfehlen bzw. welche Funktionen sind
optimierter sinnvoll? Die obigen Funktionen liegen natürlich auch in C
vor.
Wer nutzt einen Compiler ohne Optimierungsoptionen?
Das macht man doch höchstens während der Entwicklung, damit man im
Debugger auch sicher alles sieht.
Sobald Optimierungen an sind, wirst du nur noch in seltenen
Ausnahmefällen eine bessere Performance haben als der Compiler.
Bevor du einzelne Funktionen optimierst würde ich eher Wert auf
Vollständigkeit legen und Real-World-Programme gegen die eigene libc
linken.
Ich könnt' mir vorstellen, dass es da auch die eine oder andere
spannende Erkenntnis gibt.
Ich wünsch' dir viel Spaß!
Tilo R. schrieb:> Wer nutzt einen Compiler ohne Optimierungsoptionen?> Das macht man doch höchstens während der Entwicklung, damit man im> Debugger auch sicher alles sieht.> Sobald Optimierungen an sind, wirst du nur noch in seltenen> Ausnahmefällen eine bessere Performance haben als der Compiler.
Ich drücke mich mal vorsichtig aus. Verwende die Optimierungsoptionen
derzeit mit bedacht, da mir gelegentlich schon passiert ist, dass manche
Optionen das Programm kaputt optimieren. Zum Beispiel ist hier der
Kandidat -fdelete-null-pointer-checks was ich bei meinen Programmen bei
aktivierter Optimierung explizit mit -fno-delete-null-pointer-checks
ausschalte.
> Bevor du einzelne Funktionen optimierst würde ich eher Wert auf> Vollständigkeit legen und Real-World-Programme gegen die eigene libc> linken.> Ich könnt' mir vorstellen, dass es da auch die eine oder andere> spannende Erkenntnis gibt.
Ja, habe ich schon. Ist in der Tat spannend. :)
> Ich wünsch' dir viel Spaß!
Vielen Dank!
Ist halt auch die Frage für welche x86_64 calling convention man sich
entscheidet, schon da gibt es Unterschiede zwischen Gcc und microsoft,
um nur zwei player zu nennen.
Johannes K. schrieb:> dass manche> Optionen das Programm kaputt optimieren.
Dann ist sehr wahrscheinlich einfach dein Code kaputt.
Stichwort: Undefined Behavior
Kaj G. schrieb:> Dann ist sehr wahrscheinlich einfach dein Code kaputt.> Stichwort: Undefined Behavior
Ich vermute dass, dass Leute die sich einbilden, so eine lib im
Alleingang besser programmieren zu können, für solche Argumente nicht
offen sind.
Kaj G. schrieb:> Dann ist sehr wahrscheinlich einfach dein Code kaputt.
Sorry, aber das ist wieder mal die Standardaussage schlecht hin.
> Stichwort: Undefined Behavior
Gegenstichwort: Coding Style
Ich verwende halt meinen eigenen Programmierstil bei meinen Programmen
und hier ist die Optimierungsoption, dass eine Compiler Code der
überprüft ob jetzt ein Argument nicht NULL sein darf entfernt nicht
erwünscht. Ich schreibe ja nicht zum Spaß das. Meine Anforderungen an
einen Compiler sind, dass er das Geschriebene auch so umsetzt. Ganz egal
ob es im passt, oder nicht, oder es bessere Wege gibt. Ich bestimme die
Regeln im Code. Zu mindestens bei meinen Programmen.
Johannes K. schrieb:> Sorry, aber das ist wieder mal die Standardaussage schlecht hin.
Weil es halt meistens so ist. Höchstwahrscheinlich auch bei dir. Nach 30
Jahren Erfahrung in Programmierung weiß man das.
> Ich bestimme die Regeln im Code.> Zu mindestens bei meinen Programmen.
Da haben schon den Grund für das Problem. Erfahrende Programmierer
arbeiten defensiv. Sie vermeiden Probleme, indem sie ihre eigenen
Schwächen und die des Computers umgehen. So wird man wesentlich
erfolgreicher, als mit deiner Einstellung.
"Patterns" und "Best Practices" sind gute Stichwörter, falls du dich
diesbezüglich fortbilden möchtest. Es gibt da eine Reihe inzwischen
pensionierter Professoren, die am Aufbau unserer IT erheblich beteiligt
waren und ihre Erfahrungen in Bücher geschrieben haben. Keiner dieser
Experten empfiehlt, die Standard Bibliotheken neu zu schreiben.
Nicht Joachim B. schrieb:> Keiner dieser> Experten empfiehlt, die Standard Bibliotheken neu zu schreiben.
Nun ja, andere sammeln Bierdeckel oder laufen Ultra-Marathons. Wird auch
von keinem Professor empfohlen. Hobbies müssen keinen tieferen Sinn
haben, egal wie abstrus die auch sind.
Oliver
Johannes K. schrieb:> Meine Anforderungen an einen Compiler sind, dass er das Geschriebene> auch so umsetzt.
Dann solltest du allerdings auch deine eigene Programmiersprache
definieren. Wenn du C nimmst, musst du dich schon an dessen Regeln
halten.
Es steht dir natürlich frei, deinem Hobby auf deine Weise zu frönen,
aber dann solltest du dich nicht wundern, wenn der erzeugte Code kaputt
ist. Korrekter Code arbeitet unabhängig von der Optimierungsstufe und
unabhängig vom Schreibstil korrekt. (Dass du einen Compilerbug
erwischst, der zu fehlerhaftem Code führt, ist sehr wenig
wahrscheinlich.)
Moin,
Johannes K. schrieb:> hab ich doch> festgestellt, dass ich bis jetzt immer eine schnellere und kleinere> Variante von Grundfunktionen als GCC "heraus gezaubert" habe.
Na, das wird dann wohl daran liegen, dass die Leute, die da schon seit
Jahren an der libc rumstuempern, es nicht mit so einem grossen Loeffel
gefressen haben wie du ;-)
Johannes K. schrieb:> bzw. welche Funktionen sind> optimierter sinnvoll?
Vermutlich die Funktionen die du oder irgendein Opfer deiner lib
braucht?
Obwohl natuerlich Funktionen, die niemals aufgerufen werden, weil man
sie nicht braucht, am meisten Zeit und Platz sparen.
scnr,
WK
Johannes K. schrieb:> Meine Anforderungen an einen Compiler sind, dass er das> Geschriebene auch so umsetzt. Ganz egal ob es im passt, oder nicht,
Glückwunsch, dann kannst du auch gleich noch einen eigenen Compiler
schreiben.
Irgend W. schrieb:> Du willst wirklich behaupten, dass du es mal so eben geschafft hast eine> schnellere Implementation zu coden als die von u.a. Prozessorherstellern> optimierte Versionen?
Wenn man den Compiler nicht optimieren lässt, schafft man das
problemlos.
Oliver S. schrieb:> Hobbies müssen keinen tieferen Sinn haben, egal wie abstrus die auch sind.
Nein, natürlich nicht. Aber als Hobbyist sollte man weder glauben, allen
Profis überlegen zu sein, noch sollte man so tun.
"Hey, Leute, ich habe aus Wasserrohren einen Fahrradrahmen
zusammengeschweißt, der ist gewichtsärmer als die Rahmen aus der Fabrik
und trotzdem genauso stabil. Was meint ihr, soll ich auch noch einen
Lenker in dieser Art machen?"
Hier wird jedenfalls keine Standard-Lib geschrieben, sondern eine
Privat-Lib, die die Schnittstelle der Standard-Lib nachahmt. Als solche
kann sie niemals den jahrzehntelangen Tests unterzogen werden, die die
Standard-Libs hinter sich haben.
Johannes K. schrieb:> Ich drücke mich mal vorsichtig aus. Verwende die Optimierungsoptionen> derzeit mit bedacht, da mir gelegentlich schon passiert ist, dass manche> Optionen das Programm kaputt optimieren.
In der Regel ist dein Programm dann schon vorher kaputt gewesen, und
ohne die Optimierungen sind die Auswirkungen des Fehlers nur nicht so
gravierend. Ohne Optimierungen verzeiht der Compiler Fehler sehr viel
eher als mit Optimierungen. Dennoch existiert der Fehler in deinem Code
auch ohne Optimierungen. Ehrlich gesagt finde ich eine libc, die man nur
verwenden kann, wenn die Optimierungen abgeschaltet sind, völlig
unbrauchbar.
Irgend W. schrieb:> Meine Glaskugel vermutet gerade das du deinen Compiler/Linker so> "kastriert" hast das die mitgelieferten optimierten asm-Versionen> überhaupt nicht verwendet werden können und wunderst dich jetzt:-)
Die meisten Standard-Funktionen hat gcc doch auch selbst schon
eingebaut, so dass je nach Situation die libc-Funktion oft gar nicht
erst verwendet wird. Auch das setzt aber Optimierungen voraus.
Es ist dann schon etwas seltsam, sich damit zu rühmen, es selbst
schneller hinzubekommen als ein Compiler mit angezogener Handbremse.
Johannes K. schrieb:> Kaj G. schrieb:>> Dann ist sehr wahrscheinlich einfach dein Code kaputt.>> Sorry, aber das ist wieder mal die Standardaussage schlecht hin.
Es ist so gut wie immer so.
>> Stichwort: Undefined Behavior>> Gegenstichwort: Coding Style>> Ich verwende halt meinen eigenen Programmierstil bei meinen Programmen
Wenn dein "Coding Style" dazu führt, dass das Programm abstürzt, taugt
er nichts. Gerade wenn man eine eigene libc programmieren will, muss man
schon genau wissen, was in C funktioniert und was nicht. Die Fehler als
"Coding Style" abzutun, ist dabei ganz sicher nicht hilfreich.
> und hier ist die Optimierungsoption, dass eine Compiler Code der> überprüft ob jetzt ein Argument nicht NULL sein darf entfernt nicht> erwünscht.
Die Option entfernt nur solche checks, die eh nicht sinnvoll sind. Aus
der gcc-Doku:
"these assume that a memory access to address zero always results in a
trap, so that if a pointer is checked after it has already been
dereferenced, it cannot be null."
Heißt also: Wenn du zuerst über einen Zeiger auf ein Objekt zugreifst
und erst danach prüfst, ob dieser Zeiger null ist, dann ist die Prüfung
sinnlos, denn wenn er null ist, hätte schon der Zugriff gar nicht erst
stattfinden dürfen. Der Compiler nimmt daher aufgrund des Zugriffs an,
dass der Pointer nicht null sein kann und entfernt daher die Prüfung.
Wenn das bei dir zum Absturz führt, hat dein Programm also einen Fehler.
Ich empfehle für sowas die Nutzung eines Memory-Debuggers wie valgrind.
Die können helfen, solche Probleme aufzuspüren.
> Ich schreibe ja nicht zum Spaß das. Meine Anforderungen an> einen Compiler sind, dass er das Geschriebene auch so umsetzt. Ganz egal> ob es im passt, oder nicht, oder es bessere Wege gibt. Ich bestimme die> Regeln im Code. Zu mindestens bei meinen Programmen.
Solange du eine Programmiersprache nutzt, die du nicht selbst erfunden
hast, bestimmt die Programmiersprache die Regeln, an die du dich zu
halten hast. Wenn du dich darüber hinwegzusetzen versuchst, kommt bei
sowas in der Regel nur Murks raus, und das ist meine praktische
Erfahrung aus über 30 Jahren Programmierung.
Johannes K. schrieb:> Ich habe derzeit zu viel Zeit und deshalb eine eigene C Standard> Bibliothek als größeres Projekt gestartet.
Welche OS und welche Platform?
> Es fehlt noch einiges in der> Umsetzung, dennoch hätte ich zu mindestens eine Frage was die> Optimierung von Standard Funktionen angeht. Will jetzt nicht einen> Flame-Thread C vs. Assembler anfangen, aber obwohl meine Kenntnisse zu> als Beispiel x86_64 Assembler noch bescheiden sind, hab ich doch> festgestellt, dass ich bis jetzt immer eine schnellere und kleinere> Variante von Grundfunktionen als GCC "heraus gezaubert" habe. Zu> mindestens OHNE Optimierungsoptionen von GCC.
Das ist bei vielen Standardfunktionen auch mit eingeschalteten
Optimierungen nicht schwierig.
Der C Compiler weiss nicht viel über moderne AVX/SSE Erweiterungen oder
Cache Architektur.
Siehe etwa hier für einen Benchmark, wo du siehst was möglich ist:
Beitrag "Re: c++ Code langsam">> Wie auch immer habe mal folgende C Standard Funktionen direkt in> Assembler (derzeit mal nur x86_64):>> - memcpy()> - memset()> - strlen()>> Würdet ihr noch andere Funktionen empfehlen bzw. welche Funktionen sind> optimierter sinnvoll? Die obigen Funktionen liegen natürlich auch in C> vor.
- memcmp
- strcmp
- stricmp
- strncmp
- strchr
- utf8 Konvertierung
- Innere Schleife von strtol und strtod (Überlaufverhalten)
Etliche Spezialfunktionen wie:
- chstk
- longjmp
- Exception Handling
Mathe Funktionen wie:
- sin, cos, tan, sqrt, ceil, floor, trunc, lrint, nearbyint, round,
isinf, etc.
Bei vielen Funktionen sind die Intel Intrinsic Funktionen völlig
ausreichend.
https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
Johannes K. schrieb:> Ich habe derzeit zu viel Zeit und deshalb eine eigene C Standard> Bibliothek als größeres Projekt gestartet.
Du hast einfach den 01.06.24 mit dem 01. April verwechselt.
> Es fehlt noch einiges in der> Umsetzung, dennoch hätte ich zu mindestens eine Frage was die> Optimierung von Standard Funktionen angeht. Will jetzt nicht einen> Flame-Thread C vs. Assembler anfangen, aber obwohl meine Kenntnisse zu> als Beispiel x86_64 Assembler noch bescheiden sind, hab ich doch> festgestellt, dass ich bis jetzt immer eine schnellere und kleinere> Variante von Grundfunktionen als GCC "heraus gezaubert" habe. Zu> mindestens OHNE Optimierungsoptionen von GCC.
Das schafft JEDER!
Mach weiter mit -O2.
Udo K. schrieb:> Das ist bei vielen Standardfunktionen auch mit eingeschalteten> Optimierungen nicht schwierig.> Der C Compiler weiss nicht viel über moderne AVX/SSE Erweiterungen oder> Cache Architektur.
Das gilt vielleicht bei MSVC und der MS Libc (Ich glaube das war msvcrt
oder so?).
GCC / Clang mit der glibc sind sehr gut optimiert. Die glibc nutzt auch
teils ASM wenn es wirklich was bringt, inklusive AVX/SSE. So einfach
kriegt man da nichts besseres hin. Zumindest nicht bei memcpy, memset
und strlen.
Auch witzig ist immer, wenn man Phänomenale Benchmarks sieht, und
jemandes Lösung angeblich viel schneller ist, aber man den Code nie zu
sehen bekommt.
Rolf M. schrieb:> "these assume that a memory access to address zero always results in a> trap, so that if a pointer is checked after it has already been> dereferenced, it cannot be null."
Das funktioniert aber nur auf Plattformen, die einen entsprechenden
Mechanismus zum Auslösen von Traps/Exceptions haben.
Auf einem Microcontroller gibt es derartiges durchaus eher nicht, und
daher ist dort das Prüfen eines Pointers im Code selbst eine völlig
legitime Angelegenheit.
Unabhängig davon, lies mal genau, was da steht:
> if a pointer is checked after it has already been> dereferenced
Davon ist bei der Library-Implementierung ja wohl hoffentlich nicht die
Rede, da geht es darum, den Pointer zu prüfen, bevor er dereferenziert
wird.
Rolf M. schrieb:> Johannes K. schrieb:>> Ich drücke mich mal vorsichtig aus. Verwende die Optimierungsoptionen>> derzeit mit bedacht, da mir gelegentlich schon passiert ist, dass manche>> Optionen das Programm kaputt optimieren.>> In der Regel ist dein Programm dann schon vorher kaputt gewesen, und> ohne die Optimierungen sind die Auswirkungen des Fehlers nur nicht so> gravierend. Ohne Optimierungen verzeiht der Compiler Fehler sehr viel> eher als mit Optimierungen. Dennoch existiert der Fehler in deinem Code> auch ohne Optimierungen.
Ich habe diese leidvolle Erfahrung auch schon machen muessen. Erst ueber
den Compiler geflucht und mich dann an der eigenen Nase gefasst.
Seitdem ist es fuer mich ein guter Test, die SW mit maximaler
Optimierung laufen zu lassen. Wenn dann irgendetwas anders laeuft,
heisst es suchen wo der Fehler liegt.
Harald K. schrieb:> Rolf M. schrieb:>> "these assume that a memory access to address zero always results in a>> trap, so that if a pointer is checked after it has already been>> dereferenced, it cannot be null.">> Das funktioniert aber nur auf Plattformen, die einen entsprechenden> Mechanismus zum Auslösen von Traps/Exceptions haben.
Richtig.
> Auf einem Microcontroller gibt es derartiges durchaus eher nicht, und> daher ist dort das Prüfen eines Pointers im Code selbst eine völlig> legitime Angelegenheit.
Das ist es prinzipiell auch auf den Plattformen, die die Trap haben,
denn man will ja nicht, dass das Programm vom System terminiert wird,
bzw. die Exception anschlägt. Aber nochmal: Es geht um Checks, die
durchgeführt werden, nachdem bereits ein Zugriff erfolgt ist. Und wenn
dieser Check fehlschlägt, war der Zugriff falsch, und zwar immer, auch
dann wenn der µC dafür keine Exception auslösen kann. Aber gut, wenn du
solche Checks haben willst, kannst du ja die entsprechende Option
nutzen. Das bedeutet allerdings nicht zwangsläufig, dass man deswegen
gleich alle Optimierungen ausschalten muss.
> Davon ist bei der Library-Implementierung ja wohl hoffentlich nicht die> Rede, da geht es darum, den Pointer zu prüfen, bevor er dereferenziert> wird.
Ja, eben, und in dem Fall wird der Check ja auch nicht entfernt.
Harald K. schrieb:> Auf einem Microcontroller gibt es derartiges durchaus eher nicht, und> daher ist dort das Prüfen eines Pointers im Code selbst eine völlig> legitime Angelegenheit.
Die weitaus bessere Lösung ist immer, einfach sorgfältig programmieren
und nicht schnell mal irgend was hinschludern.
Ich hatte mal Pointerprobleme vermutet und dann ein Array von
Funktionspointern initialisiert und den Test eingebaut. Es hat überhaupt
nichts geändert, weil der Fehler an ganz anderer Stelle lag.
Einen Nullpointer als Flag zu benutzen, halte ich für keine gute Idee.
Zumindest sollte dann der Test auch explizit hingeschrieben werden.
Oliver S. schrieb:> Seit Anbeginn der C-Zeitrechung findet sich etwas in der Art in so> ziemlich jedem C-Programm.
Das kam mir auch direkt in den Sinn. Ich glaube, er hat damit etwas
anderes gemeint.
Oliver S. schrieb:> Seit Anbeginn der C-Zeitrechung findet sich etwas in der Art in so> ziemlich jedem C-Programm.
Wiki meint: "In C, dereferencing a null pointer is undefined behavior."
Trotzdem verwendet z.B. malloc ihn.
Ich weiß aber nicht, wie strcpy, sscanf und die anderen Libs damit
umgehen.
Ich versuche in meinem Code, ihn zu vermeiden.
Ich hatte mal in einem übernommenen C51 Code einen kniffligen Fehler
damit. Der vorherige Programmierer hat in einer Funktion auf 0 getestet.
Nun ist aber 0x0000 beim 8051 eine gültige Adresse in XDATA und der
Linker hat dort auch brav ein Array abgelegt. Das Programm konnte aber
durch den Guard nicht darauf zugreifen. Ein Umbenennen der Variable
machte sie wieder sichtbar und eine andere dafür unsichtbar. Dem Linker
zu sagen, XDATA beginnt an 0x0001 löste dann das Problem.
Hier ist auch noch ein Artikel dazu:
https://manderc.com/types/nullpointer/index.php
Peter D. schrieb:> Trotzdem verwendet z.B. malloc ihn.
malloc() gibt ggf. einen solchen zurück, aber es dereferenziert ihn
nicht. In gleicher Weise ist für free() wohldefiniert, dass es einen
Nullzeiger verkraftet und dann einfach nichts macht – es bringt also
(bei einer standardkonformen Bibliothek) rein gar nichts,
„sicherheitshalber“ vorher selbst noch zu testen, außer verschwendetem
Speicherplatz natürlich. :)
Selbstverfreilich darf man einen Zeiger auf NULL (oder 0) testen, dabei
dereferenziert man ihn ja nicht.
Peter D. schrieb:> Wiki meint: "In C, dereferencing a null pointer is undefined behavior."> Trotzdem verwendet z.B. malloc ihn.
Ja, sicher.
Der abgeleitete DT "pointer to ..." ist der einzige Datentyp in C, der
in seinem Wertebereich den "ungültigen Wert" enthält. Und das ist auch
gut so. Damit kann z.B. auch malloc() einen Fehler signalisieren.
Zeiger realisieren ein Referenzkonzept mit der Eigenschaft, dass sie
re-seatable und nullable sind (im Gegensatz zu z.B. C++-Referenzen sind,
die auch das Referenz-Konzept realisieren, allerdings sind diese
non-nullable und non-reseatable).
> Ich weiß aber nicht, wie strcpy, sscanf und die anderen Libs damit> umgehen.
Ist dort UB, steht in der doku.
> Ich versuche in meinem Code, ihn zu vermeiden.
Warum?
Peter D. schrieb:> Dem Linker zu sagen, XDATA beginnt an 0x0001 löste dann das Problem.
Ich denke, das ist in diesem Fall eine gute pragmatische Lösung.
Rolf M. schrieb:> Ja, eben, und in dem Fall wird der Check ja auch nicht entfernt.
Das klingt hier anders:
Beitrag "Re: Allgemeine Fragen zu Eigenbau-libc"
Johannes behauptet dort, daß dort Nullpointerprüfungen wegoptimiert
würden -- da wäre es interessant, mal ein konkretes Beispiel seines
Codes mit seinem spezifischen Codierungsstil zu sehen, um das
nachvollziehen zu können.
Harald K. schrieb:> Johannes behauptet dort, daß dort Nullpointerprüfungen wegoptimiert> würden
Ich kann natürlich nicht für seinen "Stil" sprechen, aber was Rolf
meinte, ist einfach nachvollziehbar:
If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined.
Jörg W. schrieb:> Das ergibt:> test:> movl $42, %eax> ret
Das scheint aber nur bei diesem pathologisch einfachen Beispiel zu
funktionieren.
Mit meinem vergleichbar umgeschriebenen Beispiel von oben
> Das scheint aber nur bei diesem pathologisch einfachen Beispiel zu> funktionieren.
Hier scheint es immer noch Missverständnisse zu geben: das "*p"
dereferenziert p (ob da noch ein ++ hinter hängt ist erstmal egal). Der
Kompiler darf dann davon ausgehen, dass p ein gültiger Pointer ist und
darf folgende NULL/non-NULL-Tests für diesen Pointer eleminieren.
Aus "*p;if(p)foo();" wird "*p;foo();", aus "*p;if(!p)bar();" wird "*p;".
Das "*p;" wird dann auch noch wegoptimiert.
Auch das tut er aber nur in deinen ganz einfachen Fällen.
Aus "*p;if(p)foo() else bar();" wird "*p;if(p)foo() else bar();"
Eigentlich kann der den Test sammt Else-Zweig entsorgen, tut er aber
nicht.
Oliver
Wilhelm M. schrieb:> Oliver S. schrieb:>> Das scheint aber nur bei diesem pathologisch einfachen Beispiel zu>> funktionieren.>> Deine Funktion ist UB-frei, die von Jörg nicht.
Ob die von Jörg UB-frei ist, weiß man nicht, da das davon abhängt, was
der Aufrufer übergibt. Die hier diskutierte Optimierung basiert darauf,
dass der Compiler annimmt, dass der Code UB-frei ist.
Oliver S. schrieb:> Eigentlich kann der den Test sammt Else-Zweig entsorgen, tut er aber> nicht.
Weil der Compiler das "*p" schon rausnimmt, weil das keinen (Nicht-UB)
Effekt hat.
"baz(*p);if(p)foo() else bar();" wird zu "baz(*p);foo()"
Oliver schrieb:
>> Auch das tut er aber nur in deinen ganz einfachen Fällen.>> Aus "*p;if(p)foo() else bar();" wird "*p;if(p)foo() else bar();"
Es ging um's Prinzip, aber ja, ich hätte es vorher mal austesten sollen
- bei mir kommt "if(p)foo() else bar();" raus. Sorry. Ernst hat es
schon erklärt: wenn die Dereferenzierung nicht wegoptimiert werden kann,
kommt das Erwartete raus. Keine Ahnung, ob das wirklich so erwünscht
oder ein Versehen ist.
Unter Windows dereferenzieren etliche DLL Funktionen den Zeiger, und
wenn er ungültig ist, schlägt eine Exception zu. Ungültig ist der
Zeiger nicht nur wenn er 0 ist (trivial), sondern auch wenn er auf
ungültigen Speicher zeigt.
Es gibt auch Konstrukte, die die Exception ausnützen um erst dann den
Speicher gültig zu machen, etwa der Stack wird so vergrössert, oder die
Zugriffsrechte auf den Speicher werden angepasst. Es ist da nicht immer
sichergestellt, dass nach einem Dereferenzieren der Zeiger gültig ist
und die Abfrage auf 0 wegfallen kann. Im Falle von UB sollte der
Compiler sinnvolle Annahmen treffen, die jahrelang funktionierenden Code
nicht zerstören.
Udo K. schrieb:> Es gibt auch Konstrukte, die die Exception ausnützen um erst dann den> Speicher gültig zu machen, etwa der Stack wird so vergrössert, oder die> Zugriffsrechte auf den Speicher werden angepasst.
Das passiert aber alles außerhalb des Scopes der Sprache C.
Nebenwirkungen, die über das hinausgehen, was der Compiler aus der
Sprachdefinition erwarten kann, braucht der nicht zu berücksichtigen.
Dafür bietet C das Schlüsselwort volatile, und es ist der Verantwortung
des Programmierers, das entsprechend anzuwenden.
Oliver
Es wäre schön, wenn sich "krotti" mit einem Beispiel seines "coding
style" Beitrag "Re: Allgemeine Fragen zu Eigenbau-libc" zurückmelden
könnte, denn dann ließe sich herausfinden, was genau da bei ihm
vermeintlich fehlerhaft wegoptimiert wird.
Udo K. schrieb:> Ungültig ist der> Zeiger nicht nur wenn er 0 ist (trivial), sondern auch wenn er auf> ungültigen Speicher zeigt.
Speicherbereiche, die explizit gemappt wurden, z.B. mit mmap, aber mit
eingeschränkten Berechtigungen (oder gar PROT_NONE, also gar keinen),
gemappt wurden, sowie Sachen wie userfaultfd, geben aus C Sicht, sehr
wohl einen gültigen Pointer zurück. Auf ein gültiges Objekt, und Zugriff
darauf ist wohldefiniert und erlaubt. Schlägt dann halt fehl, von den
Berechtigungen usw. weiss C nämlich nichts, das ist dafür quasi
irrelevant.
Es wird oft gesagt, Zeiger dürfen nicht auf ungültigen Speicher zeigen.
Aber eigentlich stimmt das gar nicht. Sie müssen lediglich auf ein
gültiges Objekt zeigen. Jenachdem, wie das Objekt angelegt wurde, hat es
unterschiedliche Lifetimes und Sichtbarkeit, das ist für den Compiler
und dessen Optimierungen ausschlaggebend. Nicht, ob Speicher dahinter
"Gültig" ist.
Manche Libraries nutzen auch noch Sentinel Values, also irgendwelche
Konstanten, die nie auf etwas zeigen sollten, als flags. Diese sind
mindestens IB, und oft keine gute Idee. Zumindest nicht, wenn man
portablen Code schreiben will.
C hat noch einen kleinen Quirk. Ein null Pointer, ein Pointer mit dem
Wert 0, muss nicht tatsächlich hardwareseitig einem Wert 0 Entsprechen.
Ein Compiler kann auch sonst einen ungültigen Sentinel Wert nehmen, oder
sonst irgendwie dessen Ungültigkeit angeben.
Sorry, aber hast du nicht folgendes "gemeint"? p nur dann zu
dereferenzieren, wenn es ein nullptr ist, macht doch keinen Sinn!
1
inttest(int*p){
2
if(!p)
3
return42;
4
return*p;// <1>
5
}
> clang schließt aus <1>, dass p != nullptr ist, und optimiert zu return> 42; gcc tut das nicht.
Wenn clang ausschließt, dass p != nullptr, dann ist ja bei clang IMMER p
== nullptr. Das kann doch nicht sein, würde aber zum "if (!p) return 42"
passen.
Aber: Der Output von clang -Os -s ist:
Was eine C/C++ Implementation als NULL oder nullptr festlegt, ist doch
Implementation Defined?
D.h. dass ein Zeiger einen NULL-Pointer enthält ist nicht
gleichbedeutend damit, dass der Zeiger den Wert 0 enthält.
Eine Implementation könnte duchaus festlegen, dass -1u die Darstellung
von NULL ist, und dass ein Zeigen der binär 0 enthält ein gültiger
Zeiger ist.
Allerdings kenne ich keine Plattform, die das so handhabt. Auf AVR wäre
0xffff sinnvoller als 0x0, aber z.B. avr-gcc verwendet 0x0 für NULL.
Dabei kann natürlich auch 0xffff eine gültige Adresse sein, aber dass
man explizit auf Adresse 0xffff zugreift dürfte seltener vorkommen, als
auf Adresse 0x0 zuzugreifen.
Johann L. schrieb:> Auf AVR wäre> 0xffff sinnvoller als 0x0, aber z.B. avr-gcc verwendet 0x0 für NULL.> Dabei kann natürlich auch 0xffff eine gültige Adresse sein, aber dass> man explizit auf Adresse 0xffff zugreift dürfte seltener vorkommen, als> auf Adresse 0x0 zuzugreifen.
Auf AVR ist die Option -fdelete-null-pointer-checks immer abgeschaltet.
Johann L. schrieb:> D.h. dass ein Zeiger einen NULL-Pointer enthält ist nicht> gleichbedeutend damit, dass der Zeiger den Wert 0 enthält.
Nicht ganz. Wenn man die Variable setzt oder ausliest, verhält sie sich
im Falle eines Null pointers, als hätte sie den Wett 0. Aber in HW kann
der tatsächliche Wert auch was anderes sein. Sachen wie (void*)0 oder
x=0, um einen null Pointer zu bekommen, funktionieren auch immer. Aus C
perspektive merkt man davon also eigentlich nie was. Nur z.B. wenn man
was mit memset nullt oder so, könnte es Probleme geben.
In C kann NULL tatsächlich als 0, (void*)0, oder sonst wie definiert
sein. Es gibt aber ein paar Eigenschaften, die erfüllt sein müssen.
In c23 gibt es auch nullptr_t und nullptr, deren einzige mir bekannte
Zweck ist für in _Generics, um das differenzieren eines nullptr Werts zu
erlauben. In der Praxis ist es aber ziemlich nutzlos.
Oliver S. schrieb:>> Es gibt auch Konstrukte, die die Exception ausnützen um erst dann den>> Speicher gültig zu machen, etwa der Stack wird so vergrössert, oder die>> Zugriffsrechte auf den Speicher werden angepasst.>> Das passiert aber alles außerhalb des Scopes der Sprache C.> Nebenwirkungen, die über das hinausgehen, was der Compiler aus der> Sprachdefinition erwarten kann, braucht der nicht zu berücksichtigen.> Dafür bietet C das Schlüsselwort volatile, und es ist der Verantwortung> des Programmierers, das entsprechend anzuwenden.
Der Code ist aber in C geschrieben und funktioniert einwandfrei. Das
Schlüsselwort volatile hat es damals noch gar nicht gegeben.
Es gibt für jede Plattform Konventionen wie mit den Graubereichen des
C-Standards umgegangen wird. Der C-Standard lässt diese Graubereiche
genau aus diesem Grund zu. Bis vor einigen Jahren hat das auch gut
funktioniert. Inzwischen ist C ein Anhängsel von C++, und die neue
Programmierergeneration kümmert sich nicht um lästige Feinheiten. Da
heisst es nur UB, und es wird wegoptimiert - auch wenn die Optimierung
ausser Inkompatibilitäten nichts bringt. In C++ mag es anders
ausschauen, da hat keiner den Überblick über seine 10 verschachtelten
Klassen, und jede fragt jeden Zeiger auf 0 ab.
Udo K. schrieb:> Bis vor einigen Jahren hat das auch gut> funktioniert. Inzwischen ist C ein Anhängsel von C++, und die neue> Programmierergeneration kümmert sich nicht um lästige Feinheiten. Da> heisst es nur UB, und es wird wegoptimiert - auch wenn die Optimierung> ausser Inkompatibilitäten nichts bringt. In C++ mag es anders> ausschauen, da hat keiner den Überblick über seine 10 verschachtelten> Klassen, und jede fragt jeden Zeiger auf 0 ab.
Egal ob C oder C++: eine Funktion besitzt Parameter und einen
Rückgabetyp, und die Typen dieser Parameter bzw. der Typ der Funktion
haben Wertebereiche, die notwendigerweise mindestens so groß sind, wie
es für die zu realisierende Aufgabe notwendig ist, im Normalfall aber
größer. Dies kann bei einem int-Parameter der Fall sein mit bspw. einem
geforderten Wertebereich von [-100,100]. Für Werte außerhalb dieses
Bereiches ist die Funktion schlicht undefiniert. Wünschenswert wäre ein
Paramatertyp wie int<-100,100>, sowas gibt es aber nicht in C. Diese
Einschränkung sicher man daher mit einer Zusicherung ab.
So auch bei Zeigern: allerdings ist es hier eben nur unvollständig
lösbar, gültige von ungültigen Zeigern zu unterscheiden. Bis auf den
nullptr. Gehört der nullptr nicht zum gültigen Wertebereich des Zeigers
dazu (wie bei der Funktion von Jörg W. oben), so sichert man das mit
einer Zusicherung ab. Soll der nullptr dazu gehören, muss man
Vorkehrungen treffen, dass er nicht dereferenziert oder inkrementiert
wird (wie bei der Funktion von Daniel mit dem checked-pointer idiom)
(anderfalls wäre UB wäre Folge).
Die Schwachstelle ist, dass man Zeiger, die nicht der nullptr sind,
nicht auf Gültigkeit bzgl. der Zeigeroperationen wie Indirektion oder
De/Inkrement prüfen kann. Ist der Zeiger, der eigentlich auf ein Objekt
zeigen sollte, vor Eintritt in die Funktion manipuliert worden, oder ist
der Zeiger, der eigentlich auf ein Array-Element zeigen sollte, nur ein
Zeiger auf ein singuläres Objekt, dann darf man im ersteren Fall nicht
dereferenzieren, und im letzteren nicht in/dekrementieren.
Man kann bedauern, dass die libc dies bei etwa strcpy() nicht tut bzw.
dass man es nicht ein/ausschalten kann, weil es eine Object-Bibliothek
ist. Zumindest steht es aber in der Doku drin.
Achtung C++: Braucht man den nullptr im Wertebereich eines Zeigers
nicht, sollte man statt eines Zeigers als Referenz eine C++-Referenz
stattdessen nehmen. Gleiches gilt für In/Dekrement. Genau dafür wurde
die C++-Referenz als non-nullable und non-reseatable erfunden.
Udo K. schrieb:> Es gibt für jede Plattform Konventionen wie mit den Graubereichen des> C-Standards umgegangen wird. Der C-Standard lässt diese Graubereiche> genau aus diesem Grund zu.
Ja. Das ist dann „implementation defined“. Außerhalb des C-Sprachmodells
liegende Seiteneffekte gehören da aber nicht dazu, und haben es auch nie
getan.
> Der Code ist aber in C geschrieben und funktioniert einwandfrei.
Was die alte Weisheit bestärkt, daß es keinen fehlerfreien Code gibt…
Oliver
>> Sorry, aber hast du nicht folgendes "gemeint"? p nur dann zu> dereferenzieren, wenn es ein nullptr ist, macht doch keinen Sinn!>>
1
>inttest(int*p){
2
>if(!p)
3
>return42;
4
>return*p;// <1>
5
>}
6
>
>>> clang schließt aus <1>, dass p != nullptr ist, und optimiert zu return>> 42; gcc tut das nicht.>> Wenn clang ausschließt, dass p != nullptr, dann ist ja bei clang IMMER p> == nullptr. Das kann doch nicht sein, würde aber zum "if (!p) return 42"> passen.>> Aber: Der Output von clang -Os -s ist:>
Hans W. schrieb:> Hier müsste der check weg optimiert werden (ein Check bei godbolt.org> sagt es zumindest). Die Frage ist nur ob der Code so sinnvoll ist...
Ich denke diese Optimierung ist für den "delete" operator da. Der
Standard fordert, dass der delete operatur mit NULL Zeigern umgehen
können muss. Diese Abfrage kann der Compiler wegoptimieren, wenn er den
Zeiger irgendwo weiter vorne dereferenziert hat.
1
classABC
2
{
3
void*p;
4
public:
5
~ABC();
6
ABC();
7
voiddosomething();
8
};
9
10
voidfoo(ABC*p)
11
{
12
// Delete hat eine implizite Abfrage auf 0 Zeiger eingebaut.
13
deletep;// if (p) { p->~ABC(); free(p); }
14
}
15
16
voidbar(ABC*p)
17
{
18
p->dosomething();// Der Zeiger wird hier dereferenziert.
19
20
// Die Abfrage auf 0 kann wegoptimiert werden weil es sonst schon vorher gecrasht hat.
21
// Das ist ein nettes Programm, das keine Zeiger im Exception Handler geradebiegt.
Hans W. schrieb:> Hier müsste der check weg optimiert werden (ein Check bei godbolt.org> sagt es zumindest). Die Frage ist nur ob der Code so sinnvoll ist...
Es ist doch vollkommen egal, wie herum das if-statement formuliert ist.
Allein wichtig: durch ein Inkrement oder Dereferenzierung (je nach
Compiler) des Zeigers, wird eine Optimierung getriggert, die davon
ausgeht, dass der Zeiger != nullptr ist. Dem liegt die Annahme zugrunde,
dass die Auswirkungen schon vorher hätten auftauchen müssen (ggf.
crash).
Udo K. schrieb:> Ich denke diese Optimierung ist für den "delete" operator da. Der> Standard fordert, dass der delete operatur mit NULL Zeigern umgehen> können muss. Diese Abfrage kann der Compiler wegoptimieren, wenn er den> Zeiger irgendwo weiter vorne dereferenziert hat.
Nein, für jede Funktion, die den Check beinhaltet und davor eine
Dereferenzierung / Inkrement erfolgt ist.
Udo K. schrieb:> Das Schlüsselwort volatile hat es damals noch gar nicht gegeben.
Naja, über Code aus der Jungsteinzeit (volatile ist seit dem ersten
C-Standard da) muss man wohl nicht mehr diskutieren. Damals haben
Compiler auch bei weitem nicht so viel optimieren können wie heute.
> Inzwischen ist C ein Anhängsel von C++
Solche Behauptungen diskreditieren dich einfach komplett.
Johann L. schrieb:> Was eine C/C++ Implementation als NULL oder nullptr festlegt, ist doch> Implementation Defined?>> D.h. dass ein Zeiger einen NULL-Pointer enthält ist nicht> gleichbedeutend damit, dass der Zeiger den Wert 0 enthält.
Jein. Auf Sprachebene repräsentiert der Wert 0 einen Nullpointer. Das
muss aber nicht heißen, dass bei dem Wert dann alle Bits 0 sind. Das
bedeutet auch, dass (void*)0 nicht zwingend auf Adresse 0 zeigt. Auch
sollte man bedenken, dass früher auch Plattformen üblich waren, auf
denen ein Zeiger nicht einfach nur eine Zahl von 0 bis Ende des
Speichers ist, sondern auch aus mehreren Komponenten besteht.
> Eine Implementation könnte duchaus festlegen, dass -1u die Darstellung> von NULL ist, und dass ein Zeigen der binär 0 enthält ein gültiger> Zeiger ist.>> Allerdings kenne ich keine Plattform, die das so handhabt.
Es gab in der Vergangenheit welche, aber heute ist das eher unüblich.
Siehe https://c-faq.com/null/machexamp.html> Auf AVR wäre> 0xffff sinnvoller als 0x0, aber z.B. avr-gcc verwendet 0x0 für NULL.> Dabei kann natürlich auch 0xffff eine gültige Adresse sein, aber dass> man explizit auf Adresse 0xffff zugreift dürfte seltener vorkommen, als> auf Adresse 0x0 zuzugreifen.
Nö. Auf AVR liegt an Adresse 0x0 das ALU-Register r0. Darauf möchte man
in C definitiv nicht explizit zugreifen.
Daniel A. schrieb:> Johann L. schrieb:>> D.h. dass ein Zeiger einen NULL-Pointer enthält ist nicht>> gleichbedeutend damit, dass der Zeiger den Wert 0 enthält.>> Nicht ganz. Wenn man die Variable setzt oder ausliest, verhält sie sich> im Falle eines Null pointers, als hätte sie den Wett 0.
Auch das stimmt nicht ganz. Wenn man einen Null-Zeiger mit dem literalen
Wert 0 vergleicht, kommt Gleichheit heraus, und wenn man ihn auf diesen
Wert setzt, dann bekommt man einen Nullzeiger. Das gilt aber nur für
konstante Integer-Ausdrücke mit dem Wert 0. Beispiel:
1
void*p=0;// Nullpointer
2
void*q=(void*)0;// Nullpointer
3
void*r=1-1;// Nullpointer
4
inti=0;
5
void*s=(void*)i;// kein Nullpointer, da i kein konstanter Ausdruck ist
> In C kann NULL tatsächlich als 0, (void*)0, oder sonst wie definiert> sein.
Es muss in C so oder äquivalent definiert sein.
> In c23 gibt es auch nullptr_t und nullptr,
Das gibt es in C++ schon seit C++11.
> deren einzige mir bekannte Zweck ist für in _Generics, um das> differenzieren eines nullptr Werts zu erlauben. In der Praxis ist es aber> ziemlich nutzlos.
Der Grund ist hauptsächlich, dass man eine saubere Trennung zwischen
Zeigern und Integern hat. Man kann einem int NULL zuweisen, aber nicht
nullptr. Würde man die Sprache heute entwerfen, würde man 0 im
Zeigerkontext gar nicht erst zulassen, sondern hätte gleich nur nullptr
dafür. Leider kann man das jetzt nachträglich nicht mehr abschaffen,
aber den nullptr kann man wengistens trotzdem einführen.
Jörg W. schrieb:> Johannes K. schrieb:>> Meine Anforderungen an einen Compiler sind, dass er das Geschriebene>> auch so umsetzt.> Korrekter Code arbeitet unabhängig von der Optimierungsstufe und> unabhängig vom Schreibstil korrekt.
Immerhin hat nicht nur Johannes ein Problem mit Optimierung
http://blog.fefe.de/?ts=98a1585b
Bauform B. schrieb:> Immerhin hat nicht nur Johannes ein Problem mit Optimierung
So eine Erwartungshaltung kann wohl nur ein Assemblerprogramm erfüllen.
Für C gilt immer noch die "as if"-Regel.
Wenn die crypto typen clever wären, würden sie das Problem an der Wurzel
angehe, und ein C23 Attribut für Variablen standardisieren lassen,
welches Branches basierend auf Expressions die die enthalten verbietet.
Sonst ersetzen sie nur unsicheren Coden mit irgendwann wieder unsicherem
Code.
Daniel A. schrieb:> Wenn die crypto typen clever wären, würden sie das Problem an der Wurzel> angehe,
Ja, echt schade dass das alles so Doofnasen sind.
Rolf M. schrieb:> Der Grund ist hauptsächlich, dass man eine saubere Trennung zwischen> Zeigern und Integern hat. Man kann einem int NULL zuweisen, aber nicht> nullptr. Würde man die Sprache heute entwerfen, würde man 0 im> Zeigerkontext gar nicht erst zulassen, sondern hätte gleich nur nullptr> dafür. Leider kann man das jetzt nachträglich nicht mehr abschaffen,> aber den nullptr kann man wengistens trotzdem einführen.
1
// In C ist NULL meist:
2
#define NULL (void*)0
3
// In C++
4
#define NULL 0
In C ist 0 also vom Type void* und damit ein Zeigertyp mit sauberer
Trennung.
C++ kann aber einen void* nicht auf einen anderen Zeiger ohne Cast
zuweisen,
Daher hat man NULL zu 0 definiert, und hat damit das Problem
eingehandelt, dass NULL nicht von der Integer 0 zu unterscheiden ist.
Rolf M. schrieb:> Johann L. schrieb:>> Eine Implementation könnte duchaus festlegen, dass -1u die Darstellung>> von NULL ist, und dass ein Zeigen der binär 0 enthält ein gültiger>> Zeiger ist.>> Allerdings kenne ich keine Plattform, die das so handhabt.>> Auf AVR wäre>> 0xffff sinnvoller als 0x0, aber z.B. avr-gcc verwendet 0x0 für NULL.>> Dabei kann natürlich auch 0xffff eine gültige Adresse sein, aber dass>> man explizit auf Adresse 0xffff zugreift dürfte seltener vorkommen, als>> auf Adresse 0x0 zuzugreifen.>> Nö. Auf AVR liegt an Adresse 0x0 das ALU-Register r0. Darauf möchte man> in C definitiv nicht explizit zugreifen.
Das war bei den alten nicht-Xmega Typen der Fall.
Alle neueren AVRs vom Xmega Typ (0-Series, 1-Series, 2-Series, AVR-Dx,
AVR-Ex, etc) habe an Adresse 0x0 ein SFR, und die 32 GRPs R0...R31 sind
nicht mehr in den RAM-Adressraum gemappt.
Rolf M. schrieb:> Auch das stimmt nicht ganz. Wenn man einen Null-Zeiger mit dem literalen> Wert 0 vergleicht, kommt Gleichheit heraus, und wenn man ihn auf diesen> Wert setzt, dann bekommt man einen Nullzeiger. Das gilt aber nur für> konstante Integer-Ausdrücke mit dem Wert 0. Beispiel:void* p = 0;> // Nullpointer> void* q = (void*)0; // Nullpointer> void* r = 1-1; // Nullpointer> int i = 0;> void* s = (void*)i; // kein Nullpointer, da i kein konstanter Ausdruck> ist
Folgendes geht aber nur in C, in C++ fehlt der reinterpret_cast:
Andreas B. schrieb:> Ich habe diese leidvolle Erfahrung auch schon machen muessen. Erst ueber> den Compiler geflucht und mich dann an der eigenen Nase gefasst.> Seitdem ist es fuer mich ein guter Test, die SW mit maximaler> Optimierung laufen zu lassen. Wenn dann irgendetwas anders laeuft,> heisst es suchen wo der Fehler liegt.
Ja, das ist eine durchaus realistische Erfahrung.
Von eine brauchbaren Hochsprache und einem brauchbaren Compiler würde
man allerdings erwarten müssen, dass er die entsprechenden Fehler des
Programmierers beim Compilieren in jedem Fall als solche zur Anzeige
bringt. Und zwar vollkommen unabhängig von der gerade eingestellten
Optimierung.
Alles andere disqualifiziert Sprache und Compiler als untauglich.
Aufgabe einer Hochsprache ist es ja, den Programmierer zu entlasten und
ihn auf seine Fehler hinzuweisen (zumindest, so weit das möglich ist,
also unterhalb der Ebene logischer Fehler).
Sprich: C/C++ ist Schrott. Jede Hochsprache, in der es sowas wie UB gibt
und das nicht zuverlässig gemeldet wird, ist Schrott, also keine
wirkliche Hochsprache.
Ob S. schrieb:> Sprich: C/C++ ist Schrott
Versuche mal herauszufinden, warum so viele Entwickler trotzdem C/C++
verwenden.
Ein Antwort vorweg: Nicht weil sie alle doof sind.
Monk schrieb:> Ob S. schrieb:>> Sprich: C/C++ ist Schrott>> Versuche mal herauszufinden, warum so viele Entwickler trotzdem C/C++> verwenden.>> Ein Antwort vorweg: Nicht weil sie alle doof sind.
Die richtigen Antworten sind:
1) weil es für viele Targets halt keine wirkliche Hochsprache gibt.
2) weil es Unmassen unsicheren Code in C/C++ bereits gibt und alls die
faulen Drecksäcke den benutzen. Was wiederum die Teile der Software, die
mittels einer sicheren Sprache für Targets implementiert wird, für die
sie verfügbar ist, zur Makulatur machen. Die erbt die Lücken des Mists,
der importiert wurde.
Man kann sich den Sachverhalt schönreden oder schönsaufen, es bleibt
aber eine Tatsache. Nur Idioten verschliessen die Augen davor.
Udo K. schrieb:> // In C ist NULL meist:> #define NULL (void*)0
Meist, aber nicht zwingend.
> // In C++> #define NULL 0>> In C ist 0
Ich nehme an, du meinst hier NULL.
> also vom Type void* und damit ein Zeigertyp mit sauberer Trennung.
Nein, eine saubere Trennung gibt es nicht. 0 ist im Zeigerkontext auch
ohne Cast eine gültige Nullzeiger-Konstante.
> C++ kann aber einen void* nicht auf einen anderen Zeiger ohne Cast> zuweisen,> Daher hat man NULL zu 0 definiert, und hat damit das Problem> eingehandelt, dass NULL nicht von der Integer 0 zu unterscheiden ist.Wilhelm M. schrieb:> Folgendes geht aber nur in C, in C++ fehlt der reinterpret_cast:> void* r = 1-1; // Nullpointer
Ich hatte mich nur auf C bezogen.
> Und warum sollte s nun keine Nullpointer sein?
Hab ich doch geschrieben: Weil i kein konstanter Ausdruck ist. Aus dem
C-Standard:
"An integer constant expression with the value 0, or such an expression
cast to type void *, is called a null pointer constant. If a null
pointer constant is converted to a pointer type, the resulting pointer,
called a null pointer, is guaranteed to compare unequal to a pointer to
any object or function."
"An integer may be converted to any pointer type. Except as previously
specified, the result is implementation-defined, might not be correctly
aligned, might not point to an entity of the referenced type, and might
be a trap representation."
Was s ist, ist also implementation-defined. Es kann auch ein
Nullpointer sein, muss aber nicht.
C++ ist noch etwas restriktiver:
"A null pointer constant is an integer literal with value zero or a
prvalue of type std::nullptr_t."
Da muss also auch r nicht unbedingt ein Nullzeiger sein, da 1-1 nicht
nur ein Literal ist.
Ob S. schrieb:> all die faulen Drecksäcke den (C/C++ Code) benutzen.> Nur Idioten verschließen die Augen davor.
Bist du einsam? Falls ja: ich habe eine Idee, woran das liegen könnte.
Ob S. schrieb:> Sprich: C/C++ ist Schrott. Jede Hochsprache, in der es sowas wie UB gibt> und das nicht zuverlässig gemeldet wird, ist Schrott, also keine> wirkliche Hochsprache.
Das liegt nicht anders Sprache, sondern an den Compilereinstellungen.
-Wall in -Wextra sollten IMHO Default sein. Mir wäre noch nie
aufgefallen, dass der Compiler "komische Ergebnisse" liefert ohne zu
warnen.
73
Ob S. schrieb:> Von eine brauchbaren Hochsprache und einem brauchbaren Compiler würde> man allerdings erwarten müssen, dass er die entsprechenden Fehler des> Programmierers beim Compilieren in jedem Fall als solche zur Anzeige> bringt. Und zwar vollkommen unabhängig von der gerade eingestellten> Optimierung.
Woher soll der arme Compiler denn wissen was Du zu programmieren
gedenkst?
> Sprich: C/C++ ist Schrott.
Ich übersetze mal: Du hast keine Ahnung. Geh spielen.
Norbert schrieb:> Ich mag diese extrem eloquente und sorgfältig abgewogene Ausdrucksweise.
YMMD! :-)
Rolf M. schrieb:> Wilhelm M. schrieb:>> Folgendes geht aber nur in C, in C++ fehlt der reinterpret_cast:>> void* r = 1-1; // Nullpointer>> Ich hatte mich nur auf C bezogen.>>> Und warum sollte s nun keine Nullpointer sein?>> Hab ich doch geschrieben: Weil i kein konstanter Ausdruck ist. Aus dem> C-Standard:
Ja, sorry!
Ich meinte das `r` eigentlich ... falsch gelesen und falsch geantwortet.
Hi!
War leider aufgrund eines größeren privaten Problems verhindert. Bin die
ganzen Antworten jetzt mal nur kurz überflogen, habe aber
zwischenzeitlich auch an meiner eigenen libc weiter geschraubt und auch
eigene Benchmarks von den mir geschrieben Funktionen in Assembler
gemacht. Auch habe ich wirklich mal die Optimierung verwendet.
Hab da ein Benchmark unter der Verwendung des TSC von den in Assembler
und die in C Varianten gemacht. Muss sagen, dass zu mindestens auf
meinem (alten) System die Optimierung -O2 das brauchbarste Ergebnis
liefert. Das heißt die C Variante (memcpy_c_v2) ist ungefähr gleich
schnell, wie die Assembler Variante. Zu mindestens wenn ich in Assembler
nicht auf SSE2 (memcpy_v3) zurückgreife. Ohne Optimierung ist der C Code
um das 5-fache langsamer als die Assemblervariante. Hätte das aber
selbst nicht erwartet, dass das Ergebnis so groß ist...
Aber seht probiert selbst, wenn ihr Zeit habt. Hab jedenfalls die
Quellen angehängt.
Einfach mit...
Da bei AVR Funktionsaufrufe relativ billig sind, erzeugt -Os oft
kompakteren Code als -O2, ohne langsamer zu werden. Mit so einem
Mini-Code wird man das allerdings nicht sehen können.
Monk schrieb:> Da bei AVR Funktionsaufrufe relativ billig sind, erzeugt -Os oft> kompakteren Code als -O2, ohne langsamer zu werden. Mit so einem> Mini-Code wird man das allerdings nicht sehen können.
Der Code ist x86_64, nicht AVR. Aber auf AVR habe ich bis jetzt auch
immer -Os verwendet.
Moin,
Also ich glaube, es bringt viiiel mehr, in seiner Software sich drum zu
kuemmern, z.b. memcpys moeglichst sparsam einzusetzen als selbst zu
versuchen, memcpy noch weiter zu optimieren.
Gruss
WK
Monk schrieb:
[...]
> Oh. Ich war von Mikrocontrollern ausgegangen, mein Fehler.
Kein Problem.
Zugegeben der Benchmark ist in der Tat eine Miniprogramm, aber für meine
Zwecke ausreichend den x86_64 Time Stamp Counter (TSC) zu verwenden. Der
sollte ja die Taktzyklen angeben. Hab es halt so gemacht, dass ich vor
den Beginn der besagten Funktionen (in einer Schleife) den TSC auslese
und danach. Das ganze subtrahiert und ich hab (ungefähr) die Taktzyklen
was eine Funktion jetzt benötigt. Könnte ich jetzt theoretisch noch
anhand der Prozessorgeschwindigkeit (die ja bekannt ist) in die
Zeitspanne in Sekunden umrechnen. Hab ich aber nicht gemacht...
Dergute W. schrieb:> Moin,>> Also ich glaube, es bringt viiiel mehr, in seiner Software sich drum zu> kuemmern, z.b. memcpys moeglichst sparsam einzusetzen als selbst zu> versuchen, memcpy noch weiter zu optimieren.>> Gruss> WK
Hi,
Das stimmt allerdings. Ich muss allerdings zugeben, dass diese Versuche
aus Langeweile und Interesse meinerseits entstanden sind. Sonst hätte
ich daheim nichts produktives zu tun. Bin leider mit meinen 41 Jahren
schon seit ein paar Jahren in unbefristeter Invaliditätsrente.
memcpy_c_v2 von heute 17:36 ist fehlerhaft, funktioniert nicht korrekt,
wenn num kein ganzzahlig Vielfaches von sizeof(unsigned long) ist. Und
auf manchen CPUs kracht es oder die Daten werden falsch kopiert (auch
wenn num ein ganzzahlig Vielfaches von sizeof(unsigned long) ist).
Undefined Behavior eben. Außerdem scheint das eine Übung darin zu sein,
trivialen Code auf möglichst viele Zeilen zu verteilen.
Johannes K. schrieb:> Aber seht probiert selbst, wenn ihr Zeit habt. Hab jedenfalls die> Quellen angehängt.>> Einfach mit...gcc -I ./ -o bench bench.c cycle.S memcpy_c.c memcpy_a.S> ...compilieren.
Liefert unter Windows / MSYS64 einen Haufen Fehler.
1
memcpy_a.S: Assembler messages:
2
memcpy_a.S:7: Warning: .type pseudo-op used outside of .def/.endef: ignored.
3
memcpy_a.S:7: Error: junk at end of line, first unrecognized character is `m'
4
memcpy_a.S:59: Warning: .size pseudo-op used outside of .def/.endef: ignored.
5
...
Der C Code hat einen ganzen Haufen von peinlichen Fehlern. Schau
nochmal in Ruhe drauf. Es gibt übrigens auch noch andere BS als Linux,
und nicht überall ist sizeof(long) == 8.