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
Delegate Cleanables #1711
Delegate Cleanables #1711
Conversation
@maysamyabandeh has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
Summary: Cleanable objects will perform the registered cleanups when they are destructed. We however rather to delay this cleaning like when we are gathering the merge operands. Current approach is to create the Cleanable object on heap (instead of on stack) and delay deleting it. By allowing Cleanables to delegate their cleanups to another cleanable object we can delay the cleaning without however the need to craete the cleanable object on heap and keeping it around. This patch applies this technique for the cleanups of BlockIter and shows improved performance for some in-memory benchmarks: +1.8% for merge worklaod, +6.4% for non-merge workload when the merge operator is specified. https://our.intern.facebook.com/intern/tasks?t=15168163 Non-merge benchmark: TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench --benchmarks=fillrandom --num=1000000 -value_size=100 -compression_type=none Reading random with no merge operator specified: TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench --benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000 --reads=10000000 --cache_size=10000000000 -threads=32 -compression_type=none 2>&1 Before patch: readrandom [AVG 5 runs] : 2959194 ops/sec; 207.4 MB/sec readrandom [MEDIAN 5 runs] : 2945102 ops/sec; 206.4 MB/sec After patch: readrandom [AVG 5 runs] : 2954630 ops/sec; 207.0 MB/sec readrandom [MEDIAN 5 runs] : 2949443 ops/sec; 206.7 MB/sec Reading random with a dummy merge operator specified: TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench --benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000 --reads=10000000 --cache_size=10000000000 -threads=32 -compression_type=none -merge_operator=put Before patch: readrandom [AVG 5 runs] : 2801713 ops/sec; 196.3 MB/sec readrandom [MEDIAN 5 runs] : 2798286 ops/sec; 196.1 MB/sec After patch: readrandom [AVG 5 runs] : 2981616 ops/sec; 208.9 MB/sec readrandom [MEDIAN 5 runs] : 2989652 ops/sec; 209.5 MB/sec Merge benchmark: TEST_TMPDIR=/dev/shm/v100nocomp-merge/ ./db_bench --benchmarks=mergerandom --num=1000000 -value_size=100 compression_type=none --merge_keys=100000 -merge_operator=max TEST_TMPDIR=/dev/shm/v100nocomp-merge/ ./db_bench --benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000 --reads=10000000 --cache_size=10000000000 -threads=32 -compression_type=none -merge_operator=max Before patch: readrandom [AVG 5 runs] : 942688 ops/sec; 10.4 MB/sec readrandom [MEDIAN 5 runs] : 941847 ops/sec; 10.4 MB/sec After patch: readrandom [AVG 5 runs] : 960135 ops/sec; 10.6 MB/sec readrandom [MEDIAN 5 runs] : 959413 ops/sec; 10.6 MB/sec
117ab32
to
c43e0bc
Compare
@maysamyabandeh updated the pull request - view changes - changes since last import |
@maysamyabandeh has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
The sandcastle job fails with this error:
Not sure what is the problem since it complies fine in my dev machine and finds the implicit rule as for other objects in the Makefile. Here is the change to the Makefile:
@IslamAbdelRahman any idea? |
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 like this a lot
@@ -1147,6 +1148,9 @@ full_filter_block_test: table/full_filter_block_test.o $(LIBOBJECTS) $(TESTHARNE | |||
log_test: db/log_test.o $(LIBOBJECTS) $(TESTHARNESS) | |||
$(AM_LINK) | |||
|
|||
cleanable_test: table/cleanable_test.o $(LIBOBJECTS) $(TESTHARNESS) |
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.
looks to me this is not needed
@@ -16,7 +16,7 @@ namespace rocksdb { | |||
// PinnedIteratorsManager will be notified whenever we need to pin an Iterator | |||
// and it will be responsible for deleting pinned Iterators when they are | |||
// not needed anymore. | |||
class PinnedIteratorsManager { | |||
class PinnedIteratorsManager : public Cleanable { |
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.
now the cleanup code will be called in the destructor, I think we must change that so that it's called in ReleasePinnedData()
@maysamyabandeh updated the pull request - view changes - changes since last import |
@maysamyabandeh has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
cleanup_.next = nullptr; | ||
} | ||
|
||
// TODO(myabandeh): if the list is too long we should maintain a tail pointer |
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.
Let's add the possible optimizations/bottlenecks we discussed offline in the comments here
The sandcastle tests failures seems false positives. The core dump shows the following:
which starts at this line
Which seems irrelevant to the changes made by this patch. Moreover it passed when run locally:
The failure from universal compaction test also seems irrelevant:
and it passes when run locally. |
and minor refactoring
@maysamyabandeh updated the pull request - view changes - changes since last import |
@maysamyabandeh has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
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.
LGTM, I have minor comments
// LICENSE file in the root directory of this source tree. An additional grant | ||
// of patent rights can be found in the PATENTS file in the same directory. | ||
// | ||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved. |
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 dont think we need to add this LevelDB section for new files
biter = &stack_biter; | ||
NewDataBlockIterator(rep_, read_options, iiter.value(), biter); | ||
} | ||
biter = &stack_biter; |
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.
let's use remove BlockIter* biter
in line 1542
and use BlockIter biter;
here only. I think that how it used to be before I changed it
Cleanable::~Cleanable() { | ||
Cleanable::~Cleanable() { DoCleanup(); } | ||
|
||
void Cleanable::Reset() { |
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.
Let's just have one function Reset()
, instead of having 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.
Reset is more expensive than DoCleanup as it does two value assignment too. We do not need that when the object is being destructed. I was not sure about penalty but since cleanup is heavily used my preference was not to add any cost to its destruction.
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 the 2 extra assignments should be a concern (except if benchmarks showed that they are of course)
This was just a suggestion, final decision is up to you
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.
You are right but since Cleanable is inherited in many objects, any small increase in construction/deconstruction cost could potentially be observable. I am more inclined to keep the cost the same as it was before.
cleanup_.function = c->function; | ||
cleanup_.arg1 = c->arg1; | ||
cleanup_.arg2 = c->arg2; | ||
delete c; |
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 the caller should be responsible for cleanup, what do you think ?
It's also confusing that we cleanup in one branch and not in the other
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 went back and forth between these two approaches and finally went with doing the delete inside RegisterCleanup. My argument was that there are two cases:
- RegisterCleanup takes the ownership and adds the object to its linkedlist
- RegisterCleanup does not need the object and want it deleted
I found it more consistent if RegisterCleanup takes the full ownership and takes care of both cases. Otherwise it would take ownership when it needs the object and would not do that when it wanted to be deleted, which is an inconsistent behavior.
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.
Sounds good to me
} | ||
Cleanup* c = &cleanup_; | ||
other->RegisterCleanup(c->function, c->arg1, c->arg2); | ||
c = c->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.
I think it's better to remove line 64,65 and just enter the loop directly
this is also related to my comment of not doing cleanup inside RegisterCleanup
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.
But the process for delegating the first Cleanup is different from the rest: the first is an embedded object and its values needs to be copied while the rest are on heap and they can be simply be added to the destination linked 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.
yes, my bad
@@ -16,7 +16,7 @@ namespace rocksdb { | |||
// PinnedIteratorsManager will be notified whenever we need to pin an Iterator | |||
// and it will be responsible for deleting pinned Iterators when they are | |||
// not needed anymore. | |||
class PinnedIteratorsManager { | |||
class PinnedIteratorsManager : public Cleanable { |
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 feel it's better to have a Cleanable data member
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 go either way but it would be great if we discuss it and have pros and cons listed. The pros of inheritance are:
- Simplicity: no need to add the functions and the glue them to the Cleanable data member.
What would be the cons of this approach, or what would the pros of data member approach?
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 just felt it's better to have it as a data member because everywhere in the code Cleanable was doing Cleanup in the destructor. now we support to do the cleanup and reuse the Cleanable object. It just felt to me that having this behavior more explicit (having a data member instead of inheritance) is better.
but final decision is up to you
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 do not have a strong opinion here. Let's go with your instinct and make it a data member.
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.
On second thought it seems easier if we keep the current inheritance scheme. The reason is that currently we can easily pass PinnedIteratorManager as a Cleanable to DelegateCleanupsTo function:
biter.DelegateCleanupsTo(pinned_iters_mgr);
void Cleanable::DelegateCleanupsTo(Cleanable* other)
Without having PinnedIteratorManager manager we would have two choices:
- Cleanable::DelegateCleanupsTo(PinnedIteratorManager* other), which could cause circular dependency and would need more code restructuring
- PinnedIteratorManager::DelegateCleanupsFrom(Cleanable*) which would require making PinnedIteratorManager a friend class of Cleanable to be able to access its internals, which does not look clean to me.
@maysamyabandeh 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.
Thanks a lot @maysamyabandeh
} | ||
Cleanup* c = &cleanup_; | ||
other->RegisterCleanup(c->function, c->arg1, c->arg2); | ||
c = c->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.
yes, my bad
cleanup_.function = c->function; | ||
cleanup_.arg1 = c->arg1; | ||
cleanup_.arg2 = c->arg2; | ||
delete c; |
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.
Sounds good to me
Cleanable::~Cleanable() { | ||
Cleanable::~Cleanable() { DoCleanup(); } | ||
|
||
void Cleanable::Reset() { |
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 the 2 extra assignments should be a concern (except if benchmarks showed that they are of course)
This was just a suggestion, final decision is up to you
@@ -16,7 +16,7 @@ namespace rocksdb { | |||
// PinnedIteratorsManager will be notified whenever we need to pin an Iterator | |||
// and it will be responsible for deleting pinned Iterators when they are | |||
// not needed anymore. | |||
class PinnedIteratorsManager { | |||
class PinnedIteratorsManager : public Cleanable { |
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 just felt it's better to have it as a data member because everywhere in the code Cleanable was doing Cleanup in the destructor. now we support to do the cleanup and reuse the Cleanable object. It just felt to me that having this behavior more explicit (having a data member instead of inheritance) is better.
but final decision is up to you
@maysamyabandeh has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator. |
Summary: Cleanable objects will perform the registered cleanups when
they are destructed. We however rather to delay this cleaning like when
we are gathering the merge operands. Current approach is to create the
Cleanable object on heap (instead of on stack) and delay deleting it.
By allowing Cleanables to delegate their cleanups to another cleanable
object we can delay the cleaning without however the need to craete the
cleanable object on heap and keeping it around. This patch applies this
technique for the cleanups of BlockIter and shows improved performance
for some in-memory benchmarks:
+1.8% for merge worklaod, +6.4% for non-merge workload when the merge
operator is specified.
https://our.intern.facebook.com/intern/tasks?t=15168163
Non-merge benchmark:
TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench --benchmarks=fillrandom
--num=1000000 -value_size=100 -compression_type=none
Reading random with no merge operator specified:
TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench
--benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000
--reads=10000000 --cache_size=10000000000 -threads=32
-compression_type=none 2>&1
Before patch:
readrandom [AVG 5 runs] : 2959194 ops/sec; 207.4 MB/sec
readrandom [MEDIAN 5 runs] : 2945102 ops/sec; 206.4 MB/sec
After patch:
readrandom [AVG 5 runs] : 2954630 ops/sec; 207.0 MB/sec
readrandom [MEDIAN 5 runs] : 2949443 ops/sec; 206.7 MB/sec
Reading random with a dummy merge operator specified:
TEST_TMPDIR=/dev/shm/v100nocomp/ ./db_bench
--benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000
--reads=10000000 --cache_size=10000000000 -threads=32
-compression_type=none -merge_operator=put
Before patch:
readrandom [AVG 5 runs] : 2801713 ops/sec; 196.3 MB/sec
readrandom [MEDIAN 5 runs] : 2798286 ops/sec; 196.1 MB/sec
After patch:
readrandom [AVG 5 runs] : 2981616 ops/sec; 208.9 MB/sec
readrandom [MEDIAN 5 runs] : 2989652 ops/sec; 209.5 MB/sec
Merge benchmark:
TEST_TMPDIR=/dev/shm/v100nocomp-merge/ ./db_bench
--benchmarks=mergerandom --num=1000000 -value_size=100
compression_type=none --merge_keys=100000 -merge_operator=max
TEST_TMPDIR=/dev/shm/v100nocomp-merge/ ./db_bench
--benchmarks="readseq,readrandom[X5]" --use_existing_db --num=1000000
--reads=10000000 --cache_size=10000000000 -threads=32
-compression_type=none -merge_operator=max
Before patch:
readrandom [AVG 5 runs] : 942688 ops/sec; 10.4 MB/sec
readrandom [MEDIAN 5 runs] : 941847 ops/sec; 10.4 MB/sec
After patch:
readrandom [AVG 5 runs] : 960135 ops/sec; 10.6 MB/sec
readrandom [MEDIAN 5 runs] : 959413 ops/sec; 10.6 MB/sec