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

Delegate Cleanables #1711

Closed

Conversation

maysamyabandeh
Copy link
Contributor

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

@facebook-github-bot
Copy link
Contributor

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

@maysamyabandeh updated the pull request - view changes - changes since last import

@facebook-github-bot
Copy link
Contributor

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

@maysamyabandeh
Copy link
Contributor Author

The sandcastle job fails with this error:

make: *** No rule to make target `table/cleanable_test.o', needed by `cleanable_test'.  Stop.

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:

diff --git a/Makefile b/Makefile
index d768647..11220ff 100644
--- a/Makefile
+++ b/Makefile
@@ -324,6 +324,7 @@ TESTS = \
        db_properties_test \
        db_table_properties_test \
        autovector_test \
+       cleanable_test \
        column_family_test \
        table_properties_collector_test \
        arena_test \
@@ -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)
+       $(AM_LINK)
+
 table_test: table/table_test.o $(LIBOBJECTS) $(TESTHARNESS)
        $(AM_LINK)

@IslamAbdelRahman any idea?

Copy link
Contributor

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

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

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

@facebook-github-bot
Copy link
Contributor

@maysamyabandeh updated the pull request - view changes - changes since last import

@facebook-github-bot
Copy link
Contributor

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

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

@maysamyabandeh
Copy link
Contributor Author

maysamyabandeh commented Dec 23, 2016

The sandcastle tests failures seems false positives. The core dump shows the following:

Core was generated by `./external_sst_file_test --gtest_filter=ExternalSSTFileTest.PickedLevelDynamic'.

(gdb) bt
#0  0x00002ab31ceb7cfa in  ()
#1  0x00000000005283a5 in __gnu_cxx::__exchange_and_add_single (__val=-1, __mem=0x7ffc1ce68714)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/ext/atomicity.h:68
#2  0x00000000005283a5 in __gnu_cxx::__exchange_and_add_dispatch (__val=-1, __mem=0x7ffc1ce68714)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/ext/atomicity.h:84
#3  0x00000000005283a5 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() (this=0x7ffc1ce68708)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/bits/shared_ptr_base.h:163
#4  0x00000000005283a5 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() (this=0x7ffc1ce68288, __in_chrg=<optimized out>)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/bits/shared_ptr_base.h:666
#5  0x00000000005283a5 in std::__shared_ptr<rocksdb::FilterPolicy const, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() (this=0x7ffc1ce68280, __in_chrg=<optimized out>)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/bits/shared_ptr_base.h:914
#6  0x00000000005283a5 in std::shared_ptr<rocksdb::FilterPolicy const>::~shared_ptr() (this=0x7ffc1ce68280, __in_chrg=<optimized out>)
    at /mnt/gvfs/third-party2/gcc/cf7d14c625ce30bae1a4661c2319c5a283e4dd22/4.9.x/centos6-native/108cf83/include/c++/4.9.x-google/bits/shared_ptr.h:93
#7  0x00000000005283a5 in rocksdb::anon::OptionsOverride::~OptionsOverride() (this=0x7ffc1ce68280, __in_chrg=<optimized out>) at ./db/db_test_util.h:114
#8  0x00000000005283a5 in rocksdb::DBTestBase::OptionsForLogIterTest() (this=<optimized out>) at db/db_test_util.cc:951
#9  0x00000000005285ac in rocksdb::DBTestBase::ChangeCompactOptions() (this=0x7ffc1ce68700) at db/db_test_util.cc:166
#10 0x0000000000000000 in  ()

which starts at this line

166         auto options = CurrentOptions();

Which seems irrelevant to the changes made by this patch. Moreover it passed when run locally:

[?0] myabandeh@devvm17154:~/rocksdb[cleanabledelagate)]$ ./external_sst_file_test --gtest_filter=ExternalSSTFileTest.PickedLevelDynamic
Note: Google Test filter = ExternalSSTFileTest.PickedLevelDynamic
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from ExternalSSTFileTest
[ RUN      ] ExternalSSTFileTest.PickedLevelDynamic
[       OK ] ExternalSSTFileTest.PickedLevelDynamic (10 ms)
[----------] 1 test from ExternalSSTFileTest (10 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (10 ms total)
[  PASSED  ] 1 test.

The failure from universal compaction test also seems irrelevant:

[----------] 1 test from UniversalCompactionNumLevels/DBTestUniversalCompaction
[ RUN      ] UniversalCompactionNumLevels/DBTestUniversalCompaction.UniversalCompactionTrivialMoveTest2/1
db/db_universal_compaction_test.cc:1068: Failure
Value of: 0
Expected: non_trivial_move
Which is: 1

and it passes when run locally.

@facebook-github-bot
Copy link
Contributor

@maysamyabandeh updated the pull request - view changes - changes since last import

@facebook-github-bot
Copy link
Contributor

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

Copy link
Contributor

@IslamAbdelRahman IslamAbdelRahman left a 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.
Copy link
Contributor

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

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

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

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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;
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 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

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 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:

  1. RegisterCleanup takes the ownership and adds the object to its linkedlist
  2. 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.

Copy link
Contributor

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;
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 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

Copy link
Contributor Author

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.

Copy link
Contributor

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

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

Copy link
Contributor Author

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:

  1. 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?

Copy link
Contributor

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

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 do not have a strong opinion here. Let's go with your instinct and make it a data member.

Copy link
Contributor Author

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 maysamyabandeh mentioned this pull request Dec 29, 2016
@facebook-github-bot
Copy link
Contributor

@maysamyabandeh updated the pull request - view changes - changes since last import

Copy link
Contributor

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

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

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

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

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

@facebook-github-bot
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants