-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom allocator context #4
Comments
Hi! Glad you’re enjoying the library :) I haven’t had time to think much about the custom allocator issue yet, although I did bookmark @skeeto’s recent article on the topic. I did, however, realize that a context pointer parameter for the allocation function would presumably require the hash-table struct to internally carry around context pointer that the user sets when her or she initializes it. Is that right? If so, that would be an unacceptable cost to impose on users who do not need this functionality, and this seems to be the problem that STC (@stclib) attempts to solve. One solution, I think, would be to use the preprocessor to have the struct only include such a pointer if the user actually needs it (just as the bucket struct only includes a #define MALLOC_FN <name of a user-provided function with the same signature as malloc>
// OR
#define EXTENDED_MALLOC_FN <name of a user-provided function that includes a context pointer parameter> What do you think of that solution? It seems simpler than STC's approach. In any case, it will be a while before I can introduce this functionality. The priorities are the moment are to finish refactoring and share my hash-table benchmarking project, then integrate Verstable into CC (as a separate implementation, not a replacement for the standalone library), and then publish an article on the benchmarks that also includes most popular C hash-table libraries. So it will probably be a few months before I get to the custom allocators, unfortunately. Edit: Maybe it makes more sense to prioritize addressing allocator question, since that will take less time than those other things I mentioned. |
I think this is similar to what I propose in #5 |
Hi @yrashk and @fonghou. I’m going to make resolving the customer allocator issue my top priority. A few thoughts and questions before I implement anything:
I'll tag @skeeto again, too, just in case he has some sage advice to offer. |
Hi @JacksonAllan, I'd prefer Option 2 because it integrates better with Arena use cases per @skeeto example. Thanks! |
My 2 cent on this - as someone who uses custom allocators a lot - is that a user defined context pointer is a must. One key benefit of custom allocators is the ability to avoid global state. But without a context pointer, it becomes necessary to smuggle the allocator metadata through global state - which is very much undesirable. In that regard, I don't find #5 to be useful. In fact, from a library design standpoint, I find it to be harmful because it leaks unnecessary internal information out through the interface. For example, if you want to merge the two allocations into one in the future then all of a sudden it will require an API breakage.
A context pointer and a function pointer is merely 8+8=16 bytes on 64 bit platform (also see the note below). And this cost will occur per hashtable instance, not per node/item in the hashtable. I'm willing to bet that the items that will be inserted into the table will weight multiple orders of magnitude more than this pesky 16 byte. If you can come up with a way to avoid it, then that's fine. But I can't imagine this being a deal breaker for anyone. Note: If you want, you can avoid taking in the function pointer. A single context pointer is enough, the user can embed the function pointer into the context pointer and dispatch from there. Not super convenient, but not a deal breaker in my eyes. my_alloc(..., void *ctx)
{
MyStruct *a = ctx;
return a->alloc(...); // allocator function pointer embedded in the context pointer
} TL;DR: Proper support for custom allocators requires each hashtable to carry a context pointer at least. |
Thanks for weighing in!
It could make a difference for a user that has many tables that are small or empty. But in any case, the approach I outlined in my earlier comment addresses this issue by making the context pointer an opt-in mechanism. My plan is still the one I outlined in that comment. To use a custom allocator with a context pointer, users will need to define The buckets and metadata arrays will indeed be combined into one allocation. Whether or not |
@JacksonAllan, one thing not clear to me with I personally like the STC's approach as mentioned previously. The key bits are in the source linked and copied below. https://github.com/stclib/STC/blob/master/include/stc/extend.h#L54
Just to demonstrate how that design works, I wrote a custom allocator based on an arena implementation (another header file "mem/arena.h" in my forked STC repo) described in @skeeto's blogs. One difference you may notice, STC defines those custom allocator macros as function call expressions, instead of function names (ie. pointers). One advantage of it is that both struct field and function arg are static typed instead of void*. https://github.com/fonghou/STC/blob/master/include/stc/arena.h Below is an example how it looks like in client code. https://github.com/fonghou/STC/blob/master/misc/examples/mixed/arena.c |
Sorry for the belated response—I've been super busy.
Yes, exactly. With this approach, the hash table struct would look like this: typedef struct
{
size_t key_count;
size_t bucket_count;
uint16_t *metadata;
VT_CAT( NAME, _bucket ) *buckets;
#ifdef EXTENDED_MALLOC_FN
void *allocator_context;
#endif
} NAME; As for the STC approach, initially I had some reservations. But the more I think about it, the more I like it. Advantages:
Disadvantages:
|
@JacksonAllan Hi, I prefer strict conforming C, but I personally don't worry much about this "possible" non-conformance of container_of(). I use it also in the linked list implementation. However, I still think it is a good idea to have the "contained object" as the initial member, so casting can be used instead (actually container_of() will collapse into a regular cast). I'll likely do it with both the list node and the extend wrapped struct. |
@JacksonAllan I have committed an update to STC in the v50dev branch, and have removed the dependency on container_of in the library, and added a clone function for the extended container struct. |
Hi @tylov, I've tested out this change.
I find it makes below code work as expected (and api more ergonomic).
However, both gcc and clang give incompatible pointer type warning like below. Do you recommend this usage? or stick with old way of table.get?
|
I would go with the type-correct version, i.e. the .get doesn't bother me much. You can always make an extra pointer directly to the container if you use it a lot. |
Right, the arguments in favor of |
I've now implemented the extended custom allocator support in the newly created dev branch. Initially, I took the STC wrapper-struct approach. However, that experiment highlighted a few drawbacks of this approach that caused me to abandon it:
Hence, the approach I ultimately followed is a combination of my original idea (in that a member is conditionally added to the hash table struct) and the STC approach (in that the user fully controls the type of that member). It works as follows: To enable and specify the type of the hash table struct’s If If Here's how this system might look in practice: #include <stddef.h>
// Example context data as a struct.
typedef struct
{
// Context data...
} context;
void *malloc_with_ctx( size_t size, context *ctx )
{
// Allocate size bytes, taking into account ctx, and return a pointer to the allocated memory or NULL in the case of
// failure...
}
void free_with_ctx( void *ptr, size_t size, context *ctx )
{
// Free size bytes, taking into account ctx...
}
#define NAME int_int_map
#define KEY_TY int
#define VAL_TY int
#define CTX_TY context
#define MALLOC_FN malloc_with_ctx
#define FREE_FN free_with_ctx
#include "verstable.h"
int main( void )
{
context our_ctx;
// Initialize our_ctx...
int_int_map our_map;
vt_init( &our_map, our_ctx );
// Do things with our_map...
vt_cleanup( &our_map );
} One thing I wasn't sure about was whether Edit: Fixed above example. |
The above changes are now part of the main branch (v2.0.0). I recommend updating to this new version because, besides introducing optimizations, it also fixes a significant bug whereby a key was hashed after its user-supplied destructor was called during erasure. I'll leave this issue open for now in case there's any feedback. |
Just letting you know, in STC branch v50dev, there is a include/hmap-robin.h. Would be great if you could include it in your benchmarks if you have time at some point. It appears to be quite fast (I think insert is slightly slower, but erase and lookup are faster and probably more consistent). I will likely replace it with hmap.h if it works well. |
Will do ASAP. I'll probably post the results in the discussion in order to keep talk of design and benchmarks centralized there. |
Hi, thanks for sharing an awesome library. love the efficient design and clean api.
Regarding the topic of custom allocator context raised by @skeeto here https://old.reddit.com/r/C_Programming/comments/18gnxkd/verstable_a_versatile_highperformance_generic/kd25s9z/
Would you think the approach https://github.com/stclib/STC?tab=readme-ov-file#per-container-instance-customization fit Verstable well? It seems "zero cost" at runtime if not opted in, also less intrusive than what @skeeto's approach.
Regards!
The text was updated successfully, but these errors were encountered: