DOC: reduce DependencyFactory page in the TiddlyWiki

...since it has been published almost 1:1 on the Lumiera website.
Retain only some technical reference information here
This commit is contained in:
Fischlurch 2018-04-04 01:43:24 +02:00
parent b3c5142c2f
commit fb8a5333fc

View file

@ -1927,11 +1927,11 @@ As we don't have a Prolog interpreter on board yet, we utilize a mock store with
{{{default(Obj)}}} is a predicate expressing that the object {{{Obj}}} can be considered the default setup under the given conditions. Using the //default// can be considered as a shortcut for actually finding an exact and unique solution. The latter would require to specify all sorts of detailed properties up to the point where only one single object can satisfy all conditions. On the other hand, leaving some properties unspecified would yield a set of solutions (and the user code issuing the query had to provide means for selecting one solution from this set). Just falling back on the //default// means that the user code actually doesn't care for any additional properties (as long as the properties he //does// care for are satisfied). Nothing is said specifically on //how//  this default gets configured; actually there can be rules //somewhere,// and, additionally, anything encountered once while asking for a default can be re-used as default under similar circumstances.
&amp;rarr; [[implementing defaults|DefaultsImplementation]]</pre>
</div>
<div title="DependencyFactory" creator="Ichthyostega" modifier="Ichthyostega" created="201803110155" modified="201803242359" tags="def Concepts draft" changecount="77">
<div title="DependencyFactory" creator="Ichthyostega" modifier="Ichthyostega" created="201803110155" modified="201804032335" tags="def Concepts draft" changecount="80">
<pre>//Access point to dependencies by-name.//
In the Lumiera code base, we refrain from building or using a full-blown Dependency Injection Container. A lot of FUD has been spread regarding Dependency Injection and Singletons, to the point that a majority of developers confuses and conflates the ~Inversion-of-Control principle (which is essential) with the use of a ~DI-Container. Today, you can not even mention the word &quot;Singleton&quot; without everyone yelling out &quot;Evil! Evil!&quot; -- while most of these people just feel comfortable living in the metadata hell.
In the Lumiera code base, we refrain from building or using a full-blown Dependency Injection Container. Rather, we rely on a generic //Singleton Factory// -- which can be augmented into a //Dependency Factory// for those rare cases where we actually need more instance and lifecycle management beyond lazy initialisation. Client code indicates the dependence on some other service by planting an instance of that Dependency Factory (for Lumiera this is {{{lib::Depend&lt;TY&gt;}}}). The //essence of a &quot;dependency&quot;// of this kind is that we ''access a service //by name//''. And this service name or service ID is in our case a //type name.//
Not Singletons as such are problematic -- rather, the coupling of the Singleton class itself with the instantiation and lifecycle mechanism is what creates the problems. This situation is similar to the use of //global variables,// which likewise are not evil as such; the problems arise from an imperative, operation driven and data centric mindset, combined with hostility towards any abstraction. In C++ such problems can be mitigated by use of a generic //Singleton Factory// -- which can be augmented into a //Dependency Factory// for those rare cases where we actually need more instance and lifecycle management beyond lazy initialisation. Client code indicates the dependence on some other service by planting an instance of that Dependency Factory (for Lumiera this is {{{lib::Depend&lt;TY&gt;}}}) and remain unaware if the instance is created lazily in singleton style (which is the default) or has been reconfigured to expose a service instance explicitly created by some subsystem lifecycle. The //essence of a &quot;dependency&quot;// of this kind is that we ''access a service //by name//''. And this service name or service ID is in our case a //type name.//
&amp;rarr; see the Introductory Page about [[Dependencies|http://lumiera.org/documentation/technical/library/Dependencies.html]] in Lumiera online documentation.
!Requirements
Our DependencyFactory satisfies the following requirements
@ -1973,12 +1973,6 @@ We consider the usage pattern of dependencies a question of architecture rather
For this reason, Lumiera's Dependency Factory prevents reconfiguration after use, but does nothing exceeding such basic sanity checks
!!!Performance considerations
We acknowledge that such a dependency or service will be accessed frequently and even from rather performance critical parts of the application. We have to optimise for low overhead on access, while initialisation happens only once and can be arbitrarily expensive. It is more important for configuration, setup and initialisation to be readable, and to be found at that location within the code where the related concerns are treated -- which is not at the usage site, and which is likewise not within some global central core application setup. At which point precisely initialisation happens is a question of architecture -- lazy initialisation can be used to avoid expensive setup of rarely used services, or it can be employed to simplify the bootstrap of complex subsystems, or to break service dependency cycles. All of this builds on the assumption that the global application structure is fixed and finite and well-known -- we assume we are in full control about when and how parts of the application start and stop working.
Our requirements on (optional) reconfigurability have some impact on the implementation technique though, since we need access to the instance pointer for individual service types. This basically rules out //Meyers Singleton// -- and so the adequate implementation technique for our usage pattern is //Double Checked Locking.// In the past, there was much debate about DCL being [[broken|http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html]] -- which indeed was true when //assuming full portability and arbitrary target platform.// Since our focus is primarily on ~PC-with-Linux systems, this argument seems to lean more to the theoretical side though, since the x86/64 platform is known to employ rather strong memory and cache coherency constraints. With the recent advent of ARM systems, the situation has changed however. Anyway, since C++11 there [[is now a portable solution available|http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/]] for writing a correct DCL implementation, based on {{{std::atomic}}}.
The idea underlying Double Checked Locking is to optimise for the access path, which is achieved by moving the expensive locking entirely out of that path. However, any kind of concurrent consistency assertion requires us to establish a &quot;happens before&quot; relation between two events of information exchange. Both traditional locking and lock-free concurrency implement this relation by establishing a ''synchronises-with'' relation between two actions on a common ''guard'' entity -- for traditional locking, this would be a Lock, Mutex, Monitor or Semaphore, while lock-free concurrency uses the notion of a //fence// connected with some well defined action on a guard variable. In modern C++, typically we use //Atomic variables// as guard. In addition to well defined semantics regarding concurrent visibility of changes, these [[&quot;atomics&quot;|http://en.cppreference.com/w/cpp/atomic]] offer indivisible access and exchange operations. A correct concurrent interaction must involve some kind of well defined handshake to establish the aforementioned synchronises-with relation -- otherwise we just can not assume anything. Herein lies the problem with Double Checked Locking: when we move all concurrency precautions away from the optimised access path, we get performance close to a direct local memory access, but we can not give any correctness assertions in this setup. If we are lucky (and the underlying hardware does much to yield predictable behaviour), everything works as expected, but we can never be sure about that. A correct solution thus inevitably needs to take away some of the performance from the optimised access path. Fortunately, with properly used atomics this price tag is known to be low. At the end of the day, correctness is more important than some superficially performance boost.
To gain insight into the rough proportions of performance impact, in 2018 we conducted some micro benchmarks (using a 8 core AMD ~FX-8350 64bit CPU running Debian/Jessie and GCC 4.9 compiler)
The following table lists averaged results //in relative numbers,// in relation to a single threaded optimised direct non virtual member function invocation (≈ 0.3ns)
| !Access Technique |&gt;| !development |&gt;| !optimised |
@ -1990,7 +1984,7 @@ The following table lists averaged results //in relative numbers,// in relation
|Double Checked Locking with mutex | 27.37| 26.27| 2.04| 3.26|
|DCL with std::atomic and mutex for init | 44.06| 52.27| 2.79| 4.04|
These benchmarks used a dummy service class holding a volatile int, initialised to a random value. The complete code was visible to the compiler and thus eligible for inlining. Repeatedly the benchmarked code accessed this dummy object through the means listed in the table, then retrieved the (actually constant) value from the private volatile variable within the service and compared it to zero. This setup ensures the optimiser can not remove the code altogether, while the access to the service dominates the measured time. The concurrent measurement used 8 threads (number of cores), each performing the same timing loop on a local instance. The number of invocations within each thread was high enough (several millions) to amortise the actual costs of object allocation. Some observations:
Some observations:
* The numbers obtained pretty much confirm [[other people's measurments|http://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton]]
* Synchronisation is indeed necessary; the unprotected lazy init crashed several times randomly during multithreaded tests.
* Contention on concurrent access is very tangible; even for unguarded access the cache and memory hardware has to perform additional work
@ -1999,12 +1993,6 @@ These benchmarks used a dummy service class holding a volatile int, initialised
* Unprotected lazy initialisation performs spurious duplicate initialisations, which can be avoided by DCL
* Naïve Mutex locking is slow even with non-recursive Mutex without contention
* Optimisation achieves access times around ≈ 1ns
!Architecture
Dependency management does not define the architecture, nor can it solve architecture problems. Rather, its purpose is to enact the architecture. A dependency is something we need to do the task at hand, yet a dependency lies beyond that task and relates to concerns outside the scope and theme of this actual task. A naïve functional approach -- pass everything you need as argument -- would be as harmful as thoughtlessly manipulating some off-site data to fit current needs. The local function would be splendid, strict and referentially transparent -- yet anyone using it would be infected with issues of tangling and tight coupling. As remedy, a //global context// can be introduced, which works well as long as this global context does not exhibit any other state than &quot;being available&quot;. The root of those problems lies in the drive to conceive matters simpler as they are.
* collaboration to form a whole typically leads to indirect mutual dependency. We can only define precisely //what is required locally,// and then //pull our requirements// on demand.
* a given local action can be part of a process, or a conversation or interaction chain, which in turn might originate from various, quite distinct contexts. At //that level,// we might find a simpler structure to hinge questions of lifecycle on.
In Lumiera we encounter both these kinds of circumstances. On a global level, we have a simple and well defined order of dependencies, cast into [[Subsystem relations|Subsystem]]. We know e.g. that mutating changes to the session can originate from scripts or from UI interactions. It suffices thus, when the //leading subsystem// (the UI or the script runner) refrains from emitting any external activities, prior to reaching that point in the lifecycle where everything is &quot;basically set&quot;. However self evident this insight might be, it yields some unsettling and challenging consequences: The UI //must not assume// the presence of specific data structures within the lower layers, nor is it allowed to &quot;pull&quot; session contents while starting up. Rather the UI-Layer is bound to bootstrap itself into completely usable and operative state, without the ability to attach anything onto existing tangible content structures. This runs completely counter common practice of UI programming, where it is customary to wire most of the application internals somehow directly below the UI &quot;shell&quot;. Rather, in Lumiera the UI must be conceived as a [[collection of services|GuiTopLevel]] -- and when running, a //population request// can be issued to fill the UI framework with content. This is inversion-of-control at work.
</pre>
</div>
<div title="DesignDecisions" modifier="Ichthyostega" created="200801062209" modified="201505310104" tags="decision design discuss Concepts" changecount="5">