devector feedback request
Dear Boost, I'm happy to announce the new devector container developed as part of the GSoC programme reached a presentable state. I invite you to take a look and ask for feedback. Main features: - every std::vector feature - front operations with amortized linear complexity - optional small buffer optimization - customizable growth policy (see the docs) - unsafe methods for the brave and performance addicts Documentation: - Intro: http://erenon.hu/container/container/non_standard_containers.html#container.... - Usage: http://erenon.hu/container/container/examples.html#container.examples.devect... - API: http://erenon.hu/container/boost/container/devector.html#idp86138944 Code: - Implementation: https://github.com/erenon/container/blob/devector/include/boost/container/de... - Tests: https://github.com/erenon/container/blob/devector/test/devector_test.cpp Thanks, Benedek
Benedek Thaler wrote:
Dear Boost,
I'm happy to announce the new devector container developed as part of the GSoC programme reached a presentable state. I invite you to take a look and ask for feedback.
Hi Benedek, Some feedback: 1. For a given type T whose constructor can throw, and you have a loop constructing n objects of type T via std::allocator_traits<T>::construct - should you not be handling that potential construction failure? (i.e. Destroying those elements that were constructed before the constructor that throws, before re-throwing) e.g. Turning something like: for (i = 0; i < n; ++i) { // construct i } Into: try { for (i = 0; i < n; ++i) { // construct i } } catch (...) { for (; i > 0; i--) { // destroy i-1 } throw; } (My apologies if you already do this, and I missed it). 2. size_type being unsigned int instead of std::size_t - I understand the motivation, but could this not be customized - e.g. via another policy? Or should it not instead be based on std::allocator_traits<Allocator>::size_type instead? 3. I understand deriving from Allocator for EBO, but should that type be std::allocator_traits<Allocator>::rebind_alloc<T> instead? (And have the 'allocator_type' member typedef also be this rebound type) . Best, Glen
On Sun, Jul 26, 2015 at 5:54 PM, Glen Fernandes
Some feedback:
Thanks for the feedback!
1. For a given type T whose constructor can throw, and you have a loop constructing n objects of type T via std::allocator_traits<T>::construct - should you not be handling that potential construction failure? (i.e. Destroying those elements that were constructed before the constructor that throws, before re-throwing)
[snip]
(My apologies if you already do this, and I missed it).
There are two such constructors: explicit devector(size_type n, const Allocator& allocator = Allocator()) devector(size_type n, const T& value, const Allocator& allocator = Allocator()) The first one has a `construction_guard` which takes care cleanup on failure. The second one calls `opt_copy` at the end, which also has the same kind of guard if needed. The failure scenario for both constructor (and for other metods) is tested: https://github.com/erenon/container/blob/devector/test/devector_test.cpp#L22... https://github.com/erenon/container/blob/devector/test/devector_test.cpp#L26... Did I missed something?
2. size_type being unsigned int instead of std::size_t - I understand the motivation, but could this not be customized - e.g. via another policy? Or should it not instead be based on std::allocator_traits<Allocator>::size_type instead?
We could provide a way. However, using allocator_traits is not the best option, I guess, since std::allocator::size_type is size_t, and for most users unsigned would be enough. GrowthPolicy could be used, however.
3. I understand deriving from Allocator for EBO, but should that type be std::allocator_traits<Allocator>::rebind_alloc<T> instead? (And have the 'allocator_type' member typedef also be this rebound type) .
What would this buy us? The user is expected to provide an allocator for T. As far as I know, the usecase of rebind is to allocate other types with the provided allocator, e.g: nodes, in node based containers. I'm not against the idea, I just don't know how is it better. Thanks, Benedek
Benedek Thaler wrote:
The first one has a `construction_guard` which takes care cleanup on failure. The second one calls `opt_copy` at the end, which also has the same kind of guard if needed.
Ah, I see. Looks good!
We could provide a way. However, using allocator_traits is not the best option, I guess, since std::allocator::size_type is size_t, and for most users unsigned would be enough. GrowthPolicy could be used, however.
It's up to you. It may be more intuitive having a separate policy, instead of turning GrowthPolicy into GrowthAndSizePolicy.
What would this buy us? The user is expected to provide an allocator for T. As far as I know, the usecase of rebind is to allocate other types with the provided allocator, e.g: nodes, in node based containers.
I'm not against the idea, I just don't know how is it better.
It only enables someone to supply an allocator type without having to
supply the rebound allocator type themselves.
e.g. The following code:
template<class A>
struct T {
container1 c1;
container2
On Sun, Jul 26, 2015 at 11:13 PM, Glen Fernandes
It's up to you. It may be more intuitive having a separate policy, instead of turning GrowthPolicy into GrowthAndSizePolicy.
I'd like to avoid having too many policies. One could argue, that the chosen size type limits the maximum growth, thus it fits into the concept of the GrowthPolicy. I'll think on it.
It only enables someone to supply an allocator type without having to supply the rebound allocator type themselves.
e.g. The following code: template<class A> struct T { container1 c1; container2
c2; container3 c3; };
In this case, one should write: template <class A> struct T { template <class Y> using AY = typename std::allocator_traits<A>::rebind_alloc<Y>; container1 c1; // etc. }
.... looks cleaner than: template<class A> struct T { container1 c1; container2
c2; container3 c3; }; I know that some standard library vendors do this already: e.g. Using Dinkumware with Intel C++, the following will compile { std::vector
v; v.push_back(1); } Others do not: e.g. Using libc++ with Clang there is a static_assert "Allocator::value_type must be the same as value_type". It's up to you. Personally I think the former behavior is better, and if it isn't tolerable by the standard, it should be.
I think this version obscures intent and behaviour, and encourages sloppy use. It looks like the container uses allocator<X>, but really, it uses allocator<Y>, which is not nice. Or maybe it's just me being too strict. Thanks, Benedek
Benedek Thaler wrote:
I'd like to avoid having too many policies. One could argue, that the chosen size type limits the maximum growth, thus it fits into the concept of the GrowthPolicy. I'll think on it.
Sounds good.
It looks like the container uses allocator<X>, but really, it uses allocator<Y>, which is not nice. Or maybe it's just me being too strict.
I'm also undecided:
- On the one hand, requiring that explicit specification of allocator
type whose value_type matches the desired value_type is nice.
- On the other, it's a little repetitive: we already specify
value_type in the parameter list, and every allocator type provides a
mechanism to rebind for a given value_type. It may also feel a little
inconsistent: e.g. with Clang/libc++ the above example with
std::vector static_asserts, yet { std::list l; l.push_back(1); } will compile fine. Unfortunately, I have no benchmarks to prove it, yet. Benchmarks comparing against std::vector would be nice to have
(whenever you have time).
Best,
Glen
On Sun, Jul 26, 2015 at 11:53 AM, Benedek Thaler
On Sun, Jul 26, 2015 at 5:54 PM, Glen Fernandes
wrote: Some feedback:
Thanks for the feedback!
1. For a given type T whose constructor can throw, and you have a loop constructing n objects of type T via std::allocator_traits<T>::construct - should you not be handling that potential construction failure? (i.e. Destroying those elements that were constructed before the constructor that throws, before re-throwing)
[snip]
(My apologies if you already do this, and I missed it).
There are two such constructors:
explicit devector(size_type n, const Allocator& allocator = Allocator()) devector(size_type n, const T& value, const Allocator& allocator = Allocator())
The first one has a `construction_guard` which takes care cleanup on failure. The second one calls `opt_copy` at the end, which also has the same kind of guard if needed.
The failure scenario for both constructor (and for other metods) is tested:
https://github.com/erenon/container/blob/devector/test/devector_test.cpp#L22...
https://github.com/erenon/container/blob/devector/test/devector_test.cpp#L26...
Did I missed something?
An alternate approach, if you can require C++11, is to use "delegating constructors". See libc++'s dynarray implementation for an example of this. ( https://android.googlesource.com/platform/ndk/+/5de42e6621b3d0131472c3f8838b..., default constructor at line 138, other constructor at line 231) Basic idea: * Call the default constructor first. That can be noexcept. * Insert the elements, one by one, maintaining the class invariants. * If one of the insertions throws, then the destructor will be called, and it can do the cleanup. -- Marshall
I'm happy to announce the new devector container developed as part of the GSoC programme reached a presentable state. I invite you to take a look and ask for feedback.
From the documentation, it sounds like a devector is strictly superior to std::vector, as it offers a superset of functionality. It would be great if you could discuss a bit more the trade-offs, for example, in which cases a devector is inferior to a std::vector performance-wise.
Matthias
On Sun, Jul 26, 2015 at 9:35 PM, Matthias Vallentin < vallentin@icsi.berkeley.edu> wrote:
From the documentation, it sounds like a devector is strictly superior to std::vector, as it offers a superset of functionality. It would be great if you could discuss a bit more the trade-offs, for example, in which cases a devector is inferior to a std::vector performance-wise.
Thanks for the feedback! The only compromise I know about is using `unsigned` vs. `std::size_t` as `size_type`, described in the docs: " The devector class template intends to be a drop in replacement of std:: vector. However, to keep its size slim (16 bytes on 32 bit, 24 bytes on a typical 64 bit architecture), its size_type is defined as unsigned int which reduces max_size() to UINT_MAX. " Assuming no small buffer optimization is used, devector should have the same performance as std::vector. Unfortunately, I have no benchmarks to prove it, yet. However, even if the current implementation falls behind std::vector, that only means I missed some optimization, it shouldn't be something inherent, to my understanding. Benedek
Hi Matthias,
From the documentation, it sounds like a devector is strictly superior to std::vector, as it offers a superset of functionality. It would be great if you could discuss a bit more the trade-offs, for example, in which cases a devector is inferior to a std::vector performance-wise.
This comparison makes most sense if the two containers are configured in a compatible way. That is, no internal buffer for devector and the same eponential growth of the buffer. In this senario one should expect std::vector to be superior by a very small constant factor for many operations (so there is /never/ any big-Oh advantage of std::vector). For example, to initialize a default std::vector, you need to initialize three data members, whereas the devector needs to initialize four data members. Similarly, devector sometimes needs to adjust two members instead of one. For many operations, I expect there will be very little difference, and as soon as you start using stuff that a devector can do efficiently, then devector code will perform much better. kind regards Thorsten
I'm happy to announce the new devector container developed as part of the GSoC programme reached a presentable state. I invite you to take a look and ask for feedback.
Main features: - every std::vector feature - front operations with amortized linear complexity
Don't we get linear complexity for this from std::vector already? Regards Hartmut --------------- http://boost-spirit.com http://stellar.cct.lsu.edu
- optional small buffer optimization - customizable growth policy (see the docs) - unsafe methods for the brave and performance addicts
Documentation: - Intro: http://erenon.hu/container/container/non_standard_containers.html#containe r.non_standard_containers.devector - Usage: http://erenon.hu/container/container/examples.html#container.examples.deve ctor_usage - API: http://erenon.hu/container/boost/container/devector.html#idp86138944
Code: - Implementation: https://github.com/erenon/container/blob/devector/include/boost/container/ devector.hpp - Tests: https://github.com/erenon/container/blob/devector/test/devector_test.cpp
Thanks, Benedek
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/mailman/listinfo.cgi/boost
On 30-07-2015 15:20, Hartmut Kaiser wrote:
I'm happy to announce the new devector container developed as part of the GSoC programme reached a presentable state. I invite you to take a look and ask for feedback.
Main features: - every std::vector feature - front operations with amortized linear complexity
Don't we get linear complexity for this from std::vector already?
Yes. I assume Benedek meant specifically push_front, and that push_front has amortized constant time complexity for devector, but not for vector. regards -Thorsten
participants (6)
-
Benedek Thaler
-
Glen Fernandes
-
Hartmut Kaiser
-
Marshall Clow
-
Matthias Vallentin
-
Thorsten Ottosen