breese / trial.circular

Circular span
Boost Software License 1.0
8 stars 2 forks source link

API for bulk data operations #1

Open mtnpke opened 4 months ago

mtnpke commented 4 months ago

Hi,

thanks for this nice project! It was really easy to integrate and use and was the only circular buffer API that fulfilled my needs of being able to bulk resize.

Regarding bulk insertion and removal operations, I was wondering if there would be an option to offer some kind of higher-level API. You offer the segment views, but it is a bit clunky to put in user code and requires some knowledge of the internals of the buffer. I haven't found a better way, but of course it might also be that I just missed it.

For example, to insert a block of data, I currently use:

// add `std::span<std::byte> data` to the end of `circular::span<std::byte> q`
auto copyDestFirstSeg{q.first_unused_segment()};
auto copyDestLastSeg{q.last_unused_segment()};
q.expand_back(data.size());
const auto bytesInFirstSeg{std::min(data.size(), copyDestFirstSeg.size())};
const auto bytesInLastSeg{data.size() - bytesInFirstSeg};
std::copy_n(data.begin(), bytesInFirstSeg, copyDestFirstSeg.begin());
if (bytesInLastSeg > 0) {
    std::copy_n(data.begin() + bytesInFirstSeg, bytesInLastSeg, copyDestLastSeg.begin());
}

Could operations like this be added to the buffer or maybe as a free-standing function? I can imagine it is quite common to want to do this with a circular buffer and I guess it is also the reason you provide the segment queries in the first place.

breese commented 4 months ago

Does q.push_back(data.begin(), data.end()) serve your purposes?

mtnpke commented 4 months ago

Heh, I coincidentally just had a look at that as well. API-wise, I think it's fine, but the implementation would have to be changed to something like the snippet above instead of n calls to push_back(value) (EDIT: for performance reasons). If I'm not mistaken, std::copy_n should boil down to a memcpy for contiguous iterators.

This would still leave the question of bulk value extraction, though.

breese commented 4 months ago

If performance is a concern, then it may indeed be a good idea to have free-standing bulk functions for insertion and removal.

mtnpke commented 4 months ago

Is there a downside to making push_back have better performance?

breese commented 4 months ago

Larger binaries.

mtnpke commented 4 months ago

I've checked with an (admittedly naive) implementation and it's still small enough to get inlined in a release build, but of course it's arguably larger and I don't have a strong opinion here - in the end it just needs to work :)

Original function: image

Modified function: image

Would you still consider a free-standing function a possible part of your library or should I just add it to my user code?

breese commented 4 months ago

Does your implementation assume random-access iterators?

I am interested in having these bulk functions as part of the library.

mtnpke commented 4 months ago

I haven't put a lot of time into it since it was just for a quick assessment (i.e., I'm not sure this is even correct - for sure there is some error handling missing in case the iterator distance exceeds the capacity of the ring buffer), but right now a forward iterator should suffice:

template <typename T, std::size_t E>
template <typename InputIterator>
TRIAL_CXX14_CONSTEXPR
void span<T, E>::push_back_seg(InputIterator first,
                           InputIterator last) noexcept(std::is_nothrow_copy_assignable<value_type>::value)
{
    static_assert(std::is_copy_assignable<T>::value, "T must be CopyAssignable");

    const auto allocSize{static_cast<std::size_t>(distance(first, last))};

    const auto oldNext{member.next};
    expand_back(allocSize);

    const auto bytesInFirstSeg{std::min(allocSize, capacity() - oldNext)};
    const auto bytesInLastSeg{allocSize - bytesInFirstSeg};
    std::copy_n(first, bytesInFirstSeg, member.data + oldNext);
    advance(first, bytesInFirstSeg);
    std::copy_n(first, bytesInLastSeg, member.data);
}

I guess the performance gain will be most apparent with contiguous iterators since it then boils down to a memcpy/memmove. For non-contiguous ones, I'd have to benchmark. It might be a bit faster because the resizing of the container is done as one operation, but I wouldn't count on it in an optimized build.