From 75c0d66b9190a915b99457b0eebcc897956e3d5f Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 4 Sep 2026 23:41:42 +0200 Subject: [PATCH] Parameters: work out a solution for an optimised code-path Overall, this is a questionable endeavour... - I want a framework that can be extended flexibly - I am concerned about the performance penalty of that flexibility What makes this situation problematic is the fact that I am deep in prototyping work here, and have not the slightest idea as to what would be the actual usage context. So I am lacking a point of reference, yet still I am engaged into a very tricky (premature) optimisation... Anyway. I have now found a way to integrate an optimised access code-path in a safe way, while retaining the general case with flexible type conversion: - whenever a BaseDomain is used, a corresponding type-ID is marked into the ID-Record - this allows when it is possible to side-step any type conversion, - in which case we can also forcibly side-step the virtual dispatch, and invoke directly the value-accessors of the default implementation, reaching into the embedded value buffer. --- src/lib/par/disposition.hpp | 58 +++-- tests/lib/par/parameter-test.cpp | 12 - wiki/thinkPad.ichthyo.mm | 432 ++++++++++++++++++++++++++++--- 3 files changed, 429 insertions(+), 73 deletions(-) diff --git a/src/lib/par/disposition.hpp b/src/lib/par/disposition.hpp index 2c9ba36b0..b97dc5d66 100644 --- a/src/lib/par/disposition.hpp +++ b/src/lib/par/disposition.hpp @@ -55,12 +55,24 @@ namespace par { template void setVal (X); - private: + protected: template - bool isBaseDomainMatch() const; + bool + isBaseDomainMatch() const + { + return idr_.baseTypeID == BaseTypeID(); + } + + template + void + markBaseDomain() + { + idr_.baseTypeID = BaseTypeID(); + } }; + /** * A complete configuration and setup of a parameter, including a type specification * with domain and scale, and a value provision. Can be generated through a builder @@ -73,20 +85,25 @@ namespace par { VAL val_; Provision* src_; - /* === Provision Interface === */ - void retrieveInto (ValBuff&) const override; - void setValFrom (ValBuff const&) override; - public: ParamData (VAL initVal) : val_{std::move (initVal)} , src_{nullptr} - { } + { + Disposition::markBaseDomain(); + } + + /* === Provision Interface === */ + void retrieveInto (ValBuff&) const override final; ///< @note is sometimes invoked non-virtually for sake of optimisation + void setValFrom (ValBuff const&) override final; }; template using FullDomain = BaseDomain; + template + using SimpleData = ParamData; + template @@ -105,6 +122,15 @@ namespace par { + /** + * @warning optimisation to a direct access will be applied + * when the baseTypeID in the IDRecord matches the type \a X. + * It is assumed in this case that the actual implementation of + * the Disposition interface is a ParamData, allowing to side-step + * any virtual dispatch; since the implementation of #retrieveInto and + * #setValFrom is defined in the same source file, the optimiser will + * inline the call, resulting in a direct value access. + */ template X Disposition::extract() const @@ -112,8 +138,9 @@ namespace par { X result; // RVO if (isBaseDomainMatch()) - { // short-circuit by direct force-cast - retrieveInto (asValBuff (result)); + { // short-circuit by direct force-cast, bypassing virtual dispatch + auto& paramData = * static_cast const *> (this); + paramData.SimpleData::retrieveInto (asValBuff (result)); } else { // run full type-conversion dispatch @@ -130,8 +157,9 @@ namespace par { Disposition::setVal (X changedVal) { if (isBaseDomainMatch()) - { // short-circuit by direct force-cast - setValFrom (asValBuff (changedVal)); + { // short-circuit by direct force-cast, bypassing virtual dispatch + auto& paramData = * static_cast*> (this); + paramData.SimpleData::setValFrom (asValBuff (changedVal)); } else { // run full type-conversion dispatch @@ -142,13 +170,5 @@ namespace par { } - template - bool - Disposition::isBaseDomainMatch() const - { - return false; /////////////////////////OOO running into a dead end here? how can we possibly access a type tag marker without indirection?? - } - - }} // namespace lib::par #endif /*LIB_PAR_DISPOSITION_H*/ diff --git a/tests/lib/par/parameter-test.cpp b/tests/lib/par/parameter-test.cpp index b7cb2b82b..34cd13248 100644 --- a/tests/lib/par/parameter-test.cpp +++ b/tests/lib/par/parameter-test.cpp @@ -19,7 +19,6 @@ #include "test/run.hpp" //#include "test/test-helper.hpp" #include "lib/par/parameter.hpp" -//#include "lib/format-cout.hpp" #include "test/diagnostic-output.hpp"/////////////TODO //#include @@ -39,17 +38,6 @@ namespace test{ namespace {//Test fixture.... - class Thing - { }; - - template - struct Some - { - X x; - }; - - typedef Some SomeThing; - }//(End)Test fixture diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index d8540dbb1..c13acc982 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -82228,8 +82228,8 @@ - - + + @@ -83941,9 +83941,9 @@ - - - + + + @@ -84456,12 +84456,12 @@ - + - + @@ -84484,7 +84484,7 @@ - + @@ -84551,7 +84551,7 @@ - + @@ -84604,7 +84604,18 @@ - + + + + +

+ ...und zwar unter der Vorgabe, daß es keine zu starken »Verrenkungen« erfordert und sich sicher im Rahmen des Standards bewegt (⟶ das ist letztlich gelungen; ich kann tatsächlich die zwei virtuellen Implementierungs-Medoden non-virtual aufrufen; habe sie nun auch als final  markiert, um auf die latente Gefahr hinzuweisen) +

+ +
+ + + @@ -84642,7 +84653,7 @@ - + @@ -84658,8 +84669,8 @@ - - + + @@ -84680,16 +84691,13 @@ - - - +

...ich hatte es zwar definiert und im Container-Typ angegeben — aber dann im konkreten API-Aufbau doch nicht eingeschleift (weil es da thematisch ja auch nicht hingehört, Grrrr)

- -
+
@@ -84698,16 +84706,13 @@ - - - +

insofern dieses ganze clevere seitwärige Attachment überflüssig ist;  die einzige zwingende Voraussetzung ist, daß wir eine VTable haben

- -
+
@@ -84718,16 +84723,13 @@ - - - +

weil PolymorphicValue da blindlings seinen ADMIN_OVERHEAD draufschlägt. Das ist ein sekundäres Problem im API-Design — wenn das Marker-API und der »workaround« mit dem Mix-in nicht mehr gebraucht werden, dann fällt auch diese ganze Verwirrung hier weg (die mich jetzt viele Stunden gekostet hat)

- -
+
@@ -84735,6 +84737,219 @@
+ + + + +
+
+ + + + + + + + +

+ ....und dieser Code hier ist bereits jetzt verdammt komplex; ich bin unzufrieden mit der Anordnung der Typen — und so habe ich Sorge, daß eine solche »Hintertür« später zum Wartungsproblem wird +

+ +
+
+ + + + +

+ Man müßte dann permanent bedenken, daß sonst noch »jemand« von außen hereinpfuscht — das ist nicht  wie man gute Software baut +

+ +
+
+ + + + +

+ Dieser Zugriff — unter korrekter Berücksichtigung der Automation — ist ausgesprochen der Kern meines Parameter-Konzepts, und daher dargestellt im Basis-Interface Provision::retrieveInto(ValBuff&) und Provision::setValFrom(ValBuff const&). +

+ +
+
+ + + +
+ + + + + + + + +

+ der Optimiser kann ggfs devirtualisieren — final  hilft dabei +

+ +
+ +
+ + + + + + + + + + + +

+ wie üblich, eine non-inline Funtkion definieren, also üblicherweise nimmt man dafür den dtor +

+

+ ⟹ sehe daß die VTable 3 «slots» bekommt (in Clang und Gcc), einen "base object destructor" und einen "deleting destructor", und schließlich meine virtuelle test-funktion +

+ +
+
+ + + + +

+ Nachdem ich es geschafft habe, mich wieder etwas in x64-Assembler einzulesen.... +

+
    +
  • + die Referenz kommt als Pointer in einem Register +
  • +
  • + diese wird in den Methoden-Stackframe gespeichert +
  • +
  • + dann einmal dereferenziert ⟹ zeigt jetzt in das Objekt, und zwar dorthin wo der VTable-Pointer steht +
  • +
  • + diesen +16 (also den dritteh «slot») und dereferenzieren ⟹ gibt einen direkten Funktionspointer +
  • +
  • + dieser wird dann aufgerufen, nachdem die Funktionsargumente und der Rückgabewert arrangiert wurden +
  • +
+ +
+
+ + + + + + + + + + + +

+ ich WUßTE doch das es da irgend was gab, das sogar durch den Standard gedeckt ist +

+ +
+
+
+
+ + + + + + +

+ und unter O3: diese wird ge-inlined! +

+ +
+ +
+
+ + + + + + + + Qualified name lookup can be used to access a class member that is hidden by a nested declaration or by a derived class. A call to a qualified member function is never virtual + + + + + + + + +

+ If the selected function is non-virtual, or if the id-expression in the class member access expression is a +

+

+ qualified-id, that function is called. Otherwise, its final overrider (11.7.2) in the dynamic type of the object +

+

+ expression is called; such a call is referred to as a virtual function call. [Note: The dynamic type is the type of +

+

+ the object referred to by the current value of the object expression. 11.10.4 describes the behavior of virtual +

+

+ function calls when the object expression refers to an object under construction or destruction. — end note +

+ +
+
+ + + + + + + + +
void hun(T* obj, class C* c)
+{
+    //do some checking on c
+    if (some conditions from c are true)
+    {
+        //call fun from T ignoring it's virtual
+        obj->T::fun(); // T::fun is a qualified-id    }
+}
+ +
+
+
+
+
+
+ + + + +

+ Fazit: keine Optimisation sondern man kann das direkt instruieren +

+ +
+ + + + + + + +
@@ -84742,16 +84957,13 @@ - - - +

ValStorage  : ein konkreter ValBuff mit ausreichender Storage-size

- -
+ @@ -84762,10 +84974,12 @@
- + + - + + @@ -84886,7 +85100,7 @@ - + @@ -84895,12 +85109,16 @@

+
+ + +
@@ -84976,6 +85194,20 @@
+ + + + +

+ hab mich an der Stelle festgebissen und ganz stur reingebohrt, bis ich eine komplette Implementierungs-Skizze hatte, die alle erwarteten Elemente demonstriert +

+ +
+ + + + +
@@ -84986,14 +85218,14 @@ - + - - - + + + @@ -85026,6 +85258,9 @@ + + + @@ -186688,14 +186923,127 @@ class Something + + + + + + + + +

+ sprachlich sauberer formuliert, aber auch etwas umständlicher, dafür stellenweise auch genauer. +

+ +
+ + +
+ + + - + + + + + + +

+ früher, für 8086 war das zwingend, da nur esp und ebp für relative Addressierung verwendet werden konnten. Seit 80386 können alle Register für relative Addressierung verwendet werden. Daher ist ebp (und rbp für 64bit) meist nur ein weiteres Register, das man nutzen kann. Allerdings generieren die meisten Compiler immer noch Code, der den »Funktions-Frame« in rbp halten (also das, wohin der Stack-Pointer zu Beginn der Funktion gezeigt hat). Code wird dadurch viel leichter lesbar, da alle lokalen Variablen einen festen Offset haben. Allerdings kann man rbp wegoptimieren und (fast) alles mit rsp allein machen — nur ändern sich dann auch die Offsets fortwährend. Es gibt allerdings eineige Speziallfälle, wo das schwierig bis unmöglich wird (hier werden immer wieder die »flexiblen Arrays« von C genannt) +

+ +
+
+ + +
+ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ QWORD PTR  ist insgesamt einfach historisch gewachsene Intel-Syntax; das "PTR" trägt selber keine Semantik, es macht die Instruktion nur besser lesbar. Daß es sich um eine Indirektion / Dereferenzierung handelt, geht aus den [...] hervor +

+ +
+ + + + +

+ mov BYTE PTR  [rbp-48], al    ; 1 byte +

+

+ mov WORD PTR  [rbp-48], ax    ; 2 bytes +

+

+ mov DWORD PTR [rbp-48], eax   ; 4 bytes +

+

+ mov QWORD PTR [rbp-48], rax   ; 8 bytes +

+

+ +

+ +
+
+
+ + + + +

+ (hier ein quad ≙ 64 bit) +

+

+ Erläuterung: die Dereferenzierung von [rbp - 48] liefert nur eine Speicheraddresse, sagt aber nicht, wie viele Bytes ab dieser Adresse als Operand genommen werden sollen. +

+

+ Beispiel: mov QWORD PTR [rbp-48], rax +

+

+ nimm den Inhalt von eax und speichere ihn an die indirekt gegebene Adresse in die dort folgenden 8byte +

+

+ Anmerkung: viele Assembler mit Intel-Syntax können diese Operanden-Längen auomatisch inferieren (z.B. NASM kann das); im gezeigten Beispiel geht ja aus dem quell-Operanden "rax" schon hervor, daß 64bit bewegt werden sollen. Andererseits, in AT&T-Syntax wird meistens ein Suffix an den Opcode angehängt, um die Operanden-Länge anzuzeigen. Das Beispiel würde dann also lauten (AT&T-Syntax): movq %rax, -48(%rbp) +

+ +
+
+