-
Notifications
You must be signed in to change notification settings - Fork 6.2k
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
Optimize sequential insert into memtable - Part 2: Implementation #1449
Conversation
@yiwu-arbug has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
Will take a look. By the way, the Windows build failed. |
Offline discussed with @siying @IslamAbdelRahman @lightmark yesterday. Will try the follow 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.
I didn't finish reviewing it yet. Some comments I have so far.
template <class Comparator> | ||
void InlineSkipList<Comparator>::InsertWithHint( | ||
const char* key, InsertHint** hint_ptr) { | ||
hint_valid_.store(false, std::memory_order_relaxed); |
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.
Why?
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.
Same as in InsertConcurrently
, any further insert not using prev_
breaks the prev_
optimization in Insert()
.
// NoBarrier_SetNext() suffices since we will add a barrier when | ||
// we publish a pointer to "x" in prev[i]. | ||
x->NoBarrier_SetNext(i, prev_[i]->NoBarrier_Next(i)); | ||
prev_[i]->SetNext(i, x); |
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.
Can we avoid slowdown in existing Insert()? It does look like PushLast() is more expensive than SetNext(). Insert() is a very critical code path. Please make sure the performance doesn't regress.
In fact, I suggest we keep the Insert() logic as it is. Even the logic like hint_valid_ may cause performance regression.
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.
PushLast() captures line 567-569 and line 581-585 in the existing code and they are doing exactly the same thing.
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 review the current code. I'll update the PR later today, which looks quite different than this version.
// NoBarrier_SetNext() suffices since we will add a barrier when | ||
// we publish a pointer to "x" in prev[i]. | ||
x->NoBarrier_SetNext(i, prev_[i]->NoBarrier_Next(i)); | ||
prev_[i]->SetNext(i, x); |
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.
PushLast() captures line 567-569 and line 581-585 in the existing code and they are doing exactly the same thing.
template <class Comparator> | ||
void InlineSkipList<Comparator>::InsertWithHint( | ||
const char* key, InsertHint** hint_ptr) { | ||
hint_valid_.store(false, std::memory_order_relaxed); |
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.
Same as in InsertConcurrently
, any further insert not using prev_
breaks the prev_
optimization in Insert()
.
b8a2774
to
6560d3f
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
PR updated. Will update inline comment and prepare docs to explain the implementation. |
return false; | ||
} | ||
Node* next = n->NoBarrier_Next(level); | ||
return next == nullptr || compare_(key, n->Next(level)->Key()) < 0; |
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.
Why not next->key()
?
Node* FindLessThan(const char* key, Node** prev, Node* top, int start_level, | ||
int stop_level) const; | ||
|
||
void FindWithHint( |
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.
Add comments explaining what start_level
and stop_level
mean.
if (p != nullptr && h > hint->num_levels) { | ||
hint->prev[hint->num_levels] = p; | ||
hint->prev_height[hint->num_levels] = h; | ||
hint->num_levels++; |
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 function is too hard for me to understand. Is there a way we go with a simple solution that treats hint->prev the same way as prev_?
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'm getting ~40% less comparisons with this approach per my benchmark (inserting 5M keys into a skip-list, there are 10000 prefixes and for each of the prefixes keys are mostly inserted sequentially but can be reordered up to 5 positions. Each prefix gets its own hint). The benchmark is probably in favor of the current approach, but that the perf gain looks a lot.
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'm not about to understand Mathmatically how there can be 40% saving. Only 1/4 of the chance prev[0] is more than one level. In this case, perhaps extra 6 comparisons will be made. For the other 3/4 cases, the average is perhaps 8 comparisons.
In your benchmark, how many average comparisons are issued per insert?
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.
40% gain is comparing with a naive solution I run earlier but didn't sent a PR. Comparing this version with the version I present last week there's 20% gain, with average comparisons per insert being ~6.4 vs ~5.3.
In terms of correctness, does it make sense to write a validation function to validate the skip list and use it in the unit test? It's very hard to prove correctness just by code review. |
@siying that's my plan. I'm wanting to send the PR before finishing the test to get early feedback. |
6560d3f
to
0c4de23
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Updated with inline comments, address comments and fix test failures. Pending unit test. Will send benchmark code in a separate PR. @siying If you want I can prepare a quip doc with better explanation. |
break; | ||
} | ||
} | ||
if (level >= hint->num_levels) { |
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'm confused here. How can level > hint->num_levels?
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 if (level == hint->num_levels)
here. Will update.
if (level > stop_level) { | ||
FindLessThan(key, hint->prev, hint->prev[level], level, stop_level); | ||
} | ||
} |
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 function confused me. I see the function is used in two places and serve very different cases. Can we have two functions instead?
} | ||
} | ||
if (level >= hint->num_levels) { | ||
FindWithHint(key, hint, std::max<int>(level, height - 1), level); |
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 too hard for me too understand. If I understand correctly, in this case we basically start from the root. Can we write specific code for it?
hint_max_height = std::max<int>(hint_max_height, hint->prev_height[i]); | ||
} | ||
if (height > hint_max_height) { | ||
FindWithHint(key, hint, height - 1, hint_max_height); |
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 are looking for prev from root, right? Can we write more specific code for that, rather than a general FindWithHint()
?
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 both cases where I call FindWithHint()
, I'm looking for the lowest level where hint->prev
is a valid prev, and search from that level. In the worst case it can search from root.
@siying I added some inline comments which hopefully give better explanation. Hope they helps, or we can discuss offline. |
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 should discuss offline about how we can make it easier to maintain.
// [stop_level, start_level]. Using previous value of hint->prev to help | ||
// speed-up the search. | ||
void FindWithHint(const char* key, InsertHint* hint, int start_level, | ||
int stop_level) const; |
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 is still not clear to me what this function does after reading the comment. We can discuss offline.
Also maybe rename it to something like AdjustHintInPrev().
while (current_level < height && current_level < hint->prev_height[i]) { | ||
assert(KeyIsAfterNode(key, hint->prev[i])); | ||
assert(!KeyIsAfterNode(key, hint->prev[i]->Next(current_level))); | ||
x->InsertAfter(hint->prev[i], current_level); |
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.
One thing made it hard for me to understand the code is the dual function of hint->prev. It is used as the location to insert the key here, but some part of it is also the position to insert the new entry.
If we can separate the two. Use another local array for the position to insert, just as tmp
in line 828 to 835, it may be easier to understand.
@siying I agree with you. After reading your comment, I think the upper half of |
0c4de23
to
cb5edc3
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Removed |
cb5edc3
to
984f09b
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Make |
984f09b
to
c4c7114
Compare
clang-format; Fix windows build. |
@yiwu-arbug updated the pull request - view changes - changes since last import |
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 much clearer to me. I don't have further comment for the code. A comment about the test validation.
iter.Next(); | ||
} | ||
ASSERT_FALSE(iter.Valid()); | ||
} |
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 validation only validates the link of the next level is correct.
To validate it is a valid skip list, we also need to validate every level is at the correct order, for every node for a higher level ink, the node is included in lower level too.
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.
sure.
} | ||
Validate(list); | ||
} | ||
|
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 also suggest we create a randomize test and validate the skip list after that.
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.
Both InsertWithHint_MultipleHintsRandom
and InsertWithHintAndWithoutHint
has some randomness in it. Any other random test you want me to add?
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 fine then. Sorry I missed that.
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.
@siying thank you for reviewing the complex diff!
} | ||
Validate(list); | ||
} | ||
|
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.
Both InsertWithHint_MultipleHintsRandom
and InsertWithHintAndWithoutHint
has some randomness in it. Any other random test you want me to add?
iter.Next(); | ||
} | ||
ASSERT_FALSE(iter.Valid()); | ||
} |
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.
sure.
c4c7114
to
b1dea02
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Tests validate the skiplist on all levels. |
b1dea02
to
ae0d98d
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
nodes[i] = head_; | ||
} | ||
while (nodes[0] != nullptr) { | ||
nodes[0] = nodes[0]->Next(0); |
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.
Should assert nodes[0] is smaller than nodes[0]->Next(0).
} | ||
for (int i = 1; i < max_height; i++) { | ||
assert(nodes[i]->Next(i) == nullptr); | ||
} |
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 think you also need to verify all the levels in all the nodes are used. Otherwise, you may get something like this:
+-+ +-+ ++
| | | | ||
| | | | ||
| | | | ||
| | | | ||
| +------------------------> |------>
| | | | ||
| | | | ||
| +-----------> | +------------------>
| | | | ||
| | | | ||
| | +---------> | | +------> ||
+-+ +-+ ++
which will not get you the correct result.
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.
Line 918-920 is verifying it. I only advance nodes
when it appears on level 0. If nodes[i]->next(i)
is nullptr, that means all nodes on level i do appears on level < i.
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.
OK then.
ae0d98d
to
391e8ec
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Assert nodes[0] < nodes[0]->Next(). |
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.
Good to go!
// * no other nodes less than prev[level-1] has height greater than | ||
// current_level, and prev[level-1] > key. | ||
assert(KeyIsAfterNode(key, hint->prev[i])); | ||
assert(!KeyIsAfterNode(key, hint->prev[i]->Next(current_level))); |
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 still feel we should turn those asserts to actual check, just to be safe. If the check fails, simply fall back to normal Insert().
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 don't think it is a good reason to turn assert into actual check just for safety. Anything wrong in the skiplist will crash quite fatally with random tests. But I think if we can remove the requirement "keys with the same hint has to be consecutive", i.e. if we detect keys doesn't following the requirement, invalidate the hint and start over, then it make sense.
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.
will work on it on a separate PR.
Summary: Implement a insert hint into skip-list to hint insert position. This is to optimize for the write workload where there are multiple stream of sequential writes. For example, there is a stream of keys of a1, a2, a3... but also b1, b2, b2... Each stream are not neccessary strictly sequential, but can get reorder a little bit. User can specify a prefix extractor and the `SkipListRep` can thus maintan a hint for each of the stream for fast insert into memtable. This is the internal implementation part. See #1419 for the interface part. See inline comments for details. Test Plan: See the new tests.
391e8ec
to
4169336
Compare
@yiwu-arbug updated the pull request - view changes - changes since last import |
Fix lint error. |
Summary:
Implement a insert hint into skip-list to hint insert position. This is
to optimize for the write workload where there are multiple stream of
sequential writes. For example, there is a stream of keys of a1, a2,
a3... but also b1, b2, b2... Each stream are not neccessary strictly
sequential, but can get reorder a little bit. User can specify a prefix
extractor and the
SkipListRep
can thus maintan a hint for each of thestream for fast insert into memtable.
This is the internal implementation part. See #1419 for the interface part.
See inline comments for details.
Test Plan:
See the new tests.