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.
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.
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.
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.
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.
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.
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.)
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 ;-)
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.
...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.:
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:-)
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.
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.
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.
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.
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.
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.
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 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.
"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.
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.
"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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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…
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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:
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.
Versuche mal herauszufinden, warum so viele Entwickler trotzdem C/C++
verwenden.
Ein Antwort vorweg: Nicht weil sie alle doof sind.
Die richtigen Antworten sind:
weil es für viele Targets halt keine wirkliche Hochsprache gibt.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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...
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.
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.