Sammelthread zu Visual C++’ Compiler

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Krishty hat geschrieben: 07.02.2021, 01:13Hier ist der komplette Workaround:
Visual C++ kann die Funktion nicht statisch auflösen. Wenn man also bitsOf(123.0f) schreibt, dann lädt Visual C++ da nicht direkt die Hex-Konstante 0x42F60000 in ein Allzweckregister, sondern erzeugt eine float-Konstante 123.0f, lädt sie in ein xmm-Register, und überführt sie via MOVD in ein Allzweckregister. Also ein Speicherzugriff und zwei Befehle, wo eigentlich nix sein sollte.

constexpr bringt nichts, weil es für SSE-Intrinsics nicht zur Verfügung steht (Bug Report, den ich schon gepusht habe).

Wenn ich diese Grütze von Hand auflöse, ist mein powf() wieder acht Prozent schneller – ~37 Takte. (powf() muss prüfen, ob das Ergebnis überläuft, um vernünftig inf zurückzugeben. Das geschieht über Bitvergleiche mit Limits.)

Das Üble ist: Ich habe Millionen Artikel gelesen (und hier geteilt!), in denen so Dinge stehen, wie: Ein Ryzen schafft zwei L1-Speicherzugriffe pro Takt. Ein Ryzen führt spekulativ 120 Befehle im Voraus aus. CPUs haben seit 2003 keine Load-Hit-Store-Penalty. Man hat 30 Slots für die Sprungvorhersage, und konstante Sprünge sind kostenlos.

Und das scheint alles Bullshit zu sein. Man schmeißt einen unnötigen Load raus, der eigentlich perfekt gepipelinet und predicted sein sollte, und der Code wird schneller. Man schmeißt einen Sprung raus, der immer zu 100 % nicht genommen wurde (also das einfachste für die Sprungvorhersage überhaupt), und der Code wird schneller. Mit jedem einzelnen Speicherzugriff, den ich rauskicke, purzeln die Prozente, obwohl jedes Paper sagt, dass bis zu zwei pro Takt gratis wären.

tl;dr: constexpr bringt mit Intrinsics nichts, und deshalb schnappt ihr euch am besten einen Taschenrechner und rechnet alle Konstanten selber aus und hard-codet sie. Als wär’s 1983.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Fun fact: Als ich zu dem Bug recherchiert habe, kam sofort „wie kann die CRT std::bit_cast implementieren, ohne dass memcpy() eine constexpr-Funktion ist“ :(
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Der aktuelle Preview-Compiler hat begonnen, .reloc-Sections als Discardable zu markieren. Laut Raymond Chen sollte das Flag seit 30 Jahren keine Bedeutung mehr haben.

Entweder ist ihnen was aus dem /KERNEL-Modus durchgeglitcht, oder der Windows 10-Memory-Manager bekommt ein neues Feature.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Visual C++ 2019 scheint im prä-C++20-Modus echte Probleme mit statisch initialisierten std::strings zu haben.

Wenn ihr irgendwo global schreibt

  std::string lol = "hallo welt";

dann erzeugt Visual C++ glatt 500 Bytes an Code und Daten. Um den Konstruktor von std::string aufzurufen. Fünf fucking hundert Bytes! WTF

  auto const lol = "hallo welt"; // 500 Bytes kleineres Kompilat PRO STRING

Fun fact: Kompiliertes Assimp besteht zu fast einem Prozent aus statisch initialisierten std::strings, mit denen später nichts anderes gemacht wird, als c_str() aufzurufen. Patches sind unterwegs.

Ich habe oben nicht über C++20 geschrieben, weil damit ja constexpr für den K’tor eingeführt wurde. Möglicherweise fällt der Code damit radikal besser aus. Aber wenn man nur einen C-String braucht, ist es immernoch das Beste, einfach einen C-String hinzuschreiben.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
kimmi
Moderator
Beiträge: 1404
Registriert: 26.02.2009, 09:42
Echter Name: Kim Kulling
Wohnort: Luebeck
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von kimmi »

Noch besteht Assimp Kompilat zu 1% aus statisch initialisierten std::strings, noch!
Benutzeravatar
Jonathan
Establishment
Beiträge: 1962
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Jonathan »

Wie, was? Warum sind std::strings in C++ so scheiße?
Ich bin seit langem dafür absolut immer std::string zu benutzen. Aus Robustheitsgründen. Ein Großteil aller Sicherheitslücken geht auf die mangelnde Speicherverwaltung von C zurück, ich weiß gar nicht, wie oft ich in Sicherheitsmeldungen schon das Wort Buffer-Overflow gelesen habe. Meine Hoffnung war immer, dass der Overhead bei sowas wie 5% liegt (aufs ganze Programm bezogen), was durch schneller werdende Prozessoren nach ein paar Monaten also komplett egal ist und es somit total unvernünftig sein sollte, die alten C-Varianten (auch für Arrays usw.) zu verwenden.

Irgendwo hab ich auch mal einen Artikel gelesen, dass in Chrome ständig zwischen C-String und std::string konvertiert wurde, teilweise wurden Variablen durch 20 Funktionen durchgereicht und dafür jedesmal umgewandelt. Das zu beheben hat wohl extrem viel gebracht. Das ist traurig, denn nach meinem Gefühl sollte es alle diese Probleme nicht geben. Was ich mit all dem sagen will, weiß ich grad auch nicht, ich glaube dieser Post ist eher ein Rumgejammer darüber, wie schlimm schon wieder alles ist.
Lieber dumm fragen, als dumm bleiben!
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Du meinst wohl https://groups.google.com/a/chromium.or ... Z5ZK0K3gEJ

Mit C++20 sollte dieser furchtbare Overhead für Konstanten verschwinden, aber irgendwie kann noch kein Compiler constexpr-Strings?! https://godbolt.org/z/5a9qo3Yaf

Ach ja, das Originalproblem illustriert: https://godbolt.org/z/7rx7e1P73 vs https://godbolt.org/z/oYxzra9Wq
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
DerAlbi
Establishment
Beiträge: 265
Registriert: 20.05.2011, 05:37

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von DerAlbi »

Code: Alles auswählen

constexpr std::string_view foo("String")
und dann

Code: Alles auswählen

foo.data()
statt c_str().
Da haste alles Gedöns der STL falls nötig und die code-gen vom const char*
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Trotz 15 Upvotes (woher auch immer!) bleibt bool in Visual C++ erstmal langsamer als int.

https://developercommunity.visualstudio ... int/932580

Nachdem Microsoft endlich wieder C aufgegriffen hat, ist es vielleicht ganz nützlich, wenn C bei ihnen schneller als C++ bleibt ;)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Lord Delvin
Establishment
Beiträge: 455
Registriert: 05.07.2003, 11:17

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Lord Delvin »

Jonathan hat geschrieben: 04.05.2021, 13:32 Wie, was? Warum sind std::strings in C++ so scheiße?
[...]
Irgendwo hab ich auch mal einen Artikel gelesen, dass in Chrome ständig zwischen C-String und std::string konvertiert wurde, teilweise wurden Variablen durch 20 Funktionen durchgereicht und dafür jedesmal umgewandelt.
a)
Weil du zu viel damit machen kannst und es eben nur std::string gibt. Das war richtig, als man es gemacht hat, weil kein Rechner genug RAM gehabt hätte, um eine bessere Standardbibliothek zu übersetzen. Außerdem gibt es Tradeoffs, bei denen dir extrem klar sein muss, was du willst und du dir ganz schnell sehr hart in den Fuß schießt, wenn du dich irrst.
Ein Problem ist, dass es Subklassen geben kann. Ein weiteres ist data https://en.cppreference.com/w/cpp/strin ... tring/data. D.h. du kannst die Repräsentation sehen.
Und das beste bei strings (C++-Implementierung ist mir hier jetzt nicht mehr ganz klar), ist, wenn du zwischen längen-encoding und zero-termination-encoding konvertieren musst.
Was mich zum zweiten Punkt bringt:
b)
Ist normal ;)
Gerade wenn du große Projekte hast, an denen viele Leute arbeiten, nicht jeder allen Code überblicken oder ändern kann und sich teilweise die Anforderungen ändern, dann passiert es relativ schnell, dass man irgendwelchen Code hat, der ziemlich aufwändig irgendwas berechnet und am Ende eigentlich nur noch den Effekt hat, dass true oder 2 oder sowas zurückgegeben wird, weil alle anderen Fälle und Funktionen nicht mehr lebendig sind.
Das wird natürlich nicht besser, wenn man zwischen verschiedenen Repräsentationen konvertieren muss. Strings sind da ein Beispiel, aber schau dir mal JSON an. Was manche Leute da an Kapazität verbrauchen, um effektiv vier Byte aus einer Datenbank zu lesen...nicht gut ;)

Ich habe bei mir die Literale und die normalen Strings getrennt, um mir genau die "fünf fucking hundert" Bytes zu sparen: https://github.com/tyr-lang/stdlib/blob ... string.tyr
Das erspart einem aber nicht allen Overhead gegenüber keine Klasse, nur statischer Code. Dafür könntest du in dem Szenario zur Laufzeit nicht mehr nach dem Typ fragen, müsstest teilweise Code duplizieren, würdest dabei Fehler machen, würdest konvertieren müssen. Am Ende des Tages ist es schwer zu sagen, ob es wirklich Overhead ist, wenn das Projekt eine gewisse Größe hat.

Dass der VC++ da 500 Byte mehr braucht ist aber irritierend. Eigentlich würde man erwarten, dass sie clever genug sind, *das Ergebnis* der Initialisierung irgendwo in den Speicher zu schreiben.
Das kannst du natürlich nur machen, wenn dein Code ein gewisse Qualität hat und wenn du Leute hast, die Zeit für sowas haben und die erforderlichen Fähigkeiten. Das wirklich sauber und stabil hinzubekommen ist schwerer, als man vielleicht erstmal denkt, weil es immer globale Regeln gibt, in die man sich irgendwie integrieren muss.
XML/JSON in schnell: OGSS
Keine Lust mehr auf C++? Versuche Tyr: Get & Get started
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Lord Delvin hat geschrieben: 21.07.2021, 21:23Dass der VC++ da 500 Byte mehr braucht ist aber irritierend. Eigentlich würde man erwarten, dass sie clever genug sind, *das Ergebnis* der Initialisierung irgendwo in den Speicher zu schreiben.
Sind sie auch, aber darum schrieb ich, dass das Problem nur beim Kompilieren mit einem älteren C++-Standard als C++20 auftritt: Strings brauchen (meist) Speicher dynamischer Länge. Vor C++20 waren new/delete Funktionen wie alle anderen auch, und wenn du sie bspw. für deine eigene Speicherverwaltung überschrieben hast, musste sich der Compiler an die as-if-Regel halten und durfte nutzlose Allokationen nicht wegoptimieren (das würde ja das beobachtbare Verhalten deines Speicher-Managements ändern).

Seit C++20 hat der Compiler mehr Freiheiten und darf unnötige Paare aus new/delete wegoptimieren. Da ist der generierte Code radikal besser, wenn auch nicht perfekt AFAIR. Edit: Er darf nicht nur, er muss sogar, weil Strings nun constexpr können!

Langfristig wird das Problem also sterben, aber Assimp z. B. unterstützt AFAIK höchstens C++17. Bis alle Bibliotheken C++20 voraussetzen, wird noch ein Jahrzehnt vergehen.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Lord Delvin
Establishment
Beiträge: 455
Registriert: 05.07.2003, 11:17

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Lord Delvin »

Danke für die Erklärung. Mir war nicht bewusst, dass Allokationen an sich als beobachtbarer Seiteneffekt gelten.
XML/JSON in schnell: OGSS
Keine Lust mehr auf C++? Versuche Tyr: Get & Get started
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Ein paar schicke Schalter, die ich noch gefunden habe:

/external:anglebrackets /external:W0

Teilt Visual C++ mit, dass
1) alle #includes, die in <spitzen Klammern> stehen, nicht zum Projekt gehören;
2) für nicht-Projekt-#includes das Warnlevel 0 gilt.
Kurz: Damit werden euch keine Warnungen mehr in Windows-Headern oder STL-Headern gemeldet. Clang & GCC haben solche Einstellungen für Linux schon ewig; nun geht das auch auf Windows. Mich spammte z. B. schon lange ein zweifelhaftes Makro in winbase.h mit Warnungen zu; nun ist endlich Ruhe.

Wer es feiner granuliert haben möchte als „spitze Klammern“: Geht auch für bestimmte Pfade oder via Umgebungsvariable.

/X

Teilt Visual C++ mit, dass es auf der Suche nach #includes nicht die Umgebungsvariablen PATH und INCLUDE durchsuchen soll. Nur für Leute, die ihren Compiler portabel auf dem USB-Stick haben, dort aber überlebenswichtig – damit er lokale Header nutzt, und nicht die auf dem System installierten.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Neues Ticket: https://developercommunity.visualstudio ... s/10139992

Wenn man in Debug-Builds den Smaller Type Check nutzt (/RTCc), werden die Ergebnisse von Funktionen, die ein bool zurückgeben, unnötigerweise auf gültigen Wertebereich geprüft. Dadurch verdoppeln sich die Funktionsaufrufe (jede Funktion ruft zusätzlich _Rtc_check_4_to_1() auf).

Man erkenne das Muster mit:
Krishty hat geschrieben: 20.07.2021, 16:01 Trotz 15 Upvotes (woher auch immer!) bleibt bool in Visual C++ erstmal langsamer als int.

https://developercommunity.visualstudio ... int/932580

Nachdem Microsoft endlich wieder C aufgegriffen hat, ist es vielleicht ganz nützlich, wenn C bei ihnen schneller als C++ bleibt ;)
Ich glaube, dass sie bool nie richtig implementiert haben. Bedingungen waren bis in die 90er grundsätzlich int, und als man für C++ einen Typ bool brauchte, hat man halt bei Bedarf zu char gecastet. Die Workarounds sind drin geblieben und haben sich etabliert, und niemand möchte das Risiko eingehen, das geradezuziehen und einen echten Boolschen Typ einzuführen, wie z. B. Clang/LLVM ihn intern haben.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Lord Delvin
Establishment
Beiträge: 455
Registriert: 05.07.2003, 11:17

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Lord Delvin »

Krishty hat geschrieben: 06.09.2022, 08:31 [...], wie z. B. Clang/LLVM ihn intern haben.
Ist meines Wissens in LLVM nicht der Fall, aber der Support für i1 ist ziemlich gut. Hab' letztens meine Übersetzung von if/... auf gewissermaßen switch (bool) umgestellt und da passiert letztlich genau dasselbe, weil es ordentlich normalisiert und dann optimiert wird. Für Clang hast du wahrscheinlich Recht.
XML/JSON in schnell: OGSS
Keine Lust mehr auf C++? Versuche Tyr: Get & Get started
udok
Beiträge: 18
Registriert: 01.02.2022, 17:34

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von udok »

Krishty hat geschrieben: 06.02.2021, 22:50
Krishty hat geschrieben: 29.08.2018, 10:22Wenn ihr z.B. float zu int konvertiert, ist reinterpret_cast undefiniertes Verhalten (obwohl Visual C++ es frisst) und union ebenfalls (obwohl GCC/Clang es fressen). Um standardkonform zu sein, solltet ihr das via memcpy() machen (ist übrigens auch in allen aktuellen Compilern die am besten optimierte Methode).
Oh fucking fuck, es war ja klar, dass ich mich mit meinem Lehrbuchwissen in die Scheiße setze.

Nein, für float-zu-int & Co. auf Visual Studio ist memcpy() leider nicht die beste Methode. Für alle Bit-Casts, die nur mit Integer-Typen zu tun haben, ja. Sobald float und double im Spiel sind, nein.

static uint64_t asuint64(double const d) {
// Outperforms memcpy() on Visual C++ 2019
return _mm_cvtsi128_si64(_mm_castpd_si128(_mm_set_sd(d)));
}
Ich wollte anmerken, dass ältere Compiler _mm_castpd_si128 nicht kennen. Konkret der WinDDK 7.10 / VS2008. Ok, der ist alt - aber ich verwende den gerne, weil er trotz C++ Schwächen sehr guten Code produziert, der oftmals kleiner und schneller ist als der vom VS2022. Auf jeden Falls sind die Compilezeiten sehr viel kürzer, und er linkt mit der alten, langsamen und nicht immer standardkonformen mscvcrt.lib, prodziert damit aber eine sehr viel kleinere exe. Ich kompiliere alles mit -O1, weil das bei mir kleiner und ca gleichschnell ist als -O2.

Eine einfache Alternative, die denselben Assembler Code generiert ist: "return *(UINT64*) & d;"
_mm_set_sd() hat den Nachteil, dass das double Argument ja schon im xmm0 Register drinnen ist wegen der Aufrufkonventionen.
Nun optimieren ältere Compiler das _mm_set_sd() nicht weg, sondern kopieren das xmm0 Register ins xmm1 Register, und schaufeln dann das xmm1 Register ins rax Ergebnisregister... der neueste 2022 cl optimiert das aber zum Glück auch weg.
udok
Beiträge: 18
Registriert: 01.02.2022, 17:34

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von udok »

Ich habe jetzt mal schnell ein Hello-World Program in C und C++ mit WinDDK 7.10 und VS2022 kompiliert.
Der VS2022 kompiliert gefühlt 4x so lange, und das Ergebnis ist trotz all dem frischen Gehirnschmalz schlechter.
Alles ist statisch gelinkt.

C mit Win32 wsprintf (ohne C-Runtime)
VS2022: 566 Bytes (2560 Bytes auf der SSD)
WinDDK: 278 Byte (2048 Bytes auf der SSD)

C:
VS2022: 118 kB
WinDDK: 42 kB

C++
VS2022: 212 kB
WinDDK: 89 kB
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

udok hat geschrieben: 07.09.2022, 09:54Ok, der ist alt - aber ich verwende den gerne, weil er trotz C++ Schwächen sehr guten Code produziert, der oftmals kleiner und schneller ist als der vom VS2022.
Kleiner verstehe ich, aber schneller? Vor allem mit SSE sollte alles vor VS 2017 (oder sogar 2019, hab’s vergessen) drastisch langsamer sein …
Eine einfache Alternative, die denselben Assembler Code generiert ist: "return *(UINT64*) & d;"
Wie gesagt, Undefined Behavior. Ist schön, dass es mit dieser konkreten Compiler-Version funktioniert, sollte man aber unbedingt auf diese beschränken.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
udok
Beiträge: 18
Registriert: 01.02.2022, 17:34

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von udok »

Kleiner verstehe ich, aber schneller? Vor allem mit SSE sollte alles vor VS 2017 (oder sogar 2019, hab’s vergessen) drastisch langsamer sein …
Der rechenintensive SSE Teil verwendet Bibliotheksfunktionen oder Intel Intrinsics.
Bei meinen Tests ist O1 etwa gleich schnell wie O2. Der Code ist aber hardwarenahe geschrieben.
Alignment spielt auf einer aktuellen CPU keine Rolle mehr, O1 braucht weniger uOps und L1 Cache, und wenn die CPU den Asm Code erst mal dekodiert hat und die Sprungvorhersage passt dann hat auch ein Funktionsaufruf 0 Kosten. Der grösste Bottleneck ist heute das Speicherinterface.
Wie gesagt, Undefined Behavior. Ist schön, dass es mit dieser konkreten Compiler-Version funktioniert, sollte man aber unbedingt auf diese beschränken.
Es ist immer irgendwie UB, wenn man das Bitmuster eines Doubles in einen Integer kopiert, ein Double kann ja auch 4, oder 10 Bytes oder auch 16 Bytes lang sein...

Also egal wie man es macht, der Programmierer muss genau wissen was er da tut, und setzt voraus, dass sizeof(double)==sizeof(integer-type).
In weiterer Folge macht er das ja, um auf einzelne Bits direkt zuzugreifen, er setzt also voraus, dass ein double genau so ist wie er es halt glaubt.
Läuft das ganze auf der neuesten CPU im Jahr 2072, gibt das ziemlich sicher ein UB.

Ich würde mal sagen, dass die Cast Methode universeller ist, als die kryptischen ineinander verschachtelten Aufrufe von Intel Intrinsic,
oder die schnarchlangsame memcpy Version.

Die Cast Methode versteht jeder Compiler, und >50% der Programmierer können das noch entziffern.

Die Intrinisc Methode verstehen vielleicht noch 1-2 % der Programmierer, die ist CPU abhängig und läuft nur mit genau 3 aktuellen Compilern, die nicht älter als ein paar Jahre sein sollten.

Die memcpy Methode geht effizient auch nur mit den 3 aktuellen Compilern, aber sicher nicht mit einem exotischen DSP.
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

udok hat geschrieben: 08.09.2022, 10:14
Kleiner verstehe ich, aber schneller? Vor allem mit SSE sollte alles vor VS 2017 (oder sogar 2019, hab’s vergessen) drastisch langsamer sein …
Der rechenintensive SSE Teil verwendet Bibliotheksfunktionen oder Intel Intrinsics.
Bei meinen Tests ist O1 etwa gleich schnell wie O2. Der Code ist aber hardwarenahe geschrieben.
Alignment spielt auf einer aktuellen CPU keine Rolle mehr, O1 braucht weniger uOps und L1 Cache, und wenn die CPU den Asm Code erst mal dekodiert hat und die Sprungvorhersage passt dann hat auch ein Funktionsaufruf 0 Kosten. Der grösste Bottleneck ist heute das Speicherinterface.
Nein, darum geht es nicht: Visual Studio vor 2017/2019 hat äußerst begrenztes Verständnis davon, was die Intrinsics tun, und optimiert entsprechend schlecht. Wenn du bspw. mehrere verschiedene Variablen mit _mm_setzero_ps() initialisierst (oder mehrfach überschreibst), werden die nicht zusammengefasst, weil Visual C++ nicht rafft, dass das Intrinsic keine Nebenwirkung hat. Wenn du eine Vektorklasse mit Intrinsics benutzt, wird Visual C++ vor 2017/2019 quasi keine redundanten Ausdrücke reduzieren und nichts weiter optimieren. Du musst Assembler-mäßig die komplette Berechnung als einen Block Code runterschreiben und händisch optimieren.

Seit 2017/2019 ist das drastisch verbessert; du kannst nun z. B. eine Vektorklasse mit SSE-Intrinsics programmieren und bei Benutzung wird alles recht gut von Visual C++ optimiert.
udok hat geschrieben: 08.09.2022, 10:14Es ist immer irgendwie UB, wenn man das Bitmuster eines Doubles in einen Integer kopiert
Nein. std::bit_cast() und memcpy() sind extra dafür spezifiziert, dabei kein UB zu sein. Aber die Größe muss stimmen, klar (static_assert(sizeof(float) == sizeof(int));).
udok hat geschrieben: 08.09.2022, 10:14Ich würde mal sagen, dass die Cast Methode universeller ist
Die ist aber leider auch universell falsch.

Nehmen wir mal an, du benutzt GCC 4.1.2. Der ist ungefähr aus der Ära WinDDK 7. Du hast eine Funktion dieser Art geschrieben:

Code: Alles auswählen

int foo(int * i) {
    int result = *i;
    *(float*)i = 1.0f;
    result = *i;
    return result;
}
Du kompilierst mit gcc -O2. Die Funktion gibt nicht das Bitmuster von 1.0f zurück, sondern 0 (Beweis):

  foo(int*):
    mov %eax, DWORD PTR [%rdi]
    mov DWORD PTR [%rdi], 0x3f800000
    ret


Kurz darauf merkst du, dass -O1 besser als -O2 ist, weil weniger Bloat im Executable landet. Du stellst um auf gcc -O1, und … nun gibt sie 0x3f800000 zurück:

  foo(int*):
    mov DWORD PTR [%rdi], 0x3f800000
    mov %eax, DWORD PTR [%rdi]
    ret


… wie auch ein paar Versionen danach mit -O2.

Wenn du weißt, was du tust, deinen Compiler nie aktualisierst, und deine Compiler-Einstellungen nie änderst, ist das okay für dich. Aber auf die meisten Projekte trifft keiner der drei Punkte zu, und „universell“ ist das garantiert nicht.

Die universelle Methode ist memcpy() oder std::bit_cast().

Wie gesagt, für deinen konkreten Compiler mit deinen konkreten Einstellungen mag das funktionieren. Aber empfiehl das hier bitte niemandem, denn immer wenn Kollegen so einen Unsinn in ihren Code klatschen, landet das ein Bisschen später als Arbeit auf meinem Schreibtisch.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
udok
Beiträge: 18
Registriert: 01.02.2022, 17:34

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von udok »

Ich komme aus der HW nahen Programmierung, mit zeitkritischen Sachen auf einem DSP, ARM Cortex und früher auch 8051.
Da kommt es schon vor, auf write-only Speicheraddressen zu schreiben, um etwa einen ADC zu triggern.
Da ist es wichtig, dass der Compiler den C Code nicht durcheinanderwürfelt.
Der Compiler soll im Wesentlichen das machen, was ich in C hinschreiben. Oder zumindest soll es nachvollziehbar sein.
Und wenn ich zwei _mm_setzero_ps() hintereinander schreibe, dann soll das auch so im ASM drinnen stehen...
Mit den superoptimierenden Compilern tue ich mir noch schwer, auch wenn ich die Vorteile sehe.

Die Sache mit UB sehe ich pragmatischer, solange das getestet ist und nicht zufällig reinrutscht.
UB heisst ja nur, das der Standard dazu keine Meinung hat, und dem Compilerbauer die Freiheit lässt was Sinnvolles zu machen.

In deinem Beispiel kapiert der alte GNU Compiler nicht, dass zwei Adressen mit unterschiedlichem Typ auf denselben Speicher zeigen.
Ja, das ist ein Problem, da müsste man das ganze in einer Funktion kapseln... Ok dann sind wir bei sowas wie memcpy.
Das kostet halt bei etlichen Compilern richtig viel Zeit. Wenn meine zeitkritische Motorsteuerung dann zum ruckeln anfängt ist das nicht schön.

Und wenn in 5 Jahren Apple die Desktop Welt dominiert, und überall ihre ARM CPUs laufen, dann werden die _mm_ Makros nicht
mehr funktionieren. Da nützt es nichts, dass das kein UB ist. Die Cast Methode geht aber immer noch.

Was mich in der modernen C++ Welt viel mehr stört: Es ist immer schwieriger Projekte mit einem anderen Compiler oder SDK Version zu bauen.
SW-Entwicklung ist ziemlich fragil geworden, trotz besserer Standards und weniger UB's im Code wird das SW-Verfalldatum immer kürzer.

Ich bewundere Leute, die es heute noch schaffen, am Desktop gute Programme zu schreiben. Nochdazu ist SW Entwicklung am PC fast schon ein brotloser Job geworden.
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

udok hat geschrieben: 09.09.2022, 15:47 Ich komme aus der HW nahen Programmierung, mit zeitkritischen Sachen auf einem DSP, ARM Cortex und früher auch 8051.
Da kommt es schon vor, auf write-only Speicheraddressen zu schreiben, um etwa einen ADC zu triggern.
Da ist es wichtig, dass der Compiler den C Code nicht durcheinanderwürfelt.
Der Compiler soll im Wesentlichen das machen, was ich in C hinschreiben. Oder zumindest soll es nachvollziehbar sein.
Und wenn ich zwei _mm_setzero_ps() hintereinander schreibe, dann soll das auch so im ASM drinnen stehen...
Dann ist C dort die falsche Sprache, weil Speicherzugriffe kein hinreichend definierter Teil des Sprachmodells sind. Wenn du den Assembler-Code definieren willst, nutz Assembler.
Die Sache mit UB sehe ich pragmatischer, solange das getestet ist und nicht zufällig reinrutscht.
UB heisst ja nur, das der Standard dazu keine Meinung hat, und dem Compilerbauer die Freiheit lässt was Sinnvolles zu machen.
Das wäre Implementation-Defined Behavior, nicht Undefined Behavior!

Das Beispiel mit Mac und Cortex ist total daneben: memcpy() bzw. std::bit_cast() ist die Lösung, die auch in 100 Jahren noch auf jeder CPU funktioniert. Cast nämlich gerade genau nicht.

Der Rest pendelt zwischen falsch und Widersprüchlich. Du kannst schlecht überall (wissentlich!) Undefined Behavior einbauen, und dich dann beschweren, dass alte Projekte mit modernen Compilern nicht mehr richtig gebaut werden können. Ich hoffe, du erkennst das Problem.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
xq
Establishment
Beiträge: 1557
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von xq »

udok hat geschrieben: 09.09.2022, 15:47 Ich komme aus der HW nahen Programmierung, mit zeitkritischen Sachen auf einem DSP, ARM Cortex und früher auch 8051.
Da kommt es schon vor, auf write-only Speicheraddressen zu schreiben, um etwa einen ADC zu triggern.
Da ist es wichtig, dass der Compiler den C Code nicht durcheinanderwürfelt.
Und genau dafür gibt es im C-Standard volatile, welches den reinen Zugriff auf den Speicher als Side Effect markiert, damit darf der compiler auch nichts mehr durcheinander würfeln, weglassen oder sortieren. Und damit hat sich jegliches Problem mit MMIO-Code gelöst.
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

xq hat geschrieben: 10.09.2022, 11:13
udok hat geschrieben: 09.09.2022, 15:47 Ich komme aus der HW nahen Programmierung, mit zeitkritischen Sachen auf einem DSP, ARM Cortex und früher auch 8051.
Da kommt es schon vor, auf write-only Speicheraddressen zu schreiben, um etwa einen ADC zu triggern.
Da ist es wichtig, dass der Compiler den C Code nicht durcheinanderwürfelt.
Und genau dafür gibt es im C-Standard volatile, welches den reinen Zugriff auf den Speicher als Side Effect markiert, damit darf der compiler auch nichts mehr durcheinander würfeln, weglassen oder sortieren. Und damit hat sich jegliches Problem mit MMIO-Code gelöst.
Die volatile-Semantik ist aber auch alles andere als trivial.

Für solche Einsatzzwecke kenne ich aus dem beruflichen Umfeld eigentlich nur ST, SFC, und die ganzen anderen Dinge aus dem IEC 61131-3-Umfeld. Dort sind Variablenwerte (flüchtig oder nichtflüchtig) explizit beobachtbarer Teil des Programmverhaltens. PLC-Programmierung in C/C++ ist meiner Erfahrung nach eher exotisch. Ich finde, dass C/C++ da grundsätzlich das falsche Werkzeug wäre.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
dot
Establishment
Beiträge: 1682
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von dot »

udok hat geschrieben: 09.09.2022, 15:47 Die Sache mit UB sehe ich pragmatischer, solange das getestet ist und nicht zufällig reinrutscht.
UB heisst ja nur, das der Standard dazu keine Meinung hat, und dem Compilerbauer die Freiheit lässt was Sinnvolles zu machen.
Nope, UB heißt eben genau dass du dich nicht darauf verlassen kannst, dass das Programm in diesem Fall irgendwas Bestimmtes (oder gar Sinnvolles) tut. UB heißt nicht, dass der Compiler beim Kompilieren deines Codes sich denkt "aha, da macht es UB, überlegen wir mal, wie wir dieses UB heute implementieren wollen". Das ist interessanterweise genau das Gegenteil davon, was wirklich passiert. Sprachen wie C und C++ haben UB weil es Dinge gibt, die beim Kompilieren eines Programmes rein prinzipiell unmöglich oder zumindest impraktikabel zu detektieren sind. Beispiel: out-of-bounds Zugriff auf ein Array; der Compiler kann im Allgemeinen nicht wissen, ob ein bestimmer Array-Access out-of-bounds sein wird oder nicht, weil das im Allgemeinen von Kontrollfluss und Werten abhängt, die erst zur Laufzeit bestimmt sind.

Effektiv gibt es da nun zwei Möglichkeiten: a) du lässt den Compiler um jeden Array-Access Laufzeitchecks einbauen, die sicherstellen, dass kein Access jemals out of bounds geht oder b) du sagst dass out-of-bounds Access UB bedeutet. Viele "moderne" Sprachen gehen Weg a) und oft ist das gut genug weil Branch-Prediction etc. diese Checks auf modernen Desktop CPUs relativ billig machen. Das Problem mit a) ist, dass ein korrektes Programm, wo niemals out-of-bounds Accesses passieren, nun völlig unnötigen Overhead enthält. Selbst wenn dank Branch-Prediction etc. dieser Overhead in der Praxis oft sehr klein ausfällt, ist des dennoch unnötiger Overhead: Mehr Maschinencode der durch den Speicher und die CPU muss. Auch wenn es am Ende keine großen Auswirkungen auf die Laufzeit hat, das sind immer noch Speicher und Taktzyklen, die auch für tatsächlich nützliche Instruktionen hätten verwendet werden können. Und nicht alle Prozessoren haben derart komplexe Pipelines wie moderne Desktop CPUs. Auf GPUs beispielsweise wäre Overhead dieser Art absolut inakzeptabel.

Sprachen mit UB dagegen machen nicht nur keine Checks, sondern stellen die Sache effektiv komplett auf den Kopf. Nachdem es rein prinzipiell unmöglich ist, zu entscheiden, ob ein gegebenes Programm UB enthält oder nicht, drehen wir den Spieß doch einfach um und nutzen ihn dazu, besseren Code für korrekte Programme zu generieren. Nachdem ein Programm per Definition nicht davon abhängen kann, was im Falle von UB passiert, ist im Falle von UB jedes Verhalten gleich gut. Oder anders rum: Uns kann völlig egal sein, was der generierte Code im Falle von UB macht. UB ist effektiv alles, was uns nicht kümmert. UB sind effektiv alle Corner-Cases, die wir nicht behandeln müssen. D.h. der Compiler kann also einfach Code generieren unter der Annahme dass dein Programm niemals etwas tuen wird, das in UB resultieren würde, weil wenn es sowas täte, dann ist ja ganz egal was für Maschinencode dann läuft, weil du dich sowieso nicht drauf verlassen konntest. "UB ist alles, was nicht passieren kann" ist ein zentrales Axiom in modernen optimierenden Compilern. Sehen wir uns z.B. mal an, was Clang mit -O3 aus

Code: Alles auswählen

int fun();

int test(int* x)
{
    *x = 42;

    if (!x)
        return fun();

    return *x;
}
macht:

Code: Alles auswählen

test(int*):                              # @test(int*)
        mov     dword ptr [rdi], 42
        mov     eax, 42
        ret
Beachte, dass der generierte Maschinencode keine Branch mit Aufruf von fun enthält. In dem Moment wo der Compiler sieht, dass du *x = 42 machst, weiß der Compiler, dass x niemals nicht auf ein valides Objekt zeigen kann. Insbesondere kann x niemals ein Nullpointer sein, weil dann hättest du hier UB gemacht. Und UB kann nicht passieren. D.h. der einzige Weg, über den das if jemals genommen und fun jemals aufgerufen werden könnte, führt über UB, kann also nicht passieren, kann also alles wegoptimiert werden. Genau so ein Bug war afaik übrigens mal für eine Sicherheitslücke im Linuxkernel verantwortlich. Der Compiler konnte einen Check ob der Benutzer über bestimmte Rechte verfügt einfach wegoptimieren, weil der Code zuvor was gemacht hat, was er nur hätte machen können, wenn die Dinge, die der spätere Check gechecked hat, Werte gehabt hätten, unter denen der Check niemals erfolgreich ausgehen konnte…

Oder dieser Code hier

Code: Alles auswählen

int fun()
{
}
resultiert (Clang mit -O3) in

Code: Alles auswählen

fun():                                # @fun()
Man beachte, dass der generierte Maschinencode nicht einfach Müll returned, sondern einfach gar nicht returned (keine ret Instruction). Ja der Maschinencode besteht as keiner einzigen Instruction, ein Aufruf dieser Funktion läuft also einfach weiter und führt was auch immer nach dieser Funktion zufällig so im Speicher rumliegt aus als wäre es Code. Wie kommt es dazu? Der Compiler sieht, dass der einzig mögliche Kontrollfluss durch diese Funktion in der } endet, ohne dass ein return Statement angetroffen wird. Das wäre UB. Nachdem kein möglicher Kontrollfluss durch diese Funktion nicht in UB endet, kann diese Funktion in einem korrekten Programm niemals aufgerufen werden (denn jeglicher Aufruf der Funktion wäre UB). UB kann nicht passieren. D.h. wir müssen für diese Funktion auch keinen Maschinencode emitten. Fun Fact: kompiliert man das selbe Programm nicht als C++ sondern als C, bekommt man auf einmal eine ret Instruction, weil das Verlassen einer Funktion ohne return Statement in C nicht direkt UB ist (UB tritt in C erst ein, wenn der Aufrufer das Return-Value anschaut). Ich hab Situationen gesehen, wo der selbe Code kompiliert als C++ (vermutlich) aus diesem Grund besseren Maschinencode generiert als wenn man ihn (mit dem selben Compiler!) als C kompiliert. UB bedeutet für den Compiler Freiheit zur Optimierung.

Darin liegt imo der Schlüssel zum Verständis von UB. UB ist nicht etwas wo der Compiler bewusst frei entscheidet, was er in diesem Fall tut. Der Compiler kann im Allgemeinen nicht wissen wann und wo und ob in in einem gegebenen Programm UB auftritt oder nicht. Was der Compiler aber machen kann ist, sich anzuschauen in welchen Fällen im gegebenen Programm UB auftreten würde und daraus dann Annahmen abzuleiten, über die das mögliche Verhalten des Programmes eingegrenzt und somit dann effizienterer Code generiert werden kann.

Diese Idee, dass man wissen kann was bei bestimmten UB passiert, wenn man nur genug davon versteht, ist leider ein weit verbreiteter Irrtum. Ein wunderbares Beispiel für "a little bit of knowledge is a dangerous thing". Wer wirklich genug davon versteht, der versteht, dass man in der Praxis wirklich eben genau nicht wissen kann was passiert. Klar, optimierende Compiler sind auch nur Software und wenn man wirklich alle Variablen kennt und berücksichtigt, dann kann man rein theoretisch auch tatsächlich vorhersagen, was am Ende rauskommt. Das Problem ist nur, dass es so viele Variablen und komplexe Zusammenhänge gibt, dass das Gesamtsystem effektiv chaotisches Verhalten an den Tag legt. Jede auch noch so kleine Änderung an irgendeiner auch noch so unwesentlichen Stelle kann eine Kaskade an globalen Konsequenzen auslösen, die dann auf einmal Auswirkungen auf die Codegen in einem vermeintlich völlig unabhängigen Teil des Programms hat. Something Something Butterfly-Effect. Stell dir nur vor, du änderst irgendwo eine Funktion und auf einmal ist die nun kurz genug dass die beim Linken durch LTO über mehrere Module hinweg geinlined wird. Und die Funktion in die sie geinlined wurde ist nun auf einmal auch simpel genug dass die Heuristik sagt: inlinen. Und die Funktion in die diese Funktion nun auf einmal gelined wur… Und auf einmal hast du völlig anderen Maschinencode irgendwo an einer Stelle in deinem Programm, wo deine nun geinlinete Funktion über vier Ecken durch 2 verschiedene third-party Libraries hindurch indirekt aufgerufen wird. Und nachdem da auf einmal anderer Maschinencode rauskommt, beeinflusst das die Registerallokation. Und nun kommen 300 Zeilen weiter unten auf einmal auch andere Instruktionen raus, weil die Instruktionen die wir dort zuvor verwendet hatten konnten nur mit Registern arbeiten, die dort unten nun aber leider dirty sind oder in einer ungünstigen False-Dependency resultieren würden oder was auch immer. Und auf einmal ist die Art und Weise wie dein UB, das du da unten hattest, sich manifestiert eine völlig andere. Und alles ausgelöst durch eine scheinbar harmlose kleine Änderung an einer Stelle in deiner Codebase die nicht nur nichtmal in der selben Funktion, sondern die zehntausende Zeilen und viele Source Files und zwei Libraries entfernt von dem Punkt liegt, wo du das eigentliche UB gemacht hattest.

Und das Ganze funktioniert auch in die andere Richtung. Die Tatsache dass du da unten UB machst, kann indirekt plötzlich auch die Codegen aus deiner geinlineten Funktion, die selbst kein UB macht beeinflussen. Beispiel:

Code: Alles auswählen

extern int answer;

inline void fun()
{
    answer = 42;
}

void test(int* x)
{
    if (x)
        return;

    fun();

    *x = 42;
}
erzeugt

Code: Alles auswählen

test(int*):                              # @test(int*)
        ret
Die funktion fun selbst macht kein UB. Aber der Aufruf von fun in test kann nur über einen Pfad erreicht werden, der am Ende UB machen würde (x kann dort nur ein Nullpointer sein, weil alles andere hätte vorher schon returned). Daher wird auch der ganze Effekt von fun wegoptimiert, obwohl da noch gar kein UB passiert wäre. Der einzige Pfad der kein UB macht, ist der wo fun nicht aufgerufen wird und somit wird auch nur für den Pfad Code generiert.

Das ist auch, wieso UB nicht nur eine Eigenschaft einer einzelnen Operation, sondern des gesamten Programmes an sich ist. Das Verhalten eines Programmes ist nicht nur undefiniert von dem bestimmten Punkt ab, wo UB gemacht wird. Das Verhalten eines Programmes das UB enthält ist gänzlich undefiniert. Du kannst dich nicht nur auf nichts verlassen nachdem du UB gemacht hast, du kannst dich auch nicht auf irgendwas verlassen, was passiert, bevor du UB machst…

tl;dr: UB heißt nicht "der Compiler entscheidet was passiert". Das wäre implementation-defined oder unspecified Behavior. In diesen Fällen gibt es ein Behavior, du weißt nur nicht unbedingt, was das Behavior ist. UB dagegen bedeutet die Abwesenheit von jeglichem Behavior. UB heißt effektiv "dieser Fall wird nicht behandelt". Was im Fall von UB passiert hängt nicht davon ab, wie der Compiler sich entscheidet, das UB zu implementieren, sondern davon, wie der Compiler sich entscheidet, literally alles andere zu Implementieren.
Zuletzt geändert von dot am 12.09.2022, 03:41, insgesamt 13-mal geändert.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4515
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Schrompf »

Spannende Ausführungen, dot. Danke.
Häuptling von Dreamworlds. Baut an was Neuem. Hilft nebenbei nur höchst selten an der Open Asset Import Library mit.
Benutzeravatar
xq
Establishment
Beiträge: 1557
Registriert: 07.10.2012, 14:56
Alter Benutzername: MasterQ32
Echter Name: Felix Queißner
Wohnort: Stuttgart & Region
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von xq »

Krishty hat geschrieben: 10.09.2022, 15:31 Die volatile-Semantik ist aber auch alles andere als trivial.
Tatsächlich ist die volatile-Semantik basically äquivalent zu einem function call einer externen Funktion
C99 Standard hat geschrieben: 5.1.2.2.3.2 Accessing a volatile object, modifying an object, modifying a file, or calling a function
that does any of those operations are all side effects,11) which are changes in the state of
the execution environment.

6.7.3.6 An object that has volatile-qualified type may be modified in ways unknown to the
implementation or have other unknown side effects. Therefore any expression referring
to such an object shall be evaluated strictly according to the rules of the abstract machine,
as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the
object shall agree with that prescribed by the abstract machine, except as modified by the
unknown factors mentioned previously.116) What constitutes an access to an object that
has volatile-qualified type is implementation-defined.
Krishty hat geschrieben: 10.09.2022, 15:31 Für solche Einsatzzwecke kenne ich aus dem beruflichen Umfeld eigentlich nur ST, SFC, und die ganzen anderen Dinge aus dem IEC 61131-3-Umfeld. Dort sind Variablenwerte (flüchtig oder nichtflüchtig) explizit beobachtbarer Teil des Programmverhaltens. PLC-Programmierung in C/C++ ist meiner Erfahrung nach eher exotisch. Ich finde, dass C/C++ da grundsätzlich das falsche Werkzeug wäre.
Jeglicher Microcontroller, Kernel oder sonstiges benötigt volatile für seine MMIO-Geschichten, die Alternative dazu ist nur volatile inline assembly, welches ungefähr den selben Regeln folgt und nicht sortiert oder wegoptimiert werden darf. Mehr oder weniger jegliche Interaktion mit Hardware benötigt solche Speicherzugriffe.

Ganz schönes Beispiel ist das Unlocking von PLL-Registern:

Code: Alles auswählen

*PLL_FEED_REG = 0x55555555U;
*PLL_FEED_REG = 0xAAAAAAAAU;
*PLL_FEED_REG = 0x55555555U;
*PLL_FEED_REG = 0xAAAAAAAAU;
Was am Ende auch exakt die 4 Speicherzugriffe erzeugt. PLCs, die man in ST oder SFC programmiert, sind leistungstechnisch schon meist weit jenseits von dem, was ein Microcontroller kann (ATmega328p hat 2048 Byte RAM und 32k Flash).
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…

Programmiert viel in ⚡️Zig⚡️ und nervt Leute damit.
Benutzeravatar
Jonathan
Establishment
Beiträge: 1962
Registriert: 04.08.2004, 20:06
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Jonathan »

Jap, schöne Erläuterung von dot.

Jetzt stellt sich mir folgende Frage: Ich bin ja gerne prinzipiell brav und mache keinen Quatsch der eigentlich UB ist, aber vermutlich trotzdem passiert. Jedenfalls nicht absichtlich. Aber aus versehen passiert das natürlich. Und wie gesagt, UB ist global, d.h. es kann passieren dass man irgendwo einen Bug einbaut, danach ein halbes Jahr weiter programmiert und dann irgendetwas einbaut, das katastrophale Konsequenzen hat. Der Fehler ist dann nicht nur im Code weit weg, sondern liegt auch zeitlich weit zurück, d.h. er ist sau schwer zu finden.

Wie findet man ihn trotzdem? Klar, ich denke wenn man einen Debug-Build mit wenig / keine Optimierung hat, wirds danach prinzipiell laufen. Aber langfristig will man ja sowohl Optimierung wieder einschalten, als auch das UB wegbekommen. Was ich mir vorstellen könnte wäre eine bestimmte Kompillierfunktion die für alles Laufzeitchecks einfügt, die dann z.B. Zeiger und Arrayzugriffe überprüfen. Mir ist klar, weshalb das zur Kompilierzeit nicht funktioniert, aber wenn man ein Programm hat, dass wegen UB abstürzt hat man ja schon einen Codepfad (inklusive passender Eingabedaten) gefunden, der irgendwo mal UB triggert und die allermeisten Fälle von UB müsste man doch mit automatisch generierten Laufzeitchecks abfangen können, oder? Gibt es da generelle Strategien? Oder benutzt man einfach sowas wie Valgrind und Dr. Memory und die finden eigentlich schon alle Fehler?
Lieber dumm fragen, als dumm bleiben!
Benutzeravatar
Krishty
Establishment
Beiträge: 7851
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Sammelthread zu Visual C++’ Compiler

Beitrag von Krishty »

Jonathan hat geschrieben: 21.09.2022, 19:06Wie findet man ihn trotzdem? […] Gibt es da generelle Strategien? Oder benutzt man einfach sowas wie Valgrind und Dr. Memory und die finden eigentlich schon alle Fehler?
  • Compiler-Warnungen prüfen und Compiler regelmäßig aktualisieren – bei viel statisch erkennbarem UB warnen aktuelle Compiler
  • Mit Clang, GCC, und MSVC kannst du Undefined Behavior Sanitizer aktivieren. Das erfordert je nach Plattform und Umgebung etwas Setup, aber im Resultat wird ein Haltepunkt ausgelöst, wenn UB erkannt wird. Ich habe den bei meinen Clang-Debug-Builds immer an.
UBSan ist krass. Ich bin ja pingelig mit UB und Warnungen, aber auch ich bin erstmal in Fehler über Fehler gerannt. Insbesondere hat mein Code zum Laden von Dateiformaten ständig Typen mit falschem Alignment gelesen.

(Z. B. reinterpret_cast<int const *>(fileptr + 18), um die Breite einer Bitmap zu lesen – int darf aber nur an Adressen liegen, die an vier Bytes ausgerichtet sind. Darum bestehe ich jetzt immer auf memcpy() für sowas.)

Edit: Entschuldige, MSVC unterstützt nur Address Sanitizer, nicht Undefined Behavior Sanitizer. Du brauchst dafür also Clang oder GCC.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Antworten