* either the default INIT_MOVE operator works ('=')
* either an INIT_MOVE method is defined
* or it is disabled.
As a consequence, fix usage of testobj_t and mpz_t in the repository
The emulation layer of INIT_MOVE using INIT_SET+CLEAR is removed:
it wasn't working correctly with the local memory context
(INIT_MOVE doesn't need any local memory context to work,
whereas INIT_SET/CLEAR does)
Remove macro M_DO_INIT_MOVE as it is now useless and uses M_CALL_INIT_MOVE.
The INIT_MOVE default method no longer sets the old 'src' to '0':
it was useless (it doesn't catch any error) and complicates compiler parsing.
Fix POD & A1 & CSTR & classic oplists definition (missing INIT_MOVE operators).
821 lines
32 KiB
Org Mode
821 lines
32 KiB
Org Mode
*Long term issues*
|
|
==========================================================
|
|
|
|
* DONE #34 : Pass old size arguments to memory allocator :ENHANCEMENT:
|
|
|
|
** State
|
|
|
|
Some custom allocators don't store the old size of an object to maximize memory usage
|
|
used by the allocated objects. On realloc, they need the old size argument.
|
|
This is inline with the GMP allocator which provides the old size argument.
|
|
There is no difficulty in the data structure to compute the old size of the object
|
|
(it is quite easily available).
|
|
It is needed for REALLOC operator (and FREE)
|
|
|
|
** Solution #1
|
|
|
|
Add the oldsize argument to the REALLOC and FREE operators.
|
|
|
|
** Solution #2
|
|
|
|
Create a local variable m_local_old_size before calling the REALLOC or FREE operators
|
|
Modify GAIA interface to link OLDSIZE patttern to m_local_old_size
|
|
Need to handle the warnings correctly.
|
|
|
|
** Tradeof analysis
|
|
|
|
Solution #1 is simpler but breaks API. However custom allocators are not common.
|
|
Solution #2 keeps API as it is but increases code complexity by adding more hacks into the codebase.
|
|
|
|
* TODO #33 : Handling of partially constructed object :ANOMALY:
|
|
** State
|
|
|
|
Support of partially constructed object needs special code to handle it
|
|
correctly (register a correct cleanup function in case of exception).
|
|
|
|
Current containers don't support such features.
|
|
|
|
> NOTE:
|
|
An exception for init_set supposes that the object is *not* initialized.
|
|
An exception for set supposes that the object *remains* initialized.
|
|
Some containers perform an init_set using init+set
|
|
others perform a set using a clear+init_set
|
|
The property of init_set (resp. set) is broken if an exception is raised in the set (resp. init_set):
|
|
the object is not let in the expected uninitialized (resp. initialized) state.
|
|
|
|
** Actions
|
|
|
|
Add such support in M*LIB containers.
|
|
Need to analyse each function for execption safety.
|
|
|
|
It can be difficult. For example, for array _init_set
|
|
function due to the copy of the object one per one. Each copied sub object
|
|
may throw an exception, so we always need to have the container in a correct
|
|
state so that it can be cleanup.
|
|
|
|
* DONE #32 : User specialization fields in the container :ENHANCEMENT:
|
|
** State
|
|
|
|
Some methods of a contained object may need to have some options that
|
|
are neither constant nor global, but are specialized in function of the container.
|
|
|
|
For example, a memory handler may need a parameter which is needed to identify
|
|
which pool of memory has to be used.
|
|
|
|
** Plan:
|
|
|
|
Add a new operator "CONTEXT" which is a type to be added in the
|
|
defined container. Then it can be accessed by the operator through an extension
|
|
of the GAIA by adding support for identification of specialization like this:
|
|
|
|
( CONTEXT( memory_pool_ref ),
|
|
NEW( API(NONE, memory_pool_alloc, CONTEXT, ARG1)),
|
|
DEL( API(NONE, memory_pool_free, CONTEXT, ARG1)) )
|
|
|
|
** Open points:
|
|
|
|
- How to get access to the specialization field of the container?
|
|
==> Define the "self" name as the name of the container for all methods?
|
|
- What to do when there are several containers for a method?
|
|
==> self shall be the destination?
|
|
|
|
- How to initialize the specialization field of the container? provide custom initialization functions? It seems to be the hard point.
|
|
Use GAIA interface and a local variable with force naming : m_local_context for example.
|
|
|
|
** Proposed Solution:
|
|
|
|
New operator USER_CONTEXT created. If operator USER_CONTEXT exist:
|
|
+ add a USER_CONTEXT user_data in the data structure.
|
|
+ for INIT & INIT_WITH, add a new parameter to the function to pass the context to initialize
|
|
+ for INIT_SET & INIT_MOVE & MOVE, initialize it with the source object (FIXME: This may not be what we want for INIT_SET if we want to copy an object from an allocator to another)
|
|
+ for each functions that use an operator that may use the USER_CONTEXT, add a local variable m_local_context and initialize to the user context
|
|
==> Limit user context to pointer or integer (Good enough)
|
|
+ Expand GAIA interface to handle specially the term USER_CONTEXT: if it exists, expands it to m_local_context. The user will therefore be able to expand its operator with the user provided local context.
|
|
+ Update OPLIST of data structure to change the call to INIT & INIT_WITH to use GAIA/USER_CONTEXT if the basic type has a USER_CONTEXT
|
|
+ To be checked: EMPLACE_TYPE ?
|
|
+ Create M_USING_CONTEXT to initialize a local variable m_local_context and initalize it with the user context to be used y the M_LET macro:
|
|
M_USING_CONTEXT(int *, &y)
|
|
M_LET(x, object_t) {
|
|
|
|
}
|
|
|
|
Done in branch feature/user-data-v2 with a full working implementation for array
|
|
|
|
** Solution limitation:
|
|
|
|
INIT & INIT_WITH interface change for the user. This however is an acceptable limitation.
|
|
|
|
Adding a user context to each data structure will increase a lot the size of the data structure in case of recursive data structures: For example, an array of string_t. Each string_t will need a user data context for it to work properly, so we'll need N*sizeof(user data context) memory just to make it work. This becomes even more problematic if we chain more data structures.
|
|
And in general, we use custom allocators to be faster and use less memory: this will defeat the purpose of custom allocators a lot.
|
|
|
|
Another limitation is that string_t / bitset_t are not supported correctly. We can make this change globally but it will prevent mixing different codebase that uses the string_t together (one expects a user context, the other not).
|
|
|
|
** Alternative #1
|
|
|
|
Passing user context arguments to each function call seems to be a little bit better.
|
|
It might however reduces code optimization as one more register will be user everywhere in the calling chain, which may huge.
|
|
|
|
** Alternative #2
|
|
|
|
The custom allocator could have a thread default allocator as a global variable (thread attribute). It seems to be a better solution, more scalable.
|
|
The custom allocator will need to be able to switch quickly between scrach arena and permanent arena. This seems to be easy.
|
|
|
|
However switching allocators puts a burden to the user:
|
|
the user needs to properly be sure to call the destructor with the custom allocator set to be the same as when it was created.
|
|
This might not even be possible in case of exceptions.
|
|
|
|
* TODO #31 : Uniformize parametrization options of containers :ENHANCEMENT:
|
|
|
|
** State
|
|
|
|
Currently in order to pass some parametrization of the containers,
|
|
the parameters are passed to the generation macros through:
|
|
|
|
- extra parameters of the macros,
|
|
- special operators of the contained object oplist.
|
|
|
|
The extra parameters of some generation macros break the uniformity
|
|
of the interface.
|
|
It is conceptually wrong to put some special operators of the contained object
|
|
that have only meanings for the container object.
|
|
In case of adding new options, it break the existing interface or put another
|
|
operator in the contained object oplist.
|
|
|
|
|
|
The following parameters are used:
|
|
|
|
- Fixed size of a queue (buffer / B+Tree),
|
|
- Policy of a queue (buffer),
|
|
- Type of the container, IT type of the container, Ref type of the iterator
|
|
|
|
The following operators are used:
|
|
|
|
- NEW / DEL / REALLOC / FREE
|
|
- MEMPOOL / MEMPOOL_LINKAGE
|
|
|
|
The following new options is planned:
|
|
|
|
- Interface (See #28)
|
|
- Extra parameter for memory allocation function
|
|
|
|
** Plan
|
|
|
|
The M\_*\_DEF\_AS macros shall be changed like this:
|
|
|
|
M_*_DEF_AS(name, cont_oplist, type[, type_oplist])
|
|
|
|
with cont\_oplist being the container oplist that gives parametrization to the container definition.
|
|
The following operators are planned:
|
|
|
|
- TYPE: to define the type of the container,
|
|
- IT\_TYPE: to define the IT type of the container,
|
|
- SUBTYPE: to define the type pointed by the iterator,
|
|
- FIXED\_SIZE: to define the fixed size of a container,
|
|
- POLICY: to define the policy of a container,
|
|
- NEW / DEL / REALLOC / FREE: to define the memory interface,
|
|
- MEMPOOL / MEMPOOL_LINKAGE: to request a definition of a specialized memory interface,
|
|
- INTERFACE: to request a pure header definition, an implementation or both (see #28),
|
|
- CONTEXT: to request an extra context parameter to add in the container.
|
|
|
|
All operators should be optional.
|
|
|
|
The NEW / DEL / REALLOC / FREE / MEMPOOL / MEMPOOL_LINKAGE won't have any effects anymore if defined in the contained object oplist.
|
|
|
|
** Pro:
|
|
|
|
- Uniformize interface
|
|
|
|
** Con:
|
|
|
|
- API breakage. It can be mitigated by providing an adaptating layer by prividing the new interface for M\_*\_DEF\_AS only
|
|
and *\_DEF\_AS will provide a translation layer.
|
|
|
|
* TODO #30 : Serialization rework :ENHANCEMENT:
|
|
|
|
** State
|
|
Currently, each container supports 3 serialization methods:
|
|
|
|
- old format to FILE
|
|
- old format to string
|
|
- generic serialization
|
|
|
|
Generic serialiation connect the container format to the serialization object constraints.
|
|
It is done through a vtable. As such there is a performance penality and it avoids proper inlining.
|
|
It is however quite flexible and decouple the data structure from the serialization format.
|
|
|
|
** Evolution
|
|
|
|
Old format should be deprecated and the functions implemented it shall use the generic serialization interface.
|
|
|
|
Serialization object shall provide a special OPLIST of serialization.
|
|
For example M-CORE will provide OLD Format oplist and M-SERIAL-JSON will provide JSON format oplist.
|
|
Each oplist will provide the suffix needed for the serialization, and the interface
|
|
(see already existing interface).
|
|
|
|
Then a container will generate specialized serialization methods for each provided oplist.
|
|
|
|
** Pros:
|
|
|
|
- Faster
|
|
- In the M*LIB philosphy: much like other oplist usage.
|
|
|
|
** Cons:
|
|
|
|
- compatibility breakage.
|
|
- increase code complexity.
|
|
|
|
** Open point:
|
|
|
|
- how can a user add a new serialization object? ==> See solution implemented by M-GENERIC.
|
|
- Can we make a generic serialization object to support migration path? Seems possible.
|
|
|
|
** Example
|
|
|
|
serial-json provides
|
|
|
|
#define M_SERIAL_1() (NAME(_json), READ_INT(...), WRITE_INT(...) )
|
|
|
|
containers provides
|
|
|
|
For i in 1 100 ; do
|
|
if do exist M_SERIAL_ ## i ()
|
|
expand(SERIAL_METHOD with M_SERIAL_1())
|
|
|
|
SERIAL_METHOD(name, ....)
|
|
static inline m_serial_return_code_t \
|
|
M_C(M_F(name, _out_serial),M_GET_NAME(serial_op) )(M_GET_TYPE(serial_op) f, const list_t list) \
|
|
|
|
* DONE #29 : Support of partial initialized object :ANOMALY:
|
|
|
|
** State
|
|
|
|
In case of exception during the construction of an object,
|
|
if the object is partially constructed, there is no destructor to call.
|
|
Therefore if there is some partially constructed object
|
|
(with some fields already fully constructed), there will be some links.
|
|
This is the same problem with C++
|
|
|
|
** Proposition
|
|
|
|
Provide new macros to perform the partial initialization before doing the remaining taf.
|
|
Something like:
|
|
|
|
M_CHAIN_INIT( init_code, clear_code)
|
|
|
|
or
|
|
|
|
x is { string_t s; string_t d; int num; }
|
|
|
|
void struct_init_set(struct_t x, const struct_t y)
|
|
{
|
|
// First thing is Chained Initialization
|
|
M_CHAIN_INIT( string_init_set(x->s, y->s), string_clear(x->s) );
|
|
M_CHAIN_INIT( string_init_set(x->d, y->d), string_clear(x->d) );
|
|
// End of chain init
|
|
x->num = y->num;
|
|
}
|
|
|
|
M_CHAIN_INIT will push the clean code pretty much like M_LET.
|
|
Everything else is done in M_LET: when the constructor fully returns,
|
|
the index of the clean code to call is reset to its value before calling
|
|
the constructor. It means that the partially initialized code will be removed
|
|
from this stack before the clean method of the structure itself is pushed.
|
|
|
|
If an exception is raised after a M_CHAIN_INIT, only the clean methods
|
|
of the M_CHAIN_INIT are called.
|
|
|
|
* TODO #28 : Separate generation of interface to implementation :ENHANCEMENT:
|
|
|
|
** State
|
|
Enable support for generating an interface only for the headers
|
|
and an implementation only for the source code.
|
|
|
|
** Analysis
|
|
Try to keep API compatibility
|
|
==> Only modify renamed macro with M_ prefix by giving a new mandatory
|
|
argument for such generation.
|
|
|
|
This argument is a generation parametric argument.
|
|
oplist is a parameter that define the property of the contained type.
|
|
It could be used to pass this information but it will be an abuse of interface.
|
|
Another argument is needed.
|
|
|
|
Used arguments to parameter generation not related to the contained type:
|
|
|
|
- memory interface (NEW / DEL / ...)
|
|
- queue policy
|
|
- fixed size for queue
|
|
- fixed node size for B+Tree
|
|
- generate static inline, public (extern) or private (define) functions.
|
|
- name of the type.
|
|
- name of the iterator
|
|
- name of the iterated element.
|
|
- ...
|
|
|
|
Use another oplist as argument for M_.._DEF_AS (See #31)
|
|
|
|
M_BUFFER_DEF_AS(name, (TYPE(toto), FIXED_SIZE(10), POLICY(STACK), INTERFACE(header)), int, M_BASIC_OPLIST)
|
|
|
|
* TODO #27 : Uniformization of m-buffer interface :ENHANCEMENT:
|
|
|
|
** State
|
|
|
|
The m-buffer interface requires special argument to work:
|
|
- size & policy for BUFFER,
|
|
- policy only for the others.
|
|
This is not uniform with the others generation macros and not uniform with each other.
|
|
Moreover the others are the only one which can be used in a shared memory, so
|
|
a fixed size will be good to have too for such interface.
|
|
|
|
** Plan
|
|
|
|
- Uniformization of the interface (See #31)
|
|
- Provide static size for all QUEUE.
|
|
|
|
* DONE #26 : Hash-Map for huge objects :ENHANCEMENT:
|
|
|
|
Specialize Hashmap for huge objects on supported hardware:
|
|
|
|
Based on OA hash map with taggued pointer (needs space for tagging):
|
|
low bits = high bits of hash
|
|
pointer to object { key, hash, value }
|
|
|
|
Can solve collision without dereferencing object with 1/2^lowbits chances.
|
|
NB: hash may be uneeded due to the small chances of collision.
|
|
Reduce cache usage ==> Should be faster.
|
|
Increase cache usage for very small object (integer).
|
|
|
|
Use of pointer to avoid moving object on GC.
|
|
|
|
On AMD64, only 48 bits of 64 bits are really used for addressing data.
|
|
==> lowbits= 16
|
|
Empty representation= 0 (hash can be 0. pointer cannot be it).
|
|
Deleted representation = 1 (pointer cannot be zero)
|
|
|
|
<---- PTR ----><-HASH->
|
|
|
|
#+BEGIN_SRC C
|
|
#define LOW_BITS 16
|
|
#define ALIGNED_BITS 2
|
|
void *get_ptr(int64_t x) {
|
|
return (void*) ((x >> LOW_BITS) << ALIGNED_BITS);
|
|
}
|
|
unsigned get_small_hash(int64_t x) {
|
|
return x & ((1<<LOW_BITS)-1);
|
|
}
|
|
#+END_SRC
|
|
|
|
Can also (should?) use SIMD to test for several hash entries at the same times
|
|
In which case a complete new implementation will be needed
|
|
Note: SIMD doesn't seem to be a win if not handled properly.
|
|
Since the first guess of a good hashtable shall give the right entry > 90% of the time.
|
|
So 90% of the times SIMD will pay the cost of reading not needed memory.
|
|
It might still be a win, but proper tuning needs to be done.
|
|
|
|
* TODO #25 : Support of error return model for error handling. :ENHANCEMENT:
|
|
|
|
** State
|
|
Find a way to support error return code for the API in case of allocation
|
|
failure.
|
|
|
|
** Analysis
|
|
Any service that returns void shall return a "int" (or another type).
|
|
In case of allocation failure, it shall return an error.
|
|
M_CALL macro shall stop its execution if the service returns an error code
|
|
and the error code represents an error (avoid rewritting everything)
|
|
and throw back the error code (stopping the execution flow).
|
|
|
|
Services returning already something shall not be modified but returns the error code embedded (like a NULL pointer).
|
|
|
|
This model should be applied at the container level only and not globally.
|
|
Different containers may need different levels of error handling.
|
|
4 combinaison to take into account:
|
|
|
|
==> ABORT on container / ABORT on used type: The current model
|
|
==> ABORT on container / RETCODE on used type: Needs to abort on reported error.
|
|
==> RETCODE on container / ABORT on used type: Nothing particular to note.
|
|
==> RETCODE on container / RETCODE on used type: Forward retcode to caller.
|
|
|
|
Find a way to make it while not making the code too complex
|
|
(try to keep as simple as possible).
|
|
|
|
Using a specialized oplist for such containers (_OPLIST_RETCODE)
|
|
using a specialized API for retcode:
|
|
|
|
API_RET_*: that expands to
|
|
|
|
if (func(args) == 0) goto handler_exit_failure;
|
|
|
|
Adding at the end of each service:
|
|
M_END_FUNCTION(cleanup_code) that expands to different cases:
|
|
M_END_FUNCTION_VALUE(cleanup_code, return_success, return_failure) that expands to different cases:
|
|
|
|
ABORT/ABORT:
|
|
handler_exit_success:
|
|
handler_exit_failure:
|
|
return;
|
|
|
|
ABORT/RETCODE:
|
|
handler_exit_success:
|
|
return;
|
|
handler_exit_failure:
|
|
RAISE_FATAL_ERROR
|
|
|
|
RETCODE/ABORT
|
|
handler_exit_success:
|
|
return SUCESS;
|
|
handler_exit_failure:
|
|
cleanup_code
|
|
return FAILURE;
|
|
|
|
RETCODE/RETCODE
|
|
handler_exit_success:
|
|
return SUCESS;
|
|
handler_exit_failure:
|
|
cleanup_code
|
|
return FAILURE;
|
|
|
|
If really needed, the macro can be avoided and code can be hand written.
|
|
|
|
** Open points:
|
|
|
|
- How to handle warnings on unused labels?
|
|
- What about M_LET / M_EACH? Maybe only supports those.
|
|
|
|
* TODO #24 : New MIN-MAX-HEAP container :ENHANCEMENT:
|
|
|
|
** State
|
|
See https://en.wikipedia.org/wiki/Min-max_heap
|
|
as DPRIORITY_QUEUE_DEF ?
|
|
|
|
** Analysis
|
|
NOTE: Needs for such container?
|
|
On hold until a user needs it.
|
|
|
|
* DONE #23 : Strict MOVE semantic to clarify :ENHANCEMENT:
|
|
|
|
** State
|
|
|
|
Some type may need to have a force MOVE semantic (for example, they can store
|
|
pointer to themselves). Currently the INIT_MOVE & MOVE operators are more
|
|
a help for performance than a strict semantic usage.
|
|
|
|
** ARRAY container constraint
|
|
|
|
The ARRAY container doesn't support strict MOVE semantic for example.
|
|
It is not a simple matter as it performs a realloc of the table, thus
|
|
moving the data before they can be moved using MOVE. Two solutions:
|
|
|
|
- New operator MOVE_SELF to fix a type after it has been moved.
|
|
- If MOVE defined, force another table and then copy by hand the type. This will be slower and consumme more memory.
|
|
|
|
** MOVE in OPLIST
|
|
|
|
Proposal: do not export INIT_MOVE / MOVE operator in OPLIST if the MOVE operator is
|
|
compatible with a pure COPY semantic. An exported MOVE operator will tell
|
|
other containers than the type shall be carefully moved using the provided
|
|
MOVE operator.
|
|
|
|
For example for tuple, it shall
|
|
|
|
- create an init_move operator if no one has disabled INIT_MOVE,
|
|
- export the init_move operator if at least one has exported a INIT_MOVE and no one has disabled INIT_MOVE.
|
|
|
|
** DO_INIT_MOVE operator
|
|
|
|
DO_INIT_MOVE macro is also fully working for structure
|
|
defined with [1] tricks but without an explicit INIT_MOVE / MOVE
|
|
operators as it uses MOVE_DEFAULT which is not compatible.
|
|
==> Analyse limitation and possible constraint usages.
|
|
|
|
Being able to define a correct default for INIT_MOVE will be really good
|
|
(compatible with trivial move copy).
|
|
In such case, all INIT_MOVE & MOVE operators can be removed from oplist
|
|
to only use the default, and theses operators can be disabled or defined
|
|
only when really needed in the oplist.
|
|
However creating a default INIT_MOVE macro seems problematic.
|
|
If the type is typedef T t[1] then passing such an argument to a function,
|
|
will transform the argument to T*, and the type of the argument doesn't match
|
|
what is expecting resulting in a move of the pointers, not a move of the design data.
|
|
|
|
Defining this type seems possible with C11 _Generic and a TYPE in the oplist,
|
|
but without C11 _Generic I don't see any way to define such macro
|
|
and we still need to target C99 for such basic feature.
|
|
|
|
Without a way to write such a macro, the ticket seems pretty much a dead end.
|
|
|
|
* TODO #20 : New: Bucket priority queue :ENHANCEMENT:
|
|
** State
|
|
Add a new kind of priority queue.
|
|
See https://en.wikipedia.org/wiki/Bucket_queue
|
|
|
|
Check if it will be better as intrusive or non-intrusive container.
|
|
|
|
To test if a bucket is empty or not, a bitfield can be used to check if
|
|
the bit associated to the bucket is set or not. To get the highest bucket
|
|
non empty, we can perform a CLZ of the bitset, which shall be much faster than
|
|
performing a linear search of the buckets (algorithm complexity is the same,
|
|
except that we can scan 64 entries at a time).
|
|
|
|
Check if we can use BITSET, or introduce fixed size BITSET or use ad-hock
|
|
implementation.
|
|
|
|
** Analysis
|
|
NOTE: Needs for such container?
|
|
On hold until a user needs it.
|
|
|
|
* CANC #19 : New: Intrusive Red Black Tree :ENHANCEMENT:
|
|
** State
|
|
Add intrusive red black tree.
|
|
Look also for AVL tree (NOTE: Is there a performance difference between the two?)
|
|
|
|
** Analysis
|
|
Only needed for unmovable objects for which B+Tree cannot do the job.
|
|
But standard Read/Black Tree will do the job just fine.
|
|
There is really no need for it.
|
|
==> Cancelled
|
|
|
|
* TODO #18 : Missing methods :ENHANCEMENT:
|
|
** State
|
|
Some containers don't have all the methods they should.
|
|
See the cells in yellow here:
|
|
http://htmlpreview.github.io/?https://github.com/P-p-H-d/mlib/blob/master/doc/Container.html
|
|
|
|
** Analysis
|
|
Analyzed each missing methods and fill in the gap
|
|
|
|
* TODO #17 : New: Ressource handler :ENHANCEMENT:
|
|
|
|
** State
|
|
A global 'ressouce handler' which shall associated a unique handle to a ressource.
|
|
The handle shall be unique for the ressource and shall not be reused.
|
|
It is typically a 64 bits integers always incremented (even if the program
|
|
creates one billion ressources per second, the counter won't overflow
|
|
until 585 years).
|
|
|
|
The ressource handler shall make an association between a HANDLE 64 bits and:
|
|
|
|
- how much real owners claim to own the ressource
|
|
(the ressource is only owned by the ressource handler, however
|
|
it acts as a delegate of the real owner),
|
|
- how much users keep a pointer to the ressource.
|
|
- pointer to the resource itself.
|
|
|
|
This may be a better alternative than shared_ptr & weak_ptr:
|
|
|
|
- reduce fragmentation,
|
|
- no cycle dependencies,
|
|
- shared_ref & weak_ref becomes only HANDLE,
|
|
- all ressources can be freed in one pass.
|
|
|
|
Needs lock free dictionnary or at least concurrent dictionnary.
|
|
|
|
How to handle multiple resource ?
|
|
|
|
** Analysis
|
|
- use of variant: Pro : easy. Con: Memory usage can be (much) higher than needed if there is a lot of dissimilarity between the size of the objects.
|
|
- embedded the type in the ressource handler: Con: more work, API more complex. Pro: Memory usage seems better.
|
|
|
|
* TODO #16 : New: Lock Free List :ENHANCEMENT:
|
|
|
|
Implement a lock free list. Most of the difficulty is the memory reclamation part.
|
|
Typically this lock free list shall be compatible with RCU method.
|
|
|
|
** First step: backoff methods :DONE:
|
|
** Second step: lock free node pool : :DONE:
|
|
Done as m-c-mempool header.
|
|
|
|
** Third step: Implement generic lock free list on top of it.
|
|
|
|
The ABA problem is already taken into account by the memory alloctor
|
|
provided that the lock free list doesn't try to be smart.
|
|
|
|
backoff has be used when using CAS.
|
|
|
|
Concurrent insertion / insertion and insertion / deletion and deletion / deletion shall be crefully analyzed when taken into account.
|
|
|
|
Questions:
|
|
- singly or doubly or dual push?
|
|
- needs to be logically deleted : needs a previous field
|
|
(NULL if not logically deleted) ? TBC
|
|
|
|
NOTE: m-c-mempool doesn't seem to be fully robust. random failure of the test cases appear (more notably with Visual C++, but it is still quite rare).
|
|
|
|
* DONE #14 : Memory allocation enhancement :ENHANCEMENT:
|
|
|
|
Enhancement of the memory allocation scheme to find way to deal properly with advanced allocators:
|
|
|
|
- non-default alignment requirements for types,
|
|
- instance-based allocator (may need instance based variable access),
|
|
- expected life of created type (temporary or permanent),
|
|
- stack based allocator,
|
|
- global variable access for allocator,
|
|
- maximum allocation before failure.
|
|
|
|
Most of theses are already more or less supported. Examples shall be created to show how to deal with this:
|
|
|
|
- alignement shall be implemented with the attributes of <stdalign.h>
|
|
|
|
However I sill don't know how to implement "instance-based allocator" which is what is missing.
|
|
The problem is how to give to methods context local information store within the container itself.
|
|
|
|
Update:
|
|
|
|
API transformation support enables "instance-based allocator" to be made easily.
|
|
Needs some formal operator in the oplist to support it fully and an example.
|
|
|
|
Can be supported using another API extension, some more operators and forcing some names:
|
|
|
|
* API_N: call like FUNC(obj->extra_data, type)
|
|
|
|
'obj' is a forced named corresponding to an alias to an object in the function.
|
|
Operator needed:
|
|
|
|
- EXTRA_DATA: Add an extra-data field wihtin the container. Defines the type of data.
|
|
|
|
It is a kind of object inheritance where the container inherits some extra data from its base.
|
|
|
|
Duplicate with #32 which is more generic ==> Closed
|
|
|
|
* TODO #12 : New: Atomic shared pointer :ENHANCEMENT:
|
|
** State
|
|
Add an extension to the SHARED_PTR API:
|
|
|
|
- New type atomic_shared_ptr
|
|
- name_init_atomic_set (&atomic_shared_ptr, shared_ptr);
|
|
- name_init_set_atomic (shared_ptr, &atomic_shared_ptr);
|
|
- name_init_atomic_set_atomic (&atomic_shared_ptr, &atomic_shared_ptr);
|
|
- name_atomic_set (&atomic_shared_ptr, shared_ptr);
|
|
- name_set_atomic (shared_ptr, &atomic_shared_ptr);
|
|
- name_atomic_set_atomic (&atomic_shared_ptr, &atomic_shared_ptr);
|
|
- name_atomic_clear
|
|
|
|
No _ref or direct _init: we need to init first a normal shared_ptr then the atomic (TBC)
|
|
|
|
** _atomic_set method:
|
|
|
|
It can be implemented by incrementing the non atomic shared pointer reference,
|
|
then performs a compare_and_swap to the data of the atomic shared pointer,
|
|
finally decrement and dec/free the swapped previous data of the atomic like a normal shared pointer.
|
|
All 3 steps are safe.
|
|
|
|
** _set_atomic method:
|
|
|
|
It needs to perform the following atomic operation : <read the pointer, deref pointer and increment the pointed value> I don't known how to do it properly.
|
|
|
|
See http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4162.pdf
|
|
|
|
Proposition for _set_atomic we store temporary NULL to the atomic_ptr struct to request an exclusive access to the data (this looks like a lock and other operations need to handle NULL) :
|
|
|
|
#+BEGIN_SRC C
|
|
void shared_ptr_set_atomic(ptr a, atomic_ptr *ptr)
|
|
{
|
|
// Get exclusive access to the data
|
|
p = atomic_load(ptr);
|
|
do {
|
|
if (p == NULL) {
|
|
// TODO: exponential backoff
|
|
p = atomic_load(ptr);
|
|
continue;
|
|
}
|
|
} while (!atomic_compare_exchange_weak(ptr, &p, NULL));
|
|
// p has exclusive access to the pointer
|
|
p->data->cpt ++;
|
|
a->data = p->data;
|
|
atomic_exchange (ptr, p);
|
|
}
|
|
#+END_SRC
|
|
|
|
This prevents using NULL which obliges atomic shared pointer to point to a created object...
|
|
|
|
Other alternative solution is to use the bit 0 to mark the pointer as being updated, preventing other from using it (TBC only clear):
|
|
|
|
#+BEGIN_SRC C
|
|
void shared_ptr_set_atomic(ptr a, atomic_ptr *ptr)
|
|
{
|
|
// Get exclusive access to the data
|
|
p = atomic_load(ptr);
|
|
do {
|
|
if ( (p&1) != 0) {
|
|
// TODO: exponential backoff
|
|
p = atomic_load(ptr);
|
|
continue;
|
|
}
|
|
} while (!atomic_compare_exchange_weak(ptr, &p, p|1));
|
|
// Exclusive access (kind of lock).
|
|
p->data->cpt ++;
|
|
a->data = p->data;
|
|
atomic_set (ptr, p);
|
|
}
|
|
#+END_SRC
|
|
|
|
Other implementation seems to have it hard to be lock-free: cf. https://github.com/llvm-mirror/libcxx/commit/5fec82dc0db3623546038e4a86baa44f749e554f
|
|
|
|
* TODO #5 : New: Concurrent dictionary Container :ENHANCEMENT:
|
|
** State
|
|
Implement a more efficient dictionary than lock + std dictionary for all operations when dealing with threads.
|
|
See https://msdn.microsoft.com/en-us/library/dd287191(v=vs.110).aspx
|
|
|
|
The best parallel algorithm is still when there is as few synchronization as possible. A concurrent dictionary will fail at this and will result in average performance at best.
|
|
The typical best case will be in RCU context (a lot of readers, few writers), so the interface shall be compatible with such structure.
|
|
|
|
** Multiple locks within the dictionnary
|
|
|
|
A potential implementation may be to request at initialization time the number of concurrent thread N.
|
|
Create a static array of N dictionnary with N mutex. Then to access the data will perform :
|
|
|
|
- compute hash of object,
|
|
- access high bits of hash and select which dictionnary shall have the data,
|
|
- lock it,
|
|
- perform classic access to the data (check if the compiler can properly optimize the hash computation),
|
|
- unlock it.
|
|
|
|
The property of the hash shall allow a good dispersion of the data across multiple locks, reducing the constraints on the lock. This implementation could be build easily upon the already existent dictionary.
|
|
|
|
To test.
|
|
|
|
See also https://github.com/simonhf/sharedhashfile
|
|
|
|
** Lock Free dictionnary
|
|
|
|
Evaluate also lock-free dictionary (easier with open addressing).
|
|
It needs a complete rewrite of the inner loop through. The hard part is the dynamic resizing of the internal array.
|
|
Ssee http://preshing.com/20160222/a-resizable-concurrent-map/ for a potential solution
|
|
and http://www.cs.toronto.edu/~tomhart/papers/tomhart_thesis.pdf for memory reclamation techniques).
|
|
and https://www.research.ibm.com/people/m/michael/spaa-2002.pdf
|
|
https://www.kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.2017.11.22a.pdf presents different techniques used by linux kernel.
|
|
It needs before lock-free list: http://www.cse.yorku.ca/~ruppert/papers/lfll.pdf http://www.cse.yorku.ca/~ruppert/Mikhail.pdf
|
|
|
|
A good way maybe Open Addessing table used only for indirection and a freelist memory reclamation container for handle the entries
|
|
(like a transaction).
|
|
|
|
Solution?
|
|
--------
|
|
|
|
Use of the concurrent pool (m-c-mempool) to allocate items.
|
|
The big table will only store pointers (+ part of hash in unused bits?) to such allocate items.
|
|
Atomic update is done by allocating a new node, update it, and atomicaly store it in the dict, putting the old one as logically deleted and to reclaim later.
|
|
(Other threads may still read its data so we cannot free immediatly).
|
|
|
|
* DONE #22: Enhanced services for SPSC Queue :ENHANCEMENT:
|
|
|
|
Add services:
|
|
|
|
** DONE _push_bulk
|
|
Test the capacity of the queue and push as much as possible in the queue
|
|
with one check of the atomic structure.
|
|
|
|
** DONE _pop_bulk
|
|
Test the capacity of the queue and push as much as possible in the queue
|
|
with one check of the atomic structure.
|
|
|
|
** DONE _push_force:
|
|
|
|
If the capacity of the queue is full, pop one element and push it:
|
|
push always succeed and the queue always keep the youngest element.
|
|
|
|
* DONE #21: Generic Binary serialization :ENHANCEMENT:
|
|
|
|
Based on issue #26 of https://github.com/P-p-H-d/mlib/issues/26
|
|
|
|
Some kind of "binary serialization" on the model of get_str/parse_str
|
|
could be possible. It would be a great feature from the application
|
|
point-of-view: binary representation is more bandwidth-efficient if
|
|
used on network communications.
|
|
|
|
It will be good to have import/export methods to the
|
|
XML/JSON/MSGPACK/PROTOBUF/BINARY format.
|
|
However, adding all of them on by one in the M*LIB containers
|
|
doesn't seem satisfactory.
|
|
|
|
Instead, adding a generic interface for the serialization of data
|
|
that may be customized by the user to perform the import/export of
|
|
objects in whatever format they want into what they want (FILE/memory/...).
|
|
To simplify it, this interface could only support one kind of import/export
|
|
per compilation unit.
|
|
|
|
* DONE #15: Prologue / Epilogue for Constructor / Destructor for error handling :ENHANCEMENT:
|
|
|
|
Constructor (and destructor) need to use user-defined prologue / epilogue.
|
|
This is in order to register the constructed object into a proper Exception
|
|
Handling Stack so that throwing exceptions may work reliably.
|
|
|
|
Proposal:
|
|
|
|
- M_CONSTRUCTOR_PROLOGUE(object, oplist);
|
|
- M_CONSTRUCTOR_EPILOGUE(object, oplist);
|
|
- M_DESTRUCTOR_PROLOGUE(object, oplist);
|
|
- M_DESTRUCTOR_EPILOGUE(object, oplist);
|
|
|
|
Object creation will need to add all sub-objects into the stack,
|
|
then unstack all to push instead the root object (which recursively remove them).
|
|
|
|
See also http://freetype.sourceforge.net/david/reliable-c.html#cseh
|
|
|
|
- How to handle like allocation of the object?
|
|
- How to avoid calling the destructor multiple times? (It is needed?)
|
|
- How the code can be factorized with RETCODE needs?
|
|
|
|
|
|
|