From abeca9823363e217e53a854d9a0f83378cb16dea Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 10 Jan 2025 15:28:22 +0100 Subject: [PATCH] Invocation: Analysis regarding dispatch of information functions for Nodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The choice to rely on strictly typed functor bindings for the Node operation bears the danger to produce ''template bloat'' — it would be dangerous to add further functions to the Port-API naïvely; espeically simple information functions will likely not depend on the full type information. A remedy to explore would be to exploit properties marked into the Port's `ProcID` as key for a dispatcher hashtable; assuming that the `NodeBuilder` will be responsible for registering the corresponding implementation functions, such a solution could even be somewhat type-safe, as long as the semantics of the ProcID are maintained correctly. --- src/lib/fun-hash-dispatch.hpp | 84 ++++ src/steam/engine/proc-id.hpp | 62 ++- tests/15library.tests | 5 + tests/library/fun-hash-dispatch-test.cpp | 101 ++++ wiki/thinkPad.ichthyo.mm | 566 ++++++++++++++++++++++- 5 files changed, 803 insertions(+), 15 deletions(-) create mode 100644 src/lib/fun-hash-dispatch.hpp create mode 100644 tests/library/fun-hash-dispatch-test.cpp diff --git a/src/lib/fun-hash-dispatch.hpp b/src/lib/fun-hash-dispatch.hpp new file mode 100644 index 000000000..428b096f0 --- /dev/null +++ b/src/lib/fun-hash-dispatch.hpp @@ -0,0 +1,84 @@ +/* + FUN-HASH-DISPATCH.hpp - enrol and dispatch prepared functors keyed by hash-ID + + Copyright (C) + 2025, Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +*/ + + +/** @file fun-hash-dispatch.hpp + ** Service to register and dispatch opaque functions. + ** @see FunHashDispatch_test + */ + + +#ifndef LIB_SEVERAL_H +#define LIB_SEVERAL_H + + +#include "lib/error.hpp" +#include "lib/nocopy.hpp" +#include "lib/hash-value.h" +#include "lib/meta/function.hpp" +#include "lib/util.hpp" + +//#include +#include +//#include +//#include + + +namespace lib { + + namespace {// implementation details + + /** @internal mix-in for self-destruction capabilities + */ + }//(End)implementation details + + + + /************************************************//** + */ + template + class FunHashDispatch + : util::MoveOnly + { + using DispatchTab = std::unordered_map; + DispatchTab dispatchTab_{}; + + public: + SIG* + enrol (HashVal key, SIG* fun) + { + auto [entry,_] = dispatchTab_.emplace (key,fun); + return entry->second; + } + + bool + contains (HashVal key) const + { + return util::contains (dispatchTab_, key); + } + + SIG* + select (HashVal key) + { + auto it = dispatchTab_.find (key); + if (it == dispatchTab_.end()) + throw lumiera::error::Logic{"Expect function for given hash to be previously enrolled."}; + else + return it->second; + } + }; + + + +} // namespace lib +#endif diff --git a/src/steam/engine/proc-id.hpp b/src/steam/engine/proc-id.hpp index 1980947be..e5a2af852 100644 --- a/src/steam/engine/proc-id.hpp +++ b/src/steam/engine/proc-id.hpp @@ -16,6 +16,34 @@ ** Functionality is provided to identify a point in the processing chain for sake of ** error reporting and unit testing; moreover, identifying information can be chained ** and combined into a systematic hash key, to serve as foundation for a stable cache key. + ** @warning The specification expresses semantic distinctions and it is the responsibility + ** of the media-processing-library binding plug-in to ensure that classification + ** matches relevant semantic distinctions. In this context, "different" means + ** that two functions produce _perceptibly different results_ — which also + ** implies that for equivalent IDs we can use cached calculation results. + ** + ** ## Structure and syntax + ** A complete processing-specification combines a high-level identification of the enclosing + ** Node with qualifiers to describe a specific functionality variant for a given Port, together + ** with the structure of the input and output argument lists, and a set of additional, extended + ** attributes. Any aspects should be recorded here, if they might cause differences in computed + ** results or are relevant for some further information function (used for diagnostic, engine + ** instrumentation and job planning) + ** - the _Node symbol_ is related to the [processing asset](\ref ProcAsset) and is expected + ** to be structured as `:`, e.g. `FFmpeg:gaussianBlur` + ** - the _Port qualifier_ accounts for specific configuration applied for the given port + ** - the _Argument lists_ should follow the pattern `[(inType, ...)](outType,...)`, with the + ** allowed shorthand notation `/N` to designate N identical arguments of type `type`. + ** For a complete _processing spec,_ the node symbol is possible decorated with a dot and + ** the port qualifier (if present), then followed by the argument lists. + ** + ** ## Hash computation + ** Hash-IDs are derived from the full processing spec,_ but also from individual parts alone + ** for some use cases (e.g. dispatch of information functions). Furthermore, the Hash-IDs of + ** all Nodes in a processing chain can be combined into a Hash-ID of the chain, which is usable + ** as cache key: when the hash-ID matches, a cached result can be used instead of re-computation. + ** @todo Can we assume size_t == 64bit; is this enough to prevent collisions? + ** Can we afford using a 128bit hash? What about portability of hash values? ** @todo WIP-WIP as of 10/2024 this is a draft created as an aside while working towards ** the first integration of render engine functionality //////////////////////////////////////////////TICKET #1377 : establish a systematic processing identification ** @remark the essential requirement for a systematic and stable cache key is @@ -49,11 +77,42 @@ namespace engine { class ProcNode; + + /** + * Extended Attributes for ProcID metadata. + * @remark used for cache key calculation and + * to dispatch information functions. + * @todo if this grows beyond size_t, it should be + * deduplicated and stored in a registry, + * similar to the string spec. Storage matters! + */ + struct ProcAttrib + { + bool manifold :1; + bool isProxy :1; + + ProcAttrib() + : manifold{true} + , isProxy{false} + { } + + friend bool + operator== (ProcAttrib const& l, ProcAttrib const& r) + { + return l.manifold == r.manifold + and l.isProxy == r.isProxy; + } + }; + + /** + * Metadata to qualify a Port (and implicitly the enclosing Node). + */ class ProcID { StrView nodeSymb_; StrView portQual_; StrView argLists_; + ProcAttrib attrib_{}; ProcID (StrView nodeSymb, StrView portQual, StrView argLists); @@ -77,7 +136,8 @@ namespace engine { { return l.nodeSymb_ == r.nodeSymb_ and l.portQual_ == r.portQual_ - and l.argLists_ == r.argLists_; + and l.argLists_ == r.argLists_ + and l.attrib_ == r.attrib_; } friend bool diff --git a/tests/15library.tests b/tests/15library.tests index 9730acd5f..648b9adbb 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -363,6 +363,11 @@ return: 0 END +PLANNED "Generic function dispatch" FunHashDispatch_test < + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file fun-hash-dispatch-test.cpp + ** unit test \ref FunHashDispatch_test + */ + + + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/fun-hash-dispatch.hpp" +#include "lib/format-obj.hpp" +#include "lib/test/diagnostic-output.hpp"////////////////////TODO +//#include "lib/util.hpp" + +//#include +#include + + + +namespace lib { +namespace test{ + +// using util::isSameAdr; +// using std::rand; + using util::toString; + using std::string; + +// namespace error = lumiera::error; +// using error::LUMIERA_ERROR_FATAL; +// using error::LUMIERA_ERROR_STATE; + + + namespace { +// #define Type(_EXPR_) lib::test::showType() + } + + + + + + /***********************************************************************************//** + * @test Verify generic helper to provide a hash-based function dispatch table. + * @see fun-hash-dispatch.hpp + * @see NodeMeta_test + */ + class FunHashDispatch_test + : public Test + { + + void + run (Arg) + { + FunHashDispatch dispatch; + + auto one = [](int i) { return toString(i); }; + auto two = [](int i) { return string(size_t(i),'*'); }; + + auto* res = dispatch.enrol (1, one); + CHECK (res == one); + CHECK (dispatch.contains(1)); +SHOW_EXPR(dispatch.select(1)(42)); + CHECK (dispatch.select(1)(42) == "42"_expect); + + dispatch.enrol (2, two); + CHECK (dispatch.contains(1)); + CHECK (dispatch.contains(2)); +SHOW_EXPR(dispatch.select(1)(5)); + CHECK (dispatch.select(1)(5) == "5"_expect); +SHOW_EXPR(dispatch.select(2)(5)); + CHECK (dispatch.select(2)(5) == "*****"_expect); + + res = dispatch.enrol (1,two); + CHECK (res == one); + res = dispatch.enrol (3,two); + CHECK (res == two); + CHECK (dispatch.select(3)(2) == "**"_expect); + + VERIFY_FAIL ("Expect function for given hash" + , dispatch.select(5) ); + } + }; + + + /** Register this test class... */ + LAUNCHER (FunHashDispatch_test, "unit common"); + + +}} // namespace lib::test diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index e81753204..a9ba2daa6 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -98927,8 +98927,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -99530,6 +99530,111 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ ...da werde ich wohl noch darüber nachdenken müssen ... aber im Moment erscheint mir diese Frage unmittelbar auswegslos eindeutig: der Port ist die einzige Stelle, an der eine minimalistische, rigide Graph-Struktur virtualisiert verbunden ist mit einem unendlichen, amorphen offenen Raum der Möglichkeiten. Andererseits werde ich niemals mehr als eine opaque ID über dieses virtuelle Portal geben dürfen, denn sonst dringen Strukuren der semantischen Attributierung in das Graph-Modell ein, anstatt dieses nur zu markieren. +

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

+ das ist und bleibt eine Frage der Ontology +

+ +
+ + + + +

+ insofern ist es nur ein Tag +

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

+ jedes konkrete Projekt hat seine eigene kohärente Domain Ontology +

+ + +
+ + + +

+ Das folgt unter den Annahme, daß ein konkretes Projekt funktionieren muß. Also macht man nur Dinge, die kohärent aufgehen. Für Zweideutigkeiten findet man eine praktische Konvention, oder man weiß sie unsichtbar zu machen. Daher sollte dieses Problem für jedes konkrete Projekt in dem Sinn »lösbar« sein, daß die Cache-Steuerung fehlerfrei funktioniert. Der Beitrag der Lumiera-Infrastruktur dazu ist, für die notwendige Über-Differenzierung zu sorgen, so daß es niemals zu einer Kollision kommen kann zwischen Beiträgen, welche in disjunkten Libraries und Domain-Ontologies verankert sind. +

+ + +
+
+
+
@@ -99551,9 +99656,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

...und zwar bewußt; @@ -99562,8 +99665,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) zunächst wollte ich ja einen Routing-Descriptor, um damit nochemal eine zweite Indirektion / Flexibilisierung gegenüber dem InvocationAdapter zu machen. Dann hat sich aber die Bedeutung des Invocation-Adapters so verschoben, daß ein Teil seiner Rolle mit der FeedManifold verschmolzen ist, dafür aber ein rigideres Belegungsschema dem Binding-Functor abverlangt wird. Damit wurde explizite oder schematische Routing-Info eigentlich irrelevant, und ich habe sie durch direkte Referenzen ersetzt. Und das ist nun das Problem.

- -
+
@@ -99571,8 +99673,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + - + + @@ -99587,6 +99691,268 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + +

+ Problem dabei: wer kennt den kokreten Typ, +

+

+ um mit einer solchen Spezialisierung zu binden? +

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

+ Lösung: per Registrierung +

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

+ ...wegen Template bloat. Umso schlimmer, wenn diese Funktionen dann fast nur von Unit-Tests genutzt werden. Aber selbst eine einzige zusätzliche Funktion für einen Hash oder konkreten Descriptor würde ich gerne vermeiden wollen, denn man generiert zwangsläufig eine große Menge redundanten codes, der vermutlich niemals aufgerufen wird (was man aber nie wissen kann...) +

+ + +
+
+ + + + +

+ Man würde also ein System von Hash-IDs aufbauen, und dann würde jede Instanz des Turnout-Template bei der Generierung noch entsprechende Handler für die Diagnostik-Funktionen regisrieren. Allerdings nur, wenn die Funktion nicht bereits existiert. Dieser Ansatz würde darauf spekulieren, daß die meisten Diagnostik-Funktionen gar nicht anhand der konkreten Buffer- und Parameter-Typen differenziert sind. Beispielswese Funktionen, die auf Vorgänger-Ports zugreifen, müssen nur die Beschränkung auf die Anzahl der Vorgänger beachten. Aus der Processing-Spec, welche ja exakt den ausreichenden Differenzierungsgrad haben muß (sonst funktioniert das Caching nicht richtig) ließe sich ein Sub-Key für bestimmte Klassen von Diagnostik-Zugriffen gewinnen. Dieser würde dann ein Diagnostic-Objekt aus einer Hashtable holen (was dadurch dedupliziert wäre). +

+ + +
+
+ + + + + + +

+ ...das wäre durchaus denkbar für »operational control«, wenn es um spezielle Instrumentierung geht, also detailierte Zeitmessungen aufgezeichnet werden müssen, die sich nicht über einen einzigen Kamm scheren lassen +

+ + +
+
+ + + + + +

+ ...aber immerhin habe ich mich damals so nobel geschlagen, daß ich daraus noch eine generische Library-Funktion gemacht habe, was mir jetzt zugute kommen könnte +

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

+ Zugriff braucht nur die Differenzierung nach Weaving-Pattern, aber keine konkreten Typ-Signaturen. Denn das WeavingPattern legt fest, ob und wo die Sequenz der Vorgänger-Ports zugänglich ist. Das Layout von lib::Several wurde unabhängig von der konkreten Typisierung gehalten — aus ähnlichen Motiven. Selbst der Storage-Header beginnt mit einem einheitlichen Layout, und der size-Parameter liegt ganz vorne. Es erscheint sinnvoll, für diesen Zugriff eine ungetypte neue Library-Funktion bereitzustellen. +

+ +
+
+ + + + +

+ Und das muß auch so sein, denn sonnst wäre das kein freier Extension-Point. Jede Abweichung davon bedeutet effektiv eine Einschränkung der Implementierung — oder anders ausgedrückt, eine Erweiterung des Port-Interfaces. Das wäre durchaus denkbar; hier geht es allerdings um den sekundären Belang, wie man eine solche Erweiterung mit möglichst geringen Kosten realisiert +

+ + +
+ + + + + + +

+ Die offensichtliche Lösung ohne Überraschungen: wenn das API tatsächlich breiter ist, sollte man es breiter machen — und zwar auf dem üblichen und etablierten Weg +

+ +
+ +
+ + + + +

+ Vorschlag wäre eine Klasse PortConnectivity, und eine Alternative als ProxyPort. Davon könnten dann die beiden (derzeit definierten) generischen Weaving-Patterns erben. Dadurch wäre dann eine uniforme Implementierung für gewisse Verhaltensmuster möglich, ohne für jede Detail-Typisierung erneut Code generieren zu müssen +

+ + +
+ +
+ + + + +

+ Das wäre dann ein zusätzliches Differenzierungskriterium oder ein Qualifier; mutmaßlich zu realisieren als extended attributes, d.h. in der normalen Port-Spec unsichtbar oder nur durch einen kompakten Dekorator ausgewiesen. Ein solches zusätzliches Feature wird sehr wahrscheinlich notwendig sein, um eine hinreichend präzise differenzierte Hash-ID zu erzeugen, die auch konkrete Parameter-Werte mit einschließt, sofern diese einen Einfluß auf das Ergebnis und damit auf die Cache-Keys haben könnten. +

+ +
+ +
+
+ + + + + +

+ Jede Subklasse mit nicht-trivialem Verhalten (also effektiv jede) muß diese virtuelle Funktion implementieren; mit jeder Template-Spezialisierung wird erneut Code »abgeworfen«, um einen Funktionspointer darauf dann in die VTable aufnehmen zu können. Theoretisch könnte ein cleverer Compiler hier eine de-Duplikation machen, es ist mir aber kein Standard bekannt, der so etwas auch nur nahelegt (wobei — es ist naheliegend, da Template-Bloat ein bekanntes Problem von generischem modernen C++ ist, und trotzdem für einen durchschnittlichen Entwickler schwer zu vermitteln) +

+ +
+ +
+ + + + +

+ Egal ob der Compiler de-Duplizieren könnte — wir machen es einfach explizit: Die jeweilige Basisklasse dürfte dann aber keine Abhängikgeit von Template-Parametern haben ⟹ für die beiden bisher definierten Fälle wäre komplette de-Duplikation so realisierbar. Der Preis dafür wäre aber leider eine deutliche Verschlechterung der Lesbarkeit im Weaving-Pattern selber; nicht nur daß man nun eine Basisklasse „im Kopf haben“ müßte, zudem wären dann alle Zugriffe auf diese Basis-Parameter mit dem self-Typ im Code zu qualifizieren. Deswegen habe ich eine starke Abneigung gegenüber dieser Lösung. Es ist der typische Pragmatismus, der den Code für alle nachfolgende Wartung und Erweiterung versaut.... +

+ + +
+ +
+ + + + +

+ Die Kosten werden hierdurch in die Laufzeit verschoben, genauer gesagt, aus dem Code-Segment in die Allokation für Nodes. Es ist weiterhin Compile-Time-Logik erforderlich, um für den Standard-Fall ein entsprechendes Tag in den extended attributes zu hinterlassen. +

+

+ Zunächst einmal erzeugt diese Lösung einen erheblichen run-time-Storage-Overhead. Die dynamische Codierung ersetzt einen einzelnen Pointer in der VTable jeweils durch eine String-Spec mit vmtl. mehr als 8 char Länge, sowie dazu noch einen Eintrag in einer sekundären Dispatcher-Table, bestehend aus einer Hash-ID (size_t) plus ... genau einem Function-Pointer. So gesehen, erst mal ziemlich dämlich. ABER es gibt nun einen zusätzlichen Hebel durch de-Duplikation: zum Einen deduplizieren wir die Spec-Strings selber; was aber noch wichtiger ist, da die Detail-Operationen nur auf Teile der Spec-Strings differenzieren, wird es deutlich weniger Einträge in der sekundären Dispatcher-Table geben. Eine String-View kostet zwei «slot» — also wäre bei mehr als zwei  Zusatz-Operationen der Overhead in der ProcID selber bereits reingeholt. Allerdings gibt es noch viel zusätzliche String-Storage, und die Dispatcher-Table-Einträge. Ob sich diese amortisieren, hängt von der Differenzierung in der Praxis ab. Entsprechend gibt es auch noch den Trade-off, ob man in der ProcID weniger oder mehr Teil-Komponenten des Spec-String separat speichert +

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

+ ...und das Bauchgefühl neigt Richtung erhebliches Potential, sobald die Projekte groß und komplex werden. Für einfache Projekte, schätze ich, wird diese Lösung verschwenderisch, aber sobald wir eine fünf- bis sechstellige Anzahl an Render Nodes haben, wird das Einsparpotential gewaltig. So viele verschiedene Typen kann man in einem Projekt gar nicht haben, als daß es nicht zu einer erheblichen Reduktion der Strings und noch mehr der Dispatcher führen würde... +

+ +
+
+ + + + +

+ ...wenn es allerdings nur darum ginge, (also um die Cache-keys), könnte man genauso gut eine HashID direkt in die ProcID legen (die ja ohnehin embedded im Port liegt). Ich spekuliere also schon explizit auf das Einspar-Potential durch zusätzliche Operationen... +

+ + +
+
+ + + + +

+ Aktuell geht es nur um Zusatz-Operationen, die rein für das Testing gebraucht werden; deshalb sollte auch über einen Lazy-Init-Ansatz nochmal nachgedacht werden. Längerfristig kommen Aufgaben für Instrumentierung und Job-Planung hinzu. Diese laufen nicht in der innersten, hoch-performanten Zone. Man sollte das Thema trotzdem im Auge behalten; möglicherweise kann man Ergebniswerte für einen ganzen Render-Chain in der Exit-Node als Abkürzung speichern, und so die re-Traversierung einsparen. +

+ + +
+
+
+
+
+ + + +
+
+ + + +
@@ -99805,9 +100171,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - - +

Das ist ein realistisches Beispiel: @@ -99827,8 +100191,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + @@ -101910,6 +102273,168 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + + + + +

+ zum Einen ist das komplex, und außerdem birgt es die Gefahr der Korruption durch Updates +

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

+ keine offene Lösung +

+ + +
+ + + +

+ eine »offene« Lösung würde einer Erweiterung von Lumiera (plug-in) oder einer speziellen Library-Integration ermöglichen, Attribute durch das low-level-Model hindurch zu »tunneln«, ohne spezielle Unterstützung der Core-Implementierung. Ich halte das jedoch aktuell für einen nicht naheliegenden Ansatz, da es keinen »Rück-Kanal« vom low-level-Model in die Erweiterungen/Plug-ins gibt; generell hat sich das low-level-Model zu einer internen Struktur entwickelt und kann nicht verstanden werden ohne implizite Kenntnis mancher Entwicklungs-Aspekte +

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

+ Normalerweise verwendet man die klassischen OO-Interfaces (oder einen generischen aber getypten Kontext) auch grade dazu, die passenden Voraussetzungen durch einen Kontrakt sicherzustellen. Das ganze Unterfangen hier aber umgeht Interfaces und Kontrakt, um die Kosten dafür einzusparen. Stattdessen hinterlegen wir einen Direkt-Zugangsschlüssel, der ohne Prüfung angewendet wird..... +

+ + +
+
+ + + + +

+ ...also ist der einzige Schutz, den Schlüssel inhaltlich korrekt an die Features der ID zu binden. Diese Bindung ist aber nicht formal-logisch, sondern für eine bestimmte Intention vorkonfiguriert; ob diese Zuordnung korrekt ist und zu einem validen Aufruf führt, hängt allein daran, daß bei der Erstellung die dazu passenden Merkmale in der ID hinterlegt wurden. Da diese Klassifikation zusammen mit der Registrierung der Funktion erfolgt, ist diese Verbindung so sicher wie die Logik, die bei der Registrierung zum Tragen kommt. Das heißt, wenn es — bedingt durch eine Lücke im logischen Argument — zur Registrierung zweier inkompatibler Funktionen unter der gleichen ID kommen könnte, gibt es keinen weiteren Schutzmechanismus mehr +

+ + +
+ +
+ + + + +

+ der Aufruf ist ein blinder cast +

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

+ ...weil eine offene Lösung zwar ohne Weiteres mögilch wäre, aber mit erheblich größerem Storage-Overhead einhergeht, der sich dann erst mal durch de-Duplikation amortisieren müßte. Daher sage ich erst einmal YAGNI!  und realisiere die einfache Lösung mit direkter Kollaboration, deren Overhead und Amortisierung leicht abschätzbar ist +

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

+ erste Anwendung: PortDiagnostic::srcPorts() +

+ +
+ +
+
+
+ + + + @@ -102597,9 +103122,22 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
- + + - + + + + + + + + + + + + +