DOC: publish the microbenchmark results in the technical documentation section (closes #1086)
This commit is contained in:
parent
6f2ed76d83
commit
b3c5142c2f
2 changed files with 185 additions and 22 deletions
|
|
@ -3,10 +3,6 @@ Singletons and Dependency Handling
|
|||
:Date: 2018
|
||||
:Toc:
|
||||
|
||||
WARNING: [red]#under construction# +
|
||||
There is a rather complete page ``DependencyFactory'' in the TiddlyWiki,
|
||||
which should be integrated here
|
||||
|
||||
We encounter _dependencies as an issue at implementation level:_ In order to deal with some task at hand,
|
||||
sometimes we need to arrange matters way beyond the scope of that task. We could just thoughtlessly reach out and
|
||||
settle those extraneous concerns -- yet this kind of pragmatism has a price tag: we are now mutually dependent
|
||||
|
|
@ -23,18 +19,177 @@ can focus on its specific concern and abstract away everything else. Dependency
|
|||
application of the principle »Inversion Of Control«: each part is sovereign within its own realm, but becomes
|
||||
a client (asks for help) for anything beyond that.
|
||||
|
||||
However, 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. Nowadays,
|
||||
you can not even utter the word ``Singleton'' without everyone yelling out ``Evil! Evil!'' -- while most of these people
|
||||
at the same time feel just comfortable living in the metadata hell.
|
||||
|
||||
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<TY>`) and remains 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 ``dependency'' __ 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._
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
_tbw_
|
||||
Our *DependencyFactory* satisfies the following requirements
|
||||
|
||||
- client code is able to access some service _by-name_ -- where the name is actually
|
||||
the _type name_ of the service interface.
|
||||
- client code remains agnostic with regard to the lifecycle or backing context of the service it relies on.
|
||||
- in the simplest (and most prominent case), _nothing_ has to be done at all by anyone to manage that lifecycle. +
|
||||
By default, the Dependency Factory creates a *singleton* instance lazily (heap allocated) on demand and it ensures
|
||||
thread-safe initialisation and access.
|
||||
- we establish a policy to *disallow any significant functionality during application shutdown*.
|
||||
After leaving `main()`, only trivial dtors are invoked and possibly a few resource handles are dropped.
|
||||
No filesystem writes, no clean-up and reorganisation, not even any logging is allowed. For this reason,
|
||||
we established a link:{ldoc}/design/architecture/Subsystems.html[Subsystem] concept with explicit shutdown hooks,
|
||||
which are invoked beforehand.
|
||||
- the Dependency Factory can be re-configured for individual services (type names) to refer to an explicitly installed
|
||||
service instance. In those cases, access while the service is not available will raise an exception.
|
||||
There is a simple one-shot mechanism to reconfigure Dependency Factory and create a link to an actual
|
||||
service implementation, including automatic deregistration.
|
||||
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
_tbw_
|
||||
The DependencyFactory and thus the behaviour of dependency injection can be reconfigured, ad hoc, at runtime. +
|
||||
Deliberately, we do not enforce global consistency statically (since that would lead to one central static configuration).
|
||||
However, a runtime sanity check is performed to ensure configuration actually happens prior to any use, which means any
|
||||
invocation to retrieve (and thus lazily create) the service instance. The following flavours can be configured:
|
||||
|
||||
default::
|
||||
a singleton instance of the designated type is created lazily, on first access
|
||||
|
||||
- define an instance for access (preferably static): `Depend<Blah> theBla;`
|
||||
- access the singleton instance as `theBla().doIt()`
|
||||
|
||||
singleton subclass::
|
||||
`DependInject<Blah>::useSingleton<SubBlah>();` +
|
||||
causes the dependency factory `Depend<Bla>` to create a `SubBlah` singleton instance from now on
|
||||
|
||||
attach to service::
|
||||
`DependInject<Blah>::ServiceInstance<SubBlah> service{p1, p2, p3};`
|
||||
|
||||
- build and manage an instance of `SubBlah` in heap memory immediately (not lazily)
|
||||
- configure the dependency factory to return a reference _to this instance_
|
||||
- the instantiated `ServiceInstance<SubBlah>` object itself acts as lifecycle handle (and managing smart-ptr)
|
||||
- when it is destroyed, the dependency factory is automatically cleared, and further access will trigger an error
|
||||
|
||||
support for test mocking::
|
||||
`DependInject<Blah>::Local<SubBlah> mock;` +
|
||||
|
||||
- temporarily shadows whatever configuration resides within the dependency factory
|
||||
- the next access will create a (non singleton) `SubBlah` instance in heap memory and return a `Blah&`
|
||||
- the instantiated mock handle object again acts as lifecycle handle and smart-ptr
|
||||
to access the `SubBlah` instance like `mock->doItSpecial()`
|
||||
- when this handle goes out of scope, the original configuration of the dependency factory is restored
|
||||
|
||||
custom constructors::
|
||||
both the subclass singleton configuration and the test mock support optionally accept a functor
|
||||
or lambda argument with signature `SubBlah*()`. The contract is for this construction functor
|
||||
to return a heap allocated object, which will be owned and managed by the DependencyFactory.
|
||||
Especially this enables use of subclasses with non default ctor and / or binding to some
|
||||
additional hidden context. Please note _that this closure will be invoked later, on-demand._
|
||||
|
||||
We consider the usage pattern of dependencies a question of architecture rather --
|
||||
such can not be solved by any mechanism at implementation level. For this reason,
|
||||
Lumiera's Dependency Factory prevents reconfiguration after use, but does nothing beyond 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 that configuration, setup and initialisation code remains readable.
|
||||
And it is important to place such configuration at a 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.
|
||||
|
||||
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
|
||||
link:http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html[broken] -- 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
|
||||
link:http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11[is now a portable solution available]
|
||||
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 »happens before« 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 userspace guard variable.
|
||||
In modern C++, typically we use _Atomic variables_ as guard. In addition to well defined semantics regarding concurrent
|
||||
visibility of changes, these link:http://en.cppreference.com/w/cpp/atomic["atomics"] 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)
|
||||
|
||||
[width="80%",cols="4e,4*>",frame="topbot",options="header"]
|
||||
|==========================
|
||||
| Access Technique 2+^| development 2+^| optimised
|
||||
||[small]#singlethreaded#
|
||||
|[small]#multithreaded#
|
||||
|[small]#singlethreaded#
|
||||
|[small]#multithreaded#
|
||||
|direct invoke on shared local object | 15.13| 16.30| *1.00*| 1.59
|
||||
|invoke existing object through unique_ptr | 60.76| 63.20| 1.20| 1.64
|
||||
|lazy init unprotected (not threadsafe) | 27.29| 26.57| 2.37| 3.58
|
||||
|lazy init always mutex protected | 179.62| 10917.18| 86.40| 6661.23
|
||||
|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:
|
||||
|
||||
- The numbers obtained pretty much confirm
|
||||
link:http://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton[other people's measurments].
|
||||
- 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
|
||||
- However, the concurrency situation in this example is rather extreme and deliberately provokes collisions;
|
||||
in practice we'd be closer to the single threaded case
|
||||
- Double Checked Locking is a very effective implementation strategy and results in timings
|
||||
within the same order of magnitude as direct unprotected access
|
||||
- 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
|
||||
|
||||
Performance
|
||||
~~~~~~~~~~~
|
||||
_tbw_
|
||||
|
||||
|
||||
Architecture
|
||||
|
|
@ -62,8 +217,8 @@ It suffices thus, when the _leading subsystem_ (the UI or the script runner) ref
|
|||
external activities, _prior_ to reaching that point in the lifecycle where everything is ``basically set''.
|
||||
Yet 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
|
||||
``pull'' 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.
|
||||
``pull'' session contents as a dependency 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 ``shell''. Rather, in Lumiera the UI must be conceived
|
||||
as a _collection of services_ -- and when running, a _population request_ can be issued to fill the prepared
|
||||
|
|
|
|||
|
|
@ -10419,9 +10419,9 @@
|
|||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1520721753927" ID="ID_1564232821" MODIFIED="1520721763166" TEXT="DependencyFactory verwenden">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node CREATED="1520721767005" ID="ID_364557801" MODIFIED="1520721776960" TEXT="dazu muß diese erst mal ausgebaut werden"/>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1520721792978" ID="ID_871055051" MODIFIED="1520722011920" TEXT="#1086 unify Depend singleton and instance management">
|
||||
<node COLOR="#338800" CREATED="1520721792978" ID="ID_871055051" MODIFIED="1522739052423" TEXT="#1086 unify Depend singleton and instance management">
|
||||
<arrowlink COLOR="#7188b5" DESTINATION="ID_451964727" ENDARROW="Default" ENDINCLINATION="2227;-4042;" ID="Arrow_ID_1654818003" STARTARROW="None" STARTINCLINATION="775;393;"/>
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
|
|
@ -26610,21 +26610,25 @@
|
|||
<node CREATED="1482524498822" ID="ID_431883229" MODIFIED="1518487921096" TEXT="Datenstrom"/>
|
||||
<node CREATED="1482524516371" ID="ID_396707258" MODIFIED="1518487921096" TEXT="Event-Sourcing"/>
|
||||
<node CREATED="1482524530842" ID="ID_606738640" MODIFIED="1520718477944" TEXT="Dependency-Injection">
|
||||
<node CREATED="1515975589922" ID="ID_367021032" MODIFIED="1515975593045" TEXT="Architektur"/>
|
||||
<node CREATED="1515975589922" ID="ID_367021032" MODIFIED="1515975593045" TEXT="Architektur">
|
||||
<node CREATED="1522738923747" ID="ID_1842493496" MODIFIED="1522738930926" TEXT="feste Ordnung der Komponenten"/>
|
||||
<node CREATED="1522738931490" ID="ID_1729361793" MODIFIED="1522738935077" TEXT="Lebenszyklus"/>
|
||||
</node>
|
||||
<node CREATED="1515975593614" ID="ID_79714950" MODIFIED="1515975595605" TEXT="Technik">
|
||||
<node CREATED="1515975596673" ID="ID_1304673048" MODIFIED="1515975599660" TEXT="für Services">
|
||||
<node CREATED="1515975622405" ID="ID_962877206" MODIFIED="1515975633971" TEXT="Dependency-Injection-Manager">
|
||||
<icon BUILTIN="button_cancel"/>
|
||||
</node>
|
||||
<node CREATED="1515975635484" ID="ID_225934542" MODIFIED="1515975645478" TEXT="einfache Lösungen">
|
||||
<node CREATED="1515975650458" ID="ID_1359158691" MODIFIED="1515976422894" TEXT="Serivice by-Name">
|
||||
<node CREATED="1515975650458" ID="ID_1359158691" MODIFIED="1522738941329" TEXT="Serivice by-Name">
|
||||
<linktarget COLOR="#b6829b" DESTINATION="ID_1359158691" ENDARROW="Default" ENDINCLINATION="1134;-1646;" ID="Arrow_ID_927211935" SOURCE="ID_1305671938" STARTARROW="None" STARTINCLINATION="1359;916;"/>
|
||||
<font BOLD="true" NAME="SansSerif" SIZE="12"/>
|
||||
<node CREATED="1515975677462" ID="ID_1848164117" MODIFIED="1515975685784" TEXT="Problem: Lebenszyklus">
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1492094235291" HGAP="36" ID="ID_451964727" MODIFIED="1522030153616" TEXT="#1086 unify Depend singleton and instance management" VSHIFT="-20">
|
||||
<node COLOR="#338800" CREATED="1492094235291" FOLDED="true" HGAP="36" ID="ID_451964727" MODIFIED="1522738871131" TEXT="#1086 unify Depend singleton and instance management" VSHIFT="-20">
|
||||
<linktarget COLOR="#7188b5" DESTINATION="ID_451964727" ENDARROW="Default" ENDINCLINATION="2227;-4042;" ID="Arrow_ID_1654818003" SOURCE="ID_871055051" STARTARROW="None" STARTINCLINATION="775;393;"/>
|
||||
<linktarget COLOR="#5c71a3" DESTINATION="ID_451964727" ENDARROW="Default" ENDINCLINATION="767;-2073;" ID="Arrow_ID_1454095581" SOURCE="ID_1714114896" STARTARROW="None" STARTINCLINATION="1892;380;"/>
|
||||
<linktarget COLOR="#8697be" DESTINATION="ID_451964727" ENDARROW="Default" ENDINCLINATION="200;-562;" ID="Arrow_ID_1211717131" SOURCE="ID_1032947061" STARTARROW="None" STARTINCLINATION="1387;-152;"/>
|
||||
<icon BUILTIN="pencil"/>
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node CREATED="1520722130803" ID="ID_1194364308" MODIFIED="1521433923249" TEXT="Anforderungen">
|
||||
<icon BUILTIN="yes"/>
|
||||
<node CREATED="1520722192387" ID="ID_1178804552" MODIFIED="1520722204325" TEXT="leicht zu verwenden">
|
||||
|
|
@ -28014,8 +28018,8 @@
|
|||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521160700669" HGAP="4" ID="ID_978221585" MODIFIED="1521791636399" TEXT="Dokumentation" VSHIFT="25">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521160700669" FOLDED="true" HGAP="4" ID="ID_978221585" MODIFIED="1522738834553" TEXT="Dokumentation" VSHIFT="25">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node COLOR="#338800" CREATED="1521790595841" ID="ID_1874367277" MODIFIED="1521936684007" TEXT="Doku-Text im TiddlyWiki">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
<node COLOR="#338800" CREATED="1521790681557" ID="ID_1346970835" MODIFIED="1521936679674" TEXT="die generelle Haltung bezügl. Performance / Korrektheit">
|
||||
|
|
@ -28083,8 +28087,8 @@
|
|||
<node COLOR="#338800" CREATED="1521929186281" ID="ID_1078337367" MODIFIED="1522025355714" TEXT="ein, zwei Einleitungssätze dazu">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
<node BACKGROUND_COLOR="#eee5c3" COLOR="#990000" CREATED="1521929199799" ID="ID_507602296" MODIFIED="1521929217037" TEXT="Wichtig: die Benchmark-Daten hier auch darstellen">
|
||||
<icon BUILTIN="flag-yellow"/>
|
||||
<node COLOR="#338800" CREATED="1521929199799" ID="ID_507602296" MODIFIED="1522738826337" TEXT="Wichtig: die Benchmark-Daten hier auch darstellen">
|
||||
<icon BUILTIN="button_ok"/>
|
||||
</node>
|
||||
</node>
|
||||
</node>
|
||||
|
|
@ -29237,7 +29241,11 @@
|
|||
<node CREATED="1515975995564" ID="ID_1901876074" MODIFIED="1515975996512" TEXT="wenn geschlossen, dann Fehler werfen"/>
|
||||
</node>
|
||||
</node>
|
||||
<node CREATED="1515976632168" ID="ID_346073460" MODIFIED="1515976637675" TEXT="Verdrahtung"/>
|
||||
<node CREATED="1515976632168" ID="ID_346073460" MODIFIED="1515976637675" TEXT="Verdrahtung">
|
||||
<node CREATED="1522738962198" ID="ID_66084476" MODIFIED="1522738989254" TEXT="per ctor-Parameter bei Services"/>
|
||||
<node CREATED="1522738949663" ID="ID_1169687839" MODIFIED="1522738958530" TEXT="per closure"/>
|
||||
<node CREATED="1522738993353" ID="ID_134172826" MODIFIED="1522739006692" TEXT="per rekursivem Depend + Lifecycle"/>
|
||||
</node>
|
||||
</node>
|
||||
<node CREATED="1515975646354" ID="ID_1049835373" MODIFIED="1515975649630" TEXT="Singleton">
|
||||
<node COLOR="#338800" CREATED="1515976424292" ID="ID_357830540" MODIFIED="1515976443809" TEXT="meist gut genug">
|
||||
|
|
|
|||
Loading…
Reference in a new issue