|
Boost : |
Subject: Re: [boost] [review] The review of Boost.DoubleEnded starts today: September 21 - September 30
From: Ion Gaztañaga (igaztanaga_at_[hidden])
Date: 2017-10-03 23:30:33
On 21/09/2017 19:38, Thorsten Ottosen via Boost wrote:
> Dear users and members of Boost,
>
> I'm proud to announce that the formal review of Benedek Thaler's
> Boost.DoubleEnded library starts today, September 21, and runs through
> September 30. This library is the result of Benedek's participation in
> Google Summer of Code 2015.
Hi, this is my review of Boost.DoubleEnded.
-------------------------------------
-------------------------------------
What is your evaluation of the design?
-------------------------------------
-------------------------------------
The library presents two containers, devector and batch_deque. I find
the first one as a relatively simple but interesting vector extension
supporting front insertions. In the future it could maybe take advantage
of the backwards memory expansion (taking the previously adjacent free
memory segment so that new elements could be added to front without
moving the rest of the elements) supported by Boost.Interprocess and the
Boost.Containerâs modified DLMalloc.
----------
devector
--------
1) From a design point of view, I would prefer not to include the small
buffer optimization with the data structure, although reviewing code, it
seems many parts are to be optimized. It's like just having small_vector
without having vector, most libraries have two classes for these two use
cases. Different classes also ease that a type erasured base class can
be defined independent from the static storage size (Boost.Container
calls it small_vector_base, LLVM calls it SmallVectorBase) I think we
should have a devector, and add small_devector and static_devector
containers if there is demand.
2) Defaulting size_type to unsigned int is arbitrary and against
existing practice. According to the usual STL rules, size_type shall
come from the allocator, which knows what type can represent the number
of elements that can be allocated. I find interesting the idea of
expressing a different size_type to save space, but I am not sure if
that should come from the allocator or from an option.
3) reserve_only_tag: I would rename it as âreserve_only_tâ (similarly to
std::allocator_arg_t and Boost.Containerâs âdefault_init_tâ). The front
and back capacity version should be enough as I think devector should
not show preference for back insertion (if a user wants only a back
capacity, then it would use a vector and not a devector). Strictly
speaking, capacity constructors are a bit redundant, but the same could
be said about default constructing constructors:
//it could be implemented as default constructor + resize
explicit vector(size_type n, const Allocator & allocator = Allocator());
A reserve-only constructor plus an âunsafeâ emplacement could make a
container similar to an array, where no redundant checks are done when
filling the array. But I donât expect noticeable performance gains with
this reserve-only constructor.
4) unsafe_uninitialized_tag: I donât like these non-safe constructors,
which are the cases?. With reserve-only constructors + unsafe emplace
back a user can achieve nearly the same performance. For POD types (like
the socket example mentioned in the documentation), I would suggest
something like Boost.Containerâs âdefault_init_tâ. Instead of
âunsafe_uninitialized_resize_frontâ I suggest resize_front(size_type,
default_init_t)
5) reserve() / resize(): I think they have no sensible meaning in the
container (capacity() could be useful, depending on the reallocation
strategy). We should not treat back insertion as the âdefaultâ
insertion. IMHO the API of devector should be symmetrical.
6) unsafe_push_back/front: I think they can be useful, many times we
iterate until a known upper bound and capacity checks might be redundant
in some code, just like we do with arrays. I suggest replacing âunsafeâ
with âuncheckedâ and âpushâ with âemplaceâ (which is much more general):
âunchecked_emplace_back/front(Args&&â¦.)â.
7) growth_policy: This is an option that could be interesting not only
for devector but also for vector. However, I would prefer passing
options as a single argument where options are combined, so that the
class declaration is not modified each time a new option is added.
Something like:
template<class T, class Allocator, class Options= void>
class devector;
using myoptions = make_options_t<growth_factor<G>, size_type<unsigned
short>, new_option<arg>, ⦠>;
usinge mydevector = devector<int, allocator<int>, myoptions>;
a similar approach is used in Boost.Intrusive and will be added to
Boost.Container.
8) Regarding "move_if_noexcept", Boost.Container does not use it as it
consider a pessimized design for a situation (throwing moves) that will
occur nearly never and few times will be handled using the provided
strong guarantees. On the other hand, most people will notice the big
slowdown that will receive because their move constructors cannot be
marked as noexcept (pimpl idiom, small buffer optimization,
sentinel-node based lists, devector itself would be copied if
small_buffer_size is used, etc.). I think âmove_if_noexceptâ was just
added for backwards compatibility with C++03âs strong guarantee, but
devector does not need to follow the same rule. I understand many
(most?) people donât agree here, but donât think we should sacrifice
devector performance just to support rare throwing cases.
9) Minor comment 2: IMHO I think iterators would be better as classes
instead of pointers, a user can always use data() to make sure pointers
are returned. Many times unwanted overload resolutions can happen if
pointers are used, so I prefer the type safety offered by a class-based
iterator. It can also offer checked iterators in the future.
10) Default growth factor of 4. Sounds like overkill, 2 or 1.5 sounds
more reasonable and in line with user expectations based on existing
practice in STL and Boost. Iâm not sure if shrinking should be part of
the trait.
----------
Batch_deque
--------
Many of the comments from devector are applicable here, so I will not
repeat them here.
1) In general, I donât see the need of batch_deque as a different
container, because itâs basically a deque with some options (segment
size and additional operations). Those could be added to
boost::container::deque without breaking anythying.
2) Segment_iterators: Raw pointers are stored in segment iterators. That
means that those could not be placed in the memory that the allocator
allocates, if the allocator does not defined ::pointer as T*.
Also, it seems strange that there is not segment_type/segment_info
class. The iterator itself has members that return the address and the
size of a segment. I think thatâs unexpected, the expected segment
iterator would return a reference to a class (e.g. segment_type) that
represents the segment, which has the required operations, and also
internal begin()/end() operations to obtain segment-local iterators (a
la unordered containers). This could allow some optimized segment-based
algorithms (see lafstern.org/matt/segmented.pdf, for more information)
3) stable_insert: I canât see the usefulness of this operation. A
batch_deque is a sequence, but the user cares about the insertion point
but does not care if additional elements are created somewhere? I fail
to see a real use case.
-------------------------------------
-------------------------------------
What is your evaluation of the implementation?
-------------------------------------
-------------------------------------
I think itâs solid. Tests look good.
--------------------------
devector
--------------------------
emplace_slow_path: The forced temporary creation should be avoided, it
is overkill for containers of containers, devector<static_vector> or
containers with sentinel nodes. IMHO supporting references to already
included elements is a bad feature of std::vector (a corner case
supported in single element insertion, ignored for ranges, etc.)
Boost.Container chose not to support it and no one has demanded that
feature:
http://www.boost.org/doc/libs/1_65_1/doc/html/container/Cpp11_conformance.html#container.Cpp11_conformance.container_const_reference_parameters
Reallocation strategy: First, I considered this strategyunnatural, but
now I am not sure. For a push_front, if no free front capacity is found,
but there is free back capacity, then all data is shifted to insert the
element. If interleaved front and back insertions are performed, I donât
know if this will lead to too many data movements. When memory is
reallocated to make room for a front insertions, the free back capacity
is unchanged and all new free capacity goes to front. This means that
the last operation (front or back) determines where all the new capacity
goes. I find a bit strange that N == front_back_capacity() does not mean
that a reallocation will happen if we push_back N times, as repeated
data movement will happen if front_free_capacity() is available.
Another strategy is to treat both ends independently: if front insertion
has no free space, it could always reallocate and make room at front, so
further push_fronts donât provoke data movement. This would be similar
to batch_deque, where memory is allocated if there is no free front
capacityregardless of the free back capacity (no data movement),f and
free segment is recycled from back capacity to insert elements at front.
Obviously, this approach consumes more memory. Yet another strategy
would be distribute any new free capacity (after reallocation) between
front and back, indepently from which operation (back or front) has
provoked the allocation.
emplace_slow_path:
// construct at back + 1 from back (no guard)
alloc_construct(end(), std::move(back()));
// move back half right
std::move_backward(position, end() - 1, end());
++_back_index;
This does not seem exception safe â++_back-indexâ should be done just
after alloc_construct, same for front insertion.
insert_range_slow_path_near_front/back: I see that construction +
rotation is done, but I think this is not optimal, the implementation
might be simplier, but more than necessary data movement is done.
Range insert: A temporary temp_devector is used, which means copying
data two times. The inline storage for 32 elements can blow the stack if
T is something big (like static_vector<T, N>)
--------------------------
batch_deque
--------------------------
1) I like that deque_[const_]iterator is lightweight, I donât know if
iteration/access itâs more efficient than more traditional approaches.
boost::container::deque iterators are really fat and should be improved
to something similar to batch_deque iterator. On the other hand
deque_segment_[const_]iterators are heavier than needed, and I think
they could be just like vector iterators if used to iterate a single
segment. The problem is that deque_segment_iterator works both as
segment information class and as a local iterator. I think a similar
approach to PolyCollectionâs âsegment_traversal_infoâ as segment
information could work better, as it allows really lightweight local
iterators that can speed up specialized segmented algorithms (local
iterators could be just vector-like iterators inside a segment).
2) The data structure o batch_deque is slightly bigger than the
traditional one, just asking if itâs intended or it was developed like
this to reuse code. Traditional deque (from SGI) used four elements (map
pointer, map size, iterator to begin, iterator to end). Your
implementation defines the map as a devector, which stores size and
capacity for the map, but this allows you handle the excess capacity of
the map, Iâm not sure if it adds performance benefits.
3) I havenât thoroughly reviewed all the code, but many devector
comments are applicable: rotation, move_if_noexcept, etc.
-------------------------------------
-------------------------------------
What is your evaluation of the documentation?
-------------------------------------
-------------------------------------
Documentation is correct, but I expected more detail. If one of the
goals of the library is efficiency, benchmarks should be there (maybe
against Boost and Std vector and deque).
-------------------------------------
-------------------------------------
What is your evaluation of the potential usefulness of the library?
-------------------------------------
-------------------------------------
devector might be useful, I have doubts with batch_deque. Deque in
general is not the most used STL container, so I donât expect a lot of
users, maybe for specialized cases.
-------------------------------------
-------------------------------------
Did you try to use the library? With which compiler(s)? Did you have any
problems?
-------------------------------------
-------------------------------------
Just executed tests and examples and debugged to help code review. MSVC
2017 triggers quite a lot of warnings, mainly about constant conditional
expressions.
-------------------------------------
-------------------------------------
How much effort did you put into your evaluation? A glance? A quick
reading? In-depth study?
-------------------------------------
-------------------------------------
Several hours analyzing the design and reading the code. An hour playing
with the library with MSVC.
-------------------------------------
-------------------------------------
Are you knowledgeable about the problem domain?
-------------------------------------
-------------------------------------
I do think so. Iâve written a lot of containers for Boost and outside
Boost. Deque is not my favorite container, though ;-)
-------------------------------------
-------------------------------------
¿Should we accept the library?
-------------------------------------
-------------------------------------
I think the library (see my design comments) and the documentation still
need improvements, but it's not the main issue. Iâm reluctant to accept
the library because I donât see substantial benefits of batch_deque
against deque (configurable segment size could be added to
Boost.Container if that feature is considered useful), and I donât know
if the use cases where devector is supposed to shine needs a whole new
library (we have no clear rules about how tiny a library can be, but in
this case it would be a new container and reimplementation of another).
JoaquÃn has proposed to merge this proposal to Boost.Container, but
supported compilers and goals are quite different and a direct merge
would be problematic as Boost.Container would loose coherency, common
goals and minimum requirements between all containers.
A middle solutions is to:
1) Start âportingâ devector, after all issues have been agreed, (without
small vector optimization, at least in the first version) to
Boost.Containerâs requirements (including some pre C++11 compilers)
2) Improve Boost.Containerâs deque to support features (and maybe code)
from batch_deque (configurable segment size, local iterators, etc.).
Thatâs a lot of work, discards a lot of batch_queue code and I donât
know if Benedek would agree to help on this task after I spoke against
this option when it was proposed and after doing so much work to write
the library.
Best,
Ion
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk