5 ms·
The C++ STL vector, as is, has some limitations: - As far as I know, it requires two calls to the allocator (one for the instance, and another for the resizabl
by faragon 11y ago
The C++ STL vector, as is, has some limitations:
- As far as I know, it requires two calls to the allocator (one for the instance, and another for the resizable buffer).
- Even if allocated in the stack, unless you use a different allocator, the buffer is allocated in the heap.
If you do lots of operations per second, it makes the memory allocation functions, e.g. malloc(), suffer (depending on allocator algorithmic complexity, it could be a bottleneck).
P.S. an implementation of vector type for C, using dynamic size arrays, with only one allocation per container, using also geometric growing, and supporting both stack and heap allocation: https://github.com/faragon/libsrt/blob/master/src/svector.h https://github.com/faragon/libsrt/blob/master/src/svector.h
- kazinator 11y agoOne allocation per resizable container means that the identity of the container may have to change when it grows. We see that in the API: functions return a new vector, which the caller must capture (and at the same time forget about the vector pointer that was passed in and all copies thereof, to avoid a use-after-free error). That captured vector must be stored somewhere, such as in a stack variable or a member variable of an object. That storage location itself must be allocated. Hence, effectively, two allocations. The memory management is manual: if you leave a scope without freeing all the "sv" vectors (by any means, including longjmp: your poor man's exception handling in C) you have a leak. { std::vector<int> stdvi; // stack for "stdvi" + heap for buffer } // heap object deallocated { sv_t *sv = // stack for "sv" sv_alloc(sizeof int, 42); // heap for buffer } // heap object leaked If a C++ programmer were to use this "sv", he or she would likely wrap it in an inline template class which manages the ownership of the buffer to prevent leaks and perhaps facilitate sharing. And that would just re-create std::vector basically. This template class would add next to zero overhead.
- faragon 11y agoThank you very much for taking a look into the code :-D Regarding identity, you can use double pointers. In the case of willing to eliminate leakage risks (or speed-up the operation, avoiding calling the heap allocator), you can use stack allocation (using sv_alloca instead of sv_alloc), with implicit free, e.g. { sv_t *a = sv_alloca_t(SV_INT32, 42); sv_t *b = sv_alloca_t(SV_INT32, 42); sv_t *c = sv_alloca(sizeof(struct mystruct), 42); struct mystruct { int r, s; } y = { 1, 2 }, z = { 3, 4 }; sv_push_i(&a, 0); sv_push_i(b, 1); sv_push_i(b, 2); sv_push_i(b, 3); sv_cat(&a, b); sv_cat(&a, a, b); sv_push(&c, &y, &z, &y, &z, &y, &z); sv_cat(&c, c); /* implicit free */ } Also, please note that is a C library, so the only "RAII"-like implicit freeing is only possible using stack allocation (or compiler-specific stack cleanup callbacks). In my opinion is not worth it to encapsulate it for C++, as for C++ the STL library is already very good for most cases. With heap allocation (you could mix both): { /* Using the heap, you can start with size 0, * the vector will grow automatically: */ sv_t *a = sv_alloc_t(SV_INT32, 0); sv_t *b = sv_alloc_t(SV_INT32, 0); sv_t *c = sv_alloc(sizeof(struct mystruct), 0); struct mystruct { int r, s; } y = { 1, 2 }, z = { 3, 4 }; sv_push_i(&a, 0); sv_push_i(b, 1); sv_push_i(b, 2); sv_push_i(b, 3); sv_cat(&a, b); sv_cat(&a, a, b); sv_push(&c, &y, &z, &y, &z, &y, &z); sv_cat(&c, c); /* explicit free */ sv_free(&a, &b, &c); }
- faragon 11y ago"That storage location itself must be allocated." Not really, e.g. you can allocate with one allocation both: header + the buffer for the data. When growing, you grow with: size_of_header + elem_size * num_elems (if allocated in the heap). If you use the stack allocation, you must allocate the maximum size you're going to use, e.g. for pre-allocating room for 100 elements you can do: sv_alloca(sizeof(struct mystruct), 100) or sv_alloca_t(SV_INT32, 100) for built-in type (built-in types for signed/unsigned ingegers allow mixing input types, passing them by value). In the case of stack allocation, if reaching the size limit via "push", you got error in return, but the vector is safe with the last successful operation.