diff --git a/src/lib/par/domain.hpp b/src/lib/par/domain.hpp index 0069237ff..37603ed28 100644 --- a/src/lib/par/domain.hpp +++ b/src/lib/par/domain.hpp @@ -41,7 +41,7 @@ namespace lib { namespace par { - + struct ValBuff { /*placeholder*/ }; template @@ -60,9 +60,8 @@ namespace par { return * static_cast (rawMem); } - template + template class TypeHandler - : public BAS { public: virtual void extractAs (X&, ValBuff&) =0; @@ -76,10 +75,11 @@ namespace par { ,uint ,uint64_t ,double + ,float ,bool >; /** build a generic visitor interface for all types in list */ - using TypeHandlerInterface = meta::InstantiateChained; + using TypeHandlerInterface = meta::InstantiateForEach; /** @@ -99,16 +99,44 @@ namespace par { virtual ~Domain(); ///< this is an interface protected: - }; + +// GCC > 13 warns at class definition when a new overload shadows an inherited virtual function. +// While theoretically correct, in practice any call will be dispatched through the base interface, +// which in turn is also generated by metaprogramming from a type list. +// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109740 +_Pragma("GCC diagnostic push") \ +_Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"") + + + + template + struct TypeDomainSetup + { + + template + class TypeHandlerImpl + : public BAS + { + void + extractAs (X& srcVal, ValBuff& valBuff) override + { + UNIMPLEMENTED ("extract into Buffer"); + } + }; + + using TypeHandlerChain = meta::InstantiateChained; + }; + + + template class BaseDomain - : public Domain + : public TypeDomainSetup::TypeHandlerChain { - void extractAs (uint&, ValBuff& valBuff) override { UNIMPLEMENTED("ex uint"); } - void extractAs (double&, ValBuff& valBuff) override { UNIMPLEMENTED("ex double"); } + public: }; diff --git a/tests/lib/par/parameter-domain-test.cpp b/tests/lib/par/parameter-domain-test.cpp index b568e345f..926fc1e48 100644 --- a/tests/lib/par/parameter-domain-test.cpp +++ b/tests/lib/par/parameter-domain-test.cpp @@ -92,9 +92,8 @@ namespace test{ uint target1{0}; CHECK (not target1); -// TypeHandler& dd1{d1}; -// dd1.extractAs (target1, src1); - d1.extractAs (target1, src1); + TypeHandler& dd1{d1}; + dd1.extractAs (target1, src1); CHECK (target1 == uint(val1)); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 7a36a0253..2c5fd8e07 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -81709,6 +81709,23 @@ + + + + +

+ Daß die meisten »Doppelkapseln«, die ich bisher gebaut haben, von einem statischen Anker ausgehen, sagt noch lange nicht, daß die Doppelkapsel-Struktur grundsätzlich ungeeignet wäre! +

+

+ +

+

+ So könnte man die VTable mit einer Typ-ID identifizieren. Im weiteren Verlauf dieser Design-Analyse hat sich das als ein möglicher Pfad herausgebildet (im Besonderen weil ich Platzbeschränkung als leitenden Constraint identifiziert habe) +

+ +
+ +
@@ -81806,7 +81823,8 @@ - + + @@ -81815,7 +81833,7 @@

- + @@ -81879,7 +81897,7 @@ - + @@ -81981,6 +81999,32 @@ + + + + + + + + +

+ Ein automatisierter Parameter wäre mithin ein Hybrid, wobei ein Anker-Wert  stets lokal vorliegt, man dann aber aus der Automationskurve einen Offset oder Faktor bezieht... +

+ +
+ +
+ + + + +

+ das ist dann definitiv nicht mehr bloß ein Typ +

+ +
+
+
@@ -82129,7 +82173,7 @@ - + @@ -82149,7 +82193,7 @@ - + @@ -82176,16 +82220,13 @@ - - - +

bestimmt die Art und Weise des Zugriffs auf eine Datenquelle: entweder als Wert der in enem Buffer abgelegt ist, oder als pull von einem Funktor

- -
+
@@ -82227,12 +82268,12 @@

- +

- Denn die Design-Analyse hat gezeigt, daß der »Opaque-Buffer«-Ansatz inhaltlich sehr nahe an einer »Inline-Funktion« liegt, und letztere ist aber strukturell am effizientesten zu implementieren als PolymorphicValue — was vor allem an der Notwendigkeit eines »Managers« liegt, um das Instanz- und Copy-Verhalten zu bestimmen. Die Begründung ist, daß ein virtuelles Interface tatsächlich bereits die effizienteste Reallisierung darstellt, sobald es mehrere virtualisierte »methoden« gibt, denn sie faßt das Instanz-Management und den Zugang zu einem gemeinsamen Datenkontext bereits optimal zusammen. Selbst wenn man bewußt nicht die Implementierung aus der C++-Runtime nimmt, (wie im Fall der std::function) so läuft die Struktur doch immer wieder darauf hinaus, Zugang zu einem gemeinsamen Datenpaket zu bekommen (egal ob das nun per PImpl im Heap liegt, oder direkt in einem Inline-Buffer). Denn ohne solchen gemeinsamen Zustand kann es auch kein tangibles Verhalten geben.... + Denn die Design-Analyse hat gezeigt, daß der »Opaque-Buffer«-Ansatz inhaltlich sehr nahe an einer »Inline-Funktion« liegt, und letztere ist aber strukturell am effizientesten zu implementieren als PolymorphicValue — was im Besonderen an der Notwendigkeit eines »Managers« liegt, um das Instanz- und Copy-Verhalten zu bestimmen. Die Begründung ist, daß ein virtuelles Interface tatsächlich bereits die effizienteste Reallisierung darstellt, sobald es mehrere virtualisierte »Methoden« gibt, denn sie faßt das Instanz-Management und den Zugang zu einem gemeinsamen Datenkontext bereits optimal zusammen. Selbst wenn man bewußt nicht die Implementierung aus der C++-Runtime nimmt, (wie im Fall der std::function) so läuft die Struktur doch immer wieder darauf hinaus, Zugang zu einem gemeinsamen Datenpaket zu bekommen (egal ob das nun per PImpl im Heap liegt, oder direkt in einem Inline-Buffer). Denn ohne solchen gemeinsamen Zustand kann es auch kein tangibles Verhalten geben....

@@ -82295,6 +82336,12 @@ + + + + + + @@ -82442,7 +82489,7 @@ - + @@ -82450,7 +82497,7 @@ nochmal zur Erinnerung: bin in dieses ganze Riesen-Thema nur reingeschlittert, weil ich den processKey als Job-Parameter aufgeben mußte (und der war eine so »schöne Lösung«, die das Thema markiert und auf später verschiebt). Dann habe ich gesagt, einen Zacken nachlegen und mal eben ein Design für Parameter machen. Ich weiß aber noch gar nicht, wo genau die Parameter sonst noch eingesetzt werden sollen. Ich weiß nur, daß ich sie universell einsetzen möchte.

- Im Besonderen ist die ganze Auswertung von Parameter komplett ungeklärt. Diese Auswertung passiert irgendwie in Response auf Interaktionen im GUI (vor allem: ändern der Playback-Position, aber natürlich auch direkte Wert-Tweaks). Es ist klar, daß die Auswertung nicht im GUI selber stattfinden wird, denn dafür haben wir die Trennung (die Commands, den UI-Bus und das Event-Sourcing). Wahrscheinlich wird eine Auswertung in einer Projektion der Events passieren, entweder nach einem Builder-Lauf, oder in vielen Fällen anstelle eines Solchen (wenn gar nichts neu gebaut werden muß). Auch der Builder ist ja konzeptionell eine Projektion. Dann aber ist andererseits auch klar, daß Parameter irgendwie in der Render-Engine ausgewerter werden, und zwar dort verpackt in einen Automations-Funktor. Möglicherweise wird diese Auswertung aber auch eine Art compilierte Variante sein. + Im Besonderen ist die ganze Auswertung von Parametern komplett ungeklärt. Diese Auswertung passiert irgendwie in Response auf Interaktionen im GUI (vor allem: ändern der Playback-Position, aber natürlich auch direkte Wert-Tweaks). Es ist klar, daß die Auswertung nicht im GUI selber stattfinden wird, denn dafür haben wir die Trennung (die Commands, den UI-Bus und das Event-Sourcing). Wahrscheinlich wird eine Auswertung in einer Projektion der Events passieren, entweder nach einem Builder-Lauf, oder in vielen Fällen anstelle eines Solchen (wenn gar nichts neu gebaut werden muß). Auch der Builder ist ja konzeptionell eine Projektion. Dann aber ist andererseits auch klar, daß Parameter irgendwie in der Render-Engine ausgewerter werden, und zwar dort verpackt in einen Automations-Funktor. Möglicherweise wird diese Auswertung aber auch eine Art compilierte Variante sein.

@@ -82568,9 +82615,7 @@ - - - +

Konsequenz: pull-Prinzip ist verpflichtend @@ -82612,12 +82657,168 @@ + + + + + + + + + + +

+ »klein« heißt nicht wirklich klein +

+ +
+ + + + + + +

+ Es wäre denkbar, eine unter-Klasse der Parameter zu bilden, die dann reine Werte sind. Die könnte man auf 2 «Slots» limitieren. Alles, was darüber hinausgeht ist zwangsläufig ein flexibel belegter, lokaler Container. Und da wir definitiv so etwas wie »generische Parameter« als einen Datentyp haben wollen, muß der Inline-Buffer groß genug sein, um darin zumindest einen Delegations-Mechanismus zu realisieren, oder eben doch ein paar Flags mit abzulegen. Außerdem gewinnt man bei diesen Trade-Offs typischerweise nicht viel, wenn man von 2-er-Potenzen abweicht, denn die Alignments auf 2, 4, oder 8 Slots sind allgegenwärtig; eine Cache-Line hat typischerweise 64 Slots (64 x 64bit). Mein Bauchgefühlt sagt mir, 3 Slots bringen überhaupt nichts (gegenüber 4 Slots), aber über 5 statt 8 könnte man reden. +

+ +
+
+ + + + +

+ Vorurteil: 4 «Slots» ≙ 32 Bytes +

+ +
+ +
+
+ + + + +

+ Trade-Off-Ansatz: »statische Basisklassen« +

+ +
+ + + + + +

+ natürlich weiterhin ein klassisches, virtuelles Interface +

+ +
+
+ + + + +

+ aber die Basisklassen-Instanz hat keine zusätzlichen Laufzeit-Daten +

+ +
+
+ + + + +

+ vielmehr wurde sie per Template-Metaprogramming generiert +

+ +
+
+ + + + +

+ Wofür man zunächst in der klassichen OO-Programmierung ein privates Datenfeld einsetzen würde, wird hier als ein statisches privates Datenfeld repräsentiert, das direkt (oder indirekt) über eine Interface-Methode zugänglich ist. Die Auslegung und Parametrisierung der Basisklasse erfolgt also zur Compile-time. +

+ +
+
+ + + + +

+ Jede, wirklich jede Ausprägungs-Variante schlägt sich nieder in Form einiter (deduplizierter) Datenfelder in einem Daten-Segment (private static), sowie einem kompletten Satz an Methoden-Implementierungen im Code-Segment, und einer VTable mit Zeigern auf diese. Muß man eine weitere Methode ins Interface aufnehmen, entsteht sofort ein starker Hebel... +

+

+ +

+

+ So ein Ansatz wäre vertretbar, wenn letztlich die Anzahl der Ausprägungen doch überschaubar bleibt, davon aber sehr viele Einzelinstanzen existieren. Im Besonderen müßte man darauf achten, daß Finite-Domain bzw. Enummerationen dann doch noch einmal kaskadierend herausgezogen werden — oder eben doch wenigstens per ID in den Payload-Daten referenziert sind. +

+ +
+ +
+
+
+ + + + +

+ Aufgabe ist nun: Bausteine skizzieren +

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

+ Vorentwurf(Zusammenfassung): +

+ +
+ + + + + + + + +

+ Diese kombiniert verschiedene Registrierungs-Indices, möglicherweise für die Base-Domain, einen (Domain)Prototype und/oder eine Scale. Denkbar wäre auch, hier noch ein »Schema« einzuführen, um ggfs direkt, an der VTable vorbei, gewisse Zugriffs-Schemata direkt über die Front-end-Funktionen zu realisieren (wobei im Moment nicht klar ist ob sowas überhaupt sinnvoll ist, und dann nicht doch gleich ein anderer statischer Basistyp angemessener wäre) +

+ +
+
+ + + + +

+ die Größe ist insgesamt auf 32 Bytes fest limitiert +

+ +
+
+
@@ -82661,39 +82862,129 @@ - + - - + + + - - - +

...interessanterweise auch nicht (mehr) beim Variant-Visitor: auch dort habe ich in die Zugriffsfunktion schon vor einiger Zeit einen Upcast auf das Type-Handler-Interface eingebaut. Der Compiler erlaubt anscheinend nicht mehr, verschiedene Overloads einer gleich benannten Funktion aus mehreren Basisklassen zusammenzuführen

+ + + + + + + + +

+ Kann jetzt nur spekulieren, warum ich da seinerzeit eine Fehlermeldung hatte. Könnte an dem vorläufigen Stand der Test-Implementierung damals liegen, denn ich wollte compilieren, ohne bereits die Implementierungs-Chain zu definieren (was ich erst jetzt kann, nachdem ich auf den Trick mit dem Typ-Parameter im umschließenden Scope gekommen bin) — hatte also explizite Implementierungen geschrieben. +

+

+ Experimente mit Varianten +

+

+ Es zeigt sich, daß dieses Konstrukt den Compiler in eine komplexe Situation bringt, in der wirklich jedes Detail stimmig sein muß. Im Besonderen kann die Diagnostik komplett irreführend sein; wenn etwas mit der Sichtbarkeit nicht stimmt, kommt es zu einer (verdeckten) Ambiguität, die u.U. (abhängig vom konkreten Typ) automatisch vom Compiler „repariert“ wird (ich meine diese problematischen Regeln, daß z.B. ein unsigned an vielen Stellen geräuschlos auf einen Signed zugewiesen werden kann). Außerdem ist es wichtig, das Pragma zum Unterdrücken von -Woverloaded-virtual bereits vor  dem Template stehen muß, in dem das Shaddowing auftritt (nicht etwa, wie man intuitiv erwarten würde, vor der Verwendung). +

+

+ In diesen Experimenten erscheint dann doch das ursprüngliche Baumuster von Alexandrescu die stabilste Lösung zu sein. Also ein multiple-mix-in für das Interface, aber eine Kette für die Implementierung. Das liegt daran, daß dann eine eventuelle Ambiguität beim Aufruf wirklich direkt im Interface passiert, nicht irgendwo in einer Kette; hinzu kommt, daß wir in einer Kette die Sichtbarkeit explizit wieder herstellen müssen durch ein using BAS::functionname; — und das kann weitere Ambiguitäten erzeugen, und erfordert vor allem irgendwo eine catch-all-Definition (z.B. mit leerer Argumentliste). Das ist verwirrend und kann weitere Ambiguitäten erzeugen. +

+

+ Wenn der Compiler irgendwo automatisch (falsch) konvertiert, kann es dann zu völlig verwirrenden anderen Problemen kommen, wie z.B. der Zugriff über eine Referenz auf ein Objekt anderen Typs (was nur mit einer const-Referenz erlaubt ist). +

+

+ +

+

+ Insofern verstehe ich durchaus die Motivation, warum -Woverloaded-virtual  eingeführt wurde; hier gibt es viele Fallen, die einen unerfahren Programmierer vor unlösbare Probleme stellen können +

+ +
+ +
- - - +

...auch das Problem kenne ich schon: -Woverloaded-virtual : da hatten die Compiler-Entwickler durchaus relevante Gefahren im Blick, aber haben die Warnung zu billig implementiert, nämlich bereits bei der Definition eines überladenen Virtual. Die Gefahr würde jedoch erst beim zweideutigen Aufruf bestehen. Daher kann man diese Warnung per Pragma übersteuern. Hab ich bereits bei verschiedenen Verwendungen des Generator-Templates getan.

+ + + + +
+
+

+ // GCC > 13 warns at class definition when a new overload shadows an inherited virtual function. +

+

+ // While theoretically correct, in practice any call will be dispatched through the base interface, +

+

+ // which in turn is also generated by metaprogramming from a type list. +

+

+ // See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109740 +

+

+ _Pragma("GCC diagnostic push") \ +

+

+ _Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"") +

+
+
+

+
+ Wichtig: muß bereits vor dem Template stehen, in dem das Shaddowing auftritt (kann aber durchaus mitten in einem Namespace sein) +

+ +
+
+ + + + +

+ ...denn an der Stelle spielt Sichtbarkeit gar keine Rolle mehr, es wird nur eine schon vorhandene Interface-Methode in der VTable auf eine konkrete Implementierung gebunden. Allerdings nur, wenn man tatsächlich das Interface als multiple-mix-in generiert — denn das umgeht das Sichtbarkeits-Problem +

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

+ Das größte Problem sind die automatischen Umwandlungen von Basis-Typen in C++ +

+ +
+ +
- - - +

das ist kniffelig, da das ein template-template-Parameter ist, mit zwei bereits festgelegten Typ-Parametern (Zieltyp und Basisklasse). Hier implementieren wir aber Domain<X>, also muß der Quell-Parameter X auch irgendiw zusätzlich in den Type-Handler gebracht werden. Idee: als nested-Templates definieren und dann am Ende per templated Typedef rausziehen @@ -152794,10 +153085,150 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + + + +

+ ...die hatte ich intuitiv so angelegt, weil ich schon vermutet habe, daß »man« ein asynchrones IO-Interface so definiert.... +

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

+ weil dann wieder ein zentraler Verteiler da wäre, der andere Threads antriggern müßte +

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

+ Will sagen: wenn man sich einen Completion-Entry nimmt, dann weiß man erst noch nicht, ob daraus nur das dekrementieren einer Gate-Activity folgt, oder ob das direkt in einen aufwendigen Computation-Job mündet. Ein reines Triggern geht sehr schnell, und steht in keinem Verhältnis zum Overhead, den ein Worker durchläuft, bis er den nächsten Job hat +

+ +
+
+ + + + +

+ Solange ein Job aus der Completion-Queue entnimmt, muß er das Grooming-Token haben, weil die Ringbuffer von io_uring nur single-Producer/Consumer-fähig sind. Man müßte also irgendwie feststellen können, ob ein Entry tatsächlich in die Verarbeitung eines Calculation-Jobs mündet. Denn dann könnte man bis dorthin arbeiten, dann das Grooming-Token droppen, und in diesen Job einsteigen. Das wäre dann konsistent mit unserer Architektur: der nächste Job hätte dann die nächsten anstehenden Completion-Entries zu bearbeiten. +

+ +
+
+ + + + +

+ Konsequenz: Prüfung und Priorisierung der Completions notwendig +

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

+ Größtenteils um das Design für die Demo-Applikation festzulegen, die ich derzeig für den FrOSCon-Talk (IO_uring für Video-Widergabe) entwickle +

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

+ aber man sollte konzeptionell zwischen der visibility mit dem Kernel und der Visibility zwischen den Workern unterscheiden; für ersteres gibt es die Barrieren in Liburing +

+ +
+
+ + + + + + + +

+ der Umstand daß neue IO-Aktivitäten gescheduled werden und Completions entnommen werden müssen, garantiert nur eine passive Backpressure (nicht entnommene Completions gehen nicht verloren, und der Platz in den URings ist begrenzt). Sehr wahrscheinlich ist mehr notwendig, damit die Engine rund läuft. +

+ +
+
+ + + + + +

+ Es ist eine etablierte und bewährte Methode, und wird nicht weggehen. Was wir geschaffen haben konvergiert gut mit den allgemeinen Einsichten im Feld +

+ +
+
+ + + + +

+ das ist ein Gegentrend, der aber auf einer anderen Größenordnung arbeitet (im Nanosekunden-Bereich) +

+ +
+
+ + + +
+
+