-
Notifications
You must be signed in to change notification settings - Fork 602
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
iobuf: replace boost::intrusive_list with hand rolled implementation #25053
Conversation
CI test resultstest results on build#61657
test results on build#61705
test results on build#61718
test results on build#61755
|
Before
After
After (updated)
|
7489680
to
379c910
Compare
f990a32
to
27b804b
Compare
I reverted c743388 from the discussion the other day, the new performance numbers look 😎
|
src/v/bytes/details/io_fragment.h
Outdated
|
||
public: | ||
io_fragment_list() noexcept = default; | ||
io_fragment_list(io_fragment_list&&) noexcept = default; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
don't we need to reset _head/_tail on the moved-from to satisfy the assertion in the destructor?
-
also i wonder if there is any code in the tree that assumes a moved-from iobuf is empty, which the default ctor wouldn't do right?
maybe i'm missing something...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's only safe because iobuf
"fixes" it:
Line 94 in 317787e
x._frags = container{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh of course, this is replacing container_t
, right
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be preferable in any way to push that down into the list's move constructor? genuine question, I don't think I have an opinion. probably roughly (or exactly) the same code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to have that here, local. The problem is I don't know how to do that for move assignment, because this is a non owning container, I can't do anything but assert that the container is empty before.
Good news we don't use move assignment, so I can just delete it and handle move ctor here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh this was a great suggestion, it's driven the benchmarks down even further:
test | iterations | median | mad | min | max | allocs | tasks | inst |
---|---|---|---|---|---|---|---|---|
iobuf.move_bench_small | 520848000 | 1.469ns | 0.000ns | 1.468ns | 1.471ns | 0.000 | 0.000 | 7.2 |
iobuf.move_bench_medium | 1694000 | 1.582ns | 0.002ns | 1.579ns | 1.586ns | 0.000 | 0.000 | 7.2 |
iobuf.move_bench_large | 528000 | 1.599ns | 0.009ns | 1.590ns | 1.614ns | 0.000 | 0.000 | 7.2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hahaha sweet 🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😎
just a few nitpicks on first pass. have not reviewed all the code yet.
Like assert but with better messages
@@ -73,31 +76,33 @@ class generic_iobuf_writer | |||
// 3. Drop the final character | |||
// For each encoded fragment that is written (except the first one): | |||
// 4. Restore the stashed character over the prefix-quote | |||
for (auto i = beg; i != end; ++i) { | |||
for (auto i = beg;;) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR pretty much LGTM, except that I had/am having a hard time convincing myself that this change here is equivalent, and I'm not sure I follow why the restructuring is needed vs just swapping out the last_frag() implementation (also ignoring the early return, which seems fine).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can't compare i to last anymore because they are different iterator types now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh makes sense. Is it generally a bad practice to allow comparing different iterator types? Like could we templatize operator== to support different iterators and just compare their _current (or maybe the template math doesn't math)?
In any case, I stared at it more and yea it does seem equivalent, so lgtm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have implicit conversion between non-const to const iterator (standard), but I don't think it's generally allowed to go from forward to backward iterator mostly because I don't know how to map the end states in a coherent way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya I can buy not being able to convert. I'm wondering if implementing a comparator would work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean semantically how to compare end vs rend?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general C++ doesn't allow comparison of iterators and corresponding reverse iterators, probably for good reasons (reverse iterators are weirder than you might think since you cannot use one-before-the-front in the same way you can use one-past-the-end), so I don't think we should either?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean semantically how to compare end vs rend?
Ah, I would've thought something like, end iters aren't equal if their Forward and containers aren't equal.
In any case, not implementing a comparator sgtm -- it does seem like a can of worms, especially if it's not a common practice
This reverts commit c743388.
Just inline the implementations
We need to correctly handle empty string, as it stands today, an empty iobuf will end up in invalid JSON. While we are here also make the algorithm here only need forward_iterator from iobuf instead of bidirectional_iterator
This is not required for the avro interface here, we only need to be able to return data from the last successful call to `next`, and since we still have a pointer to the last fragment, we can restore the data in that fragment.
We can use the reserve iterator stuff here.
To simplify the implementation and remove the need for the iterator to have both the container and the current node, to just be the current node.
This grabs a `const iobuf&`, which is then destroyed after the iterator is created. I think because the underlying buffers were shared, it was kind of fine, but certainly is sketch.
@@ -73,31 +76,33 @@ class generic_iobuf_writer | |||
// 3. Drop the final character | |||
// For each encoded fragment that is written (except the first one): | |||
// 4. Restore the stashed character over the prefix-quote | |||
for (auto i = beg; i != end; ++i) { | |||
for (auto i = beg;;) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh makes sense. Is it generally a bad practice to allow comparing different iterator types? Like could we templatize operator== to support different iterators and just compare their _current (or maybe the template math doesn't math)?
In any case, I stared at it more and yea it does seem equivalent, so lgtm!
A number of issues have cropped up in microbenchmark reports due to how expensive it is to move an iobuf. This has occurred at least three times to my knowledge:
replicate
API changes, follow up #24771Let's fix the underlying problem and remove the usage of boost::intrusive_list from iobuf. The new handrolled implementation improves performance of the iceberg translation path by ~60% in one microbenchmark (with 35% less instructions). This benchmark heavily uses
iobuf
(being all the messages are smallish strings that get moved around a bunch).Backports Required
Release Notes