Forum: Compiler & IDEs Allgemeine Fragen zu Eigenbau-libc


von Johannes K. (krotti42)


Lesenswert?

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.

von Tilo R. (joey5337) Benutzerseite


Lesenswert?

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ß!

von Johannes K. (krotti42)


Lesenswert?

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!

von Bradward B. (Firma: Starfleet) (ltjg_boimler)


Lesenswert?

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.

von Kaj G. (Firma: RUB) (bloody)


Lesenswert?

Johannes K. schrieb:
> dass manche
> Optionen das Programm kaputt optimieren.
Dann ist sehr wahrscheinlich einfach dein Code kaputt.
Stichwort: Undefined Behavior

von Monk (roehrmond)


Lesenswert?

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.

von Johannes K. (krotti42)


Lesenswert?

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.

von Monk (roehrmond)


Lesenswert?

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.

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

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

von Monk (roehrmond)


Lesenswert?

Oliver S. schrieb:
> Hobbies müssen keinen tieferen Sinn haben

Das stimmt wohl.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.)

von Dergute W. (derguteweka)


Lesenswert?

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

: Bearbeitet durch User
von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von Irgend W. (Firma: egal) (irgendwer)


Lesenswert?

Johannes K. schrieb:
> ...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.

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?

Hast du dir die "Originale" überhaupt schon mal angeschaut? z.B.:
- 
https://github.com/bminor/glibc/blob/master/sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S

Lesestoff:
- https://squadrick.dev/journal/going-faster-than-memcpy.html
- 
https://stackoverflow.com/questions/43343231/enhanced-rep-movsb-for-memcpy/43837564#43837564
- 
https://cdrdv2-public.intel.com/671488/248966-046A-software-optimization-manual.pdf
- https://www.strchr.com/strcmp_and_strlen_using_sse_4.2
- usw...

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:-)

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Rolf (rolf22)


Lesenswert?

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.

von Rolf (rolf22)


Lesenswert?

Nicht Joachim B. schrieb:
> Erfahrende Programmierer arbeiten defensiv.

Das gilt auch für Hardware-Entwickler. Und für gute 
Straßenverkehrs-Teilnehmer.

von Rolf M. (rmagnus)


Lesenswert?

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.

: Bearbeitet durch User
von Udo K. (udok)


Lesenswert?

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

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Daniel A. (daniel-a)


Lesenswert?

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.

von Harald K. (kirnbichler)


Lesenswert?

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.

von Andreas B. (bitverdreher)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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.

von Oliver S. (oliverso)


Lesenswert?

Peter D. schrieb:
> Einen Nullpointer als Flag zu benutzen, halte ich für keine gute Idee.
1
 if (p && p->someValue) ...

Seit Anbeginn der C-Zeitrechung findet sich etwas in der Art in so 
ziemlich jedem C-Programm.


Oliver

von Monk (roehrmond)


Lesenswert?

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.

von Peter D. (peda)


Lesenswert?

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

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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?

von Monk (roehrmond)


Lesenswert?

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.

von Harald K. (kirnbichler)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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:
1
int test(int *p) {
2
  *p++;
3
  if (p)
4
    return 42;
5
  return *p;
6
}

Das ergibt:
1
test:
2
  movl  $42, %eax
3
  ret

: Bearbeitet durch Moderator
von Wilhelm M. (wimalopaan)


Lesenswert?

§5.7/5:
1
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.

von Oliver S. (oliverso)


Lesenswert?

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
1
int bar(const struct foo *p){
2
    if (p){
3
        if (p->someValue)
4
            return 42;
5
    }
6
7
   return 43;
8
}

bleibt der Test auf NULL drin.

Oliver

von Wilhelm M. (wimalopaan)


Lesenswert?

Oliver S. schrieb:
> Das scheint aber nur bei diesem pathologisch einfachen Beispiel zu
> funktionieren.

Deine Funktion ist UB-frei, die von Jörg nicht.

von Foobar (asdfasd)


Lesenswert?

> 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.

von Oliver S. (oliverso)


Lesenswert?

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

: Bearbeitet durch User
von Rolf M. (rmagnus)


Lesenswert?

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.

von Εrnst B. (ernst)


Lesenswert?

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()"

von Foobar (asdfasd)


Lesenswert?

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.

von Udo K. (udok)


Lesenswert?

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.

von Oliver S. (oliverso)


Lesenswert?

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

: Bearbeitet durch User
von Harald K. (kirnbichler)


Lesenswert?

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.

von Daniel A. (daniel-a)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

Bei clang reicht folgendes:
1
int test(int* p) {
2
  if (p)
3
    return 42;
4
  return *p; // <1>
5
}

clang schließt aus <1>, dass p != nullptr ist, und optimiert zu return 
42;
gcc tut das nicht.

von Klaus R. (klausro)


Lesenswert?

Wilhelm M. schrieb:
>
1
> int test(int* p) {
2
>   if (p)
3
>     return 42;
4
>   return *p; // <1>
5
> }
6
>

Sorry, aber hast du nicht folgendes "gemeint"? p nur dann zu 
dereferenzieren, wenn es ein nullptr ist, macht doch keinen Sinn!
1
int test(int* p) {
2
  if (!p)
3
    return 42;
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:
1
test: 
2
        testq   %rdi, %rdi
3
        je      .LBB0_1
4
        movl    (%rdi), %eax
5
        retq
6
.LBB0_1:
7
        movl    $42, %eax
8
        retq

und der von gcc -Os -s
1
test:
2
        movl    $42, %eax
3
        testq   %rdi, %rdi
4
        je      .L1
5
        movl    (%rdi), %eax
6
.L1:
7
        ret

Beide male wird auf den nullptr getestet...

von Wilhelm M. (wimalopaan)


Lesenswert?

Klaus R. schrieb:
> Sorry, aber hast du nicht folgendes "gemeint"?

Nein.
Ich bezog mich auf:
Beitrag "Re: Allgemeine Fragen zu Eigenbau-libc"

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Daniel A. (daniel-a)


Lesenswert?

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.

von Udo K. (udok)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

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

von Hans W. (Firma: Wilhelm.Consulting) (hans-)


Lesenswert?

Klaus R. schrieb:
> Wilhelm M. schrieb:
>>
1
>> int test(int* p) {
2
>>   if (p)
3
>>     return 42;
4
>>   return *p; // <1>
5
>> }
6
>>
>
> Sorry, aber hast du nicht folgendes "gemeint"? p nur dann zu
> dereferenzieren, wenn es ein nullptr ist, macht doch keinen Sinn!
>
>
1
> int test(int* p) {
2
>   if (!p)
3
>     return 42;
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:
>
1
> test:
2
>         testq   %rdi, %rdi
3
>         je      .LBB0_1
4
>         movl    (%rdi), %eax
5
>         retq
6
> .LBB0_1:
7
>         movl    $42, %eax
8
>         retq
9
>
>
> und der von gcc -Os -s
>
1
> test:
2
>         movl    $42, %eax
3
>         testq   %rdi, %rdi
4
>         je      .L1
5
>         movl    (%rdi), %eax
6
> .L1:
7
>         ret
8
>
>
> Beide male wird auf den nullptr getestet...

Da ist ein "gutes" Code Beispiel:
https://joelaro.wordpress.com/2015/09/30/gcc-optimization-fdelete-null-pointer-checks/
1
int test(int* p) {
2
   int foo=*p;
3
   if (!p)
4
     return 42;
5
   return foo;
6
}

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...

73

von Udo K. (udok)


Lesenswert?

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
class ABC
2
{
3
    void *p;
4
  public:
5
    ~ABC();
6
    ABC();
7
    void dosomething();
8
};
9
10
void foo(ABC *p)
11
{
12
    // Delete hat eine implizite Abfrage auf 0 Zeiger eingebaut.
13
    delete p;    // if (p) { p->~ABC(); free(p); }
14
}
15
16
void bar(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.
22
    delete p;    // { p->~ABC(); free(p); }
23
}

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

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).

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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
int i = 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.

: Bearbeitet durch User
von Bauform B. (bauformb)


Lesenswert?

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

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von Daniel A. (daniel-a)


Lesenswert?

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.

von Le X. (lex_91)


Lesenswert?

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.

von Udo K. (udok)


Lesenswert?

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.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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:
1
 void* r = 1-1;      // Nullpointer

Und warum sollte s nun keine Nullpointer sein?

von Ob S. (Firma: 1984now) (observer)


Lesenswert?

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.

von Monk (roehrmond)


Lesenswert?

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.

von Ob S. (Firma: 1984now) (observer)


Lesenswert?

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.

von Norbert (der_norbert)


Lesenswert?

Ich mag diese extrem eloquente und sorgfältig abgewogene Ausdrucksweise.
Sehr angenehm für's Auge, da wird der Leser gleich mitgenommen.

von Rolf M. (rmagnus)


Lesenswert?

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.

: Bearbeitet durch User
von Monk (roehrmond)


Lesenswert?

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.

: Bearbeitet durch User
von Hans W. (Firma: Wilhelm.Consulting) (hans-)


Lesenswert?

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

von Andreas B. (bitverdreher)


Lesenswert?

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! :-)

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Johannes K. (krotti42)


Angehängte Dateien:

Lesenswert?

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...
1
gcc -I ./ -o bench bench.c cycle.S memcpy_c.c memcpy_a.S
...compilieren.

Ergebnis auf meinem System OHNE Optimierung:
1
glibc (system) memcpy():
2
Min: 589
3
Max: 29830
4
Avg: 2119
5
C: memcpy_c_v1():
6
Min: 75021
7
Max: 141512
8
Avg: 78389
9
C: memcpy_c_v2():
10
Min: 9975
11
Max: 10279
12
Avg: 10179
13
Assembly: memcpy_v1():
14
Min: 6232
15
Max: 6365
16
Avg: 6246
17
Assembly: memcpy_v2():
18
Min: 2147
19
Max: 2536
20
Avg: 2171
21
Assembly (with SSE2): memcpy_v3():
22
Min: 1149
23
Max: 1795
24
Avg: 1184

Mit Optimierung -O2:
1
glibc (system) memcpy():
2
Min: 1225
3
Max: 26553
4
Avg: 2605
5
C: memcpy_c_v1():
6
Min: 16435
7
Max: 16673
8
Avg: 16451
9
C: memcpy_c_v2():
10
Min: 2099
11
Max: 2546
12
Avg: 2132
13
Assembly: memcpy_v1():
14
Min: 6232
15
Max: 6355
16
Avg: 6243
17
Assembly: memcpy_v2():
18
Min: 2147
19
Max: 2442
20
Avg: 2166
21
Assembly (with SSE2): memcpy_v3():
22
Min: 1140
23
Max: 1862
24
Avg: 1187

von Monk (roehrmond)


Lesenswert?

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.

: Bearbeitet durch User
von Johannes K. (krotti42)


Lesenswert?

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.

von Monk (roehrmond)


Lesenswert?

Johannes K. schrieb:
> Der Code ist x86_64

Oh. Ich war von Mikrocontrollern ausgegangen, mein Fehler.

: Bearbeitet durch User
von Dergute W. (derguteweka)


Lesenswert?

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

von Johannes K. (krotti42)


Lesenswert?

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...

von Johannes K. (krotti42)


Lesenswert?

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.

von Zino (zinn)


Lesenswert?

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.

: Bearbeitet durch User
von Udo K. (udok)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.