Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

yiwu-arbug
Copy link
Contributor

@yiwu-arbug yiwu-arbug commented Oct 31, 2016

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.

@yiwu-arbug
Copy link
Contributor Author

cc @nbronson @mvm3k @al13n321

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@siying
Copy link
Contributor

siying commented Oct 31, 2016

Will take a look. By the way, the Windows build failed.

@yiwu-arbug
Copy link
Contributor Author

Offline discussed with @siying @IslamAbdelRahman @lightmark yesterday. Will try the follow way:

  • Only store a few levels of prev for binary search in small range.
  • If prev is not helping do a full binary search.
    In this way the code will be much simpler and takes less memory overhead, and hopefully have comparable performance.

Copy link
Contributor

@siying siying left a 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

@yiwu-arbug yiwu-arbug left a 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);
Copy link
Contributor Author

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);
Copy link
Contributor Author

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().

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

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;
Copy link
Contributor

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(
Copy link
Contributor

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++;
Copy link
Contributor

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_?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

@siying
Copy link
Contributor

siying commented Nov 8, 2016

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.

@yiwu-arbug
Copy link
Contributor Author

@siying that's my plan. I'm wanting to send the PR before finishing the test to get early feedback.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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);
}
}
Copy link
Contributor

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);
Copy link
Contributor

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);
Copy link
Contributor

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()?

Copy link
Contributor Author

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.

@yiwu-arbug
Copy link
Contributor Author

@siying I added some inline comments which hopefully give better explanation. Hope they helps, or we can discuss offline.

Copy link
Contributor

@siying siying left a 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;
Copy link
Contributor

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);
Copy link
Contributor

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.

@yiwu-arbug
Copy link
Contributor Author

@siying I agree with you. After reading your comment, I think the upper half of prev together with FindWithHint() gives little benefit. I'm to remove them and make it cleaner.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

Removed FindWithHint() and related logic.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

Make InsertHint struct public. Add unit tests to inlineskiplist_test.

@yiwu-arbug
Copy link
Contributor Author

clang-format; Fix windows build.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

Copy link
Contributor

@siying siying left a 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());
}
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

}
Validate(list);
}

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@yiwu-arbug yiwu-arbug left a 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);
}

Copy link
Contributor Author

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());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

Tests validate the skiplist on all levels.

@facebook-github-bot
Copy link
Contributor

@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);
Copy link
Contributor

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);
}
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK then.

@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

Assert nodes[0] < nodes[0]->Next().

Copy link
Contributor

@siying siying left a 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)));
Copy link
Contributor

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().

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.
@facebook-github-bot
Copy link
Contributor

@yiwu-arbug updated the pull request - view changes - changes since last import

@yiwu-arbug
Copy link
Contributor Author

Fix lint error.

@yiwu-arbug yiwu-arbug deleted the insert_hint branch November 13, 2016 23:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants