reduce memory usage of cuckoo table builder

Summary:
builder currently buffers all key value pairs as a vector of
pair<string, string>. That is too much due to std::string
overhead. It wasn't able to fit 1B key/values (12bytes total) in 100GB
of ram. Switch to use a plain string to store the key/value sequence and
use only 12GB of ram as a result.

Test Plan: db_bench

Reviewers: igor, sdong, yhchiang

Reviewed By: sdong

Subscribers: leveldb

Differential Revision: https://reviews.facebook.net/D23763
This commit is contained in:
Lei Jin 2014-09-25 16:34:24 -07:00
parent c6275956e2
commit 94997eab5e
2 changed files with 57 additions and 36 deletions

View File

@ -60,6 +60,9 @@ CuckooTableBuilder::CuckooTableBuilder(
hash_table_size_(use_module_hash ? 0 : 2), hash_table_size_(use_module_hash ? 0 : 2),
is_last_level_file_(false), is_last_level_file_(false),
has_seen_first_key_(false), has_seen_first_key_(false),
key_size_(0),
value_size_(0),
num_entries_(0),
ucomp_(user_comparator), ucomp_(user_comparator),
use_module_hash_(use_module_hash), use_module_hash_(use_module_hash),
identity_as_first_hash_(identity_as_first_hash), identity_as_first_hash_(identity_as_first_hash),
@ -72,7 +75,7 @@ CuckooTableBuilder::CuckooTableBuilder(
} }
void CuckooTableBuilder::Add(const Slice& key, const Slice& value) { void CuckooTableBuilder::Add(const Slice& key, const Slice& value) {
if (kvs_.size() >= kMaxVectorIdx - 1) { if (num_entries_ >= kMaxVectorIdx - 1) {
status_ = Status::NotSupported("Number of keys in a file must be < 2^32-1"); status_ = Status::NotSupported("Number of keys in a file must be < 2^32-1");
return; return;
} }
@ -90,15 +93,18 @@ void CuckooTableBuilder::Add(const Slice& key, const Slice& value) {
has_seen_first_key_ = true; has_seen_first_key_ = true;
smallest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size()); smallest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size());
largest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size()); largest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size());
key_size_ = is_last_level_file_ ? ikey.user_key.size() : key.size();
value_size_ = value.size();
} }
// Even if one sequence number is non-zero, then it is not last level. // Even if one sequence number is non-zero, then it is not last level.
assert(!is_last_level_file_ || ikey.sequence == 0); assert(!is_last_level_file_ || ikey.sequence == 0);
if (is_last_level_file_) { if (is_last_level_file_) {
kvs_.emplace_back(std::make_pair( kvs_.append(ikey.user_key.data(), ikey.user_key.size());
ikey.user_key.ToString(), value.ToString()));
} else { } else {
kvs_.emplace_back(std::make_pair(key.ToString(), value.ToString())); kvs_.append(key.data(), key.size());
} }
kvs_.append(value.data(), value.size());
++num_entries_;
// In order to fill the empty buckets in the hash table, we identify a // In order to fill the empty buckets in the hash table, we identify a
// key which is not used so far (unused_user_key). We determine this by // key which is not used so far (unused_user_key). We determine this by
@ -111,21 +117,32 @@ void CuckooTableBuilder::Add(const Slice& key, const Slice& value) {
largest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size()); largest_user_key_.assign(ikey.user_key.data(), ikey.user_key.size());
} }
if (!use_module_hash_) { if (!use_module_hash_) {
if (hash_table_size_ < kvs_.size() / max_hash_table_ratio_) { if (hash_table_size_ < num_entries_ / max_hash_table_ratio_) {
hash_table_size_ *= 2; hash_table_size_ *= 2;
} }
} }
} }
Slice CuckooTableBuilder::GetKey(uint64_t idx) const {
return Slice(&kvs_[idx * (key_size_ + value_size_)], key_size_);
}
Slice CuckooTableBuilder::GetUserKey(uint64_t idx) const {
return is_last_level_file_ ? GetKey(idx) : ExtractUserKey(GetKey(idx));
}
Slice CuckooTableBuilder::GetValue(uint64_t idx) const {
return Slice(&kvs_[idx * (key_size_ + value_size_) + key_size_], value_size_);
}
Status CuckooTableBuilder::MakeHashTable(std::vector<CuckooBucket>* buckets) { Status CuckooTableBuilder::MakeHashTable(std::vector<CuckooBucket>* buckets) {
buckets->resize(hash_table_size_ + cuckoo_block_size_ - 1); buckets->resize(hash_table_size_ + cuckoo_block_size_ - 1);
uint64_t make_space_for_key_call_id = 0; uint64_t make_space_for_key_call_id = 0;
for (uint32_t vector_idx = 0; vector_idx < kvs_.size(); vector_idx++) { for (uint32_t vector_idx = 0; vector_idx < num_entries_; vector_idx++) {
uint64_t bucket_id; uint64_t bucket_id;
bool bucket_found = false; bool bucket_found = false;
autovector<uint64_t> hash_vals; autovector<uint64_t> hash_vals;
Slice user_key = is_last_level_file_ ? kvs_[vector_idx].first : Slice user_key = GetUserKey(vector_idx);
ExtractUserKey(kvs_[vector_idx].first);
for (uint32_t hash_cnt = 0; hash_cnt < num_hash_func_ && !bucket_found; for (uint32_t hash_cnt = 0; hash_cnt < num_hash_func_ && !bucket_found;
++hash_cnt) { ++hash_cnt) {
uint64_t hash_val = CuckooHash(user_key, hash_cnt, use_module_hash_, uint64_t hash_val = CuckooHash(user_key, hash_cnt, use_module_hash_,
@ -140,10 +157,8 @@ Status CuckooTableBuilder::MakeHashTable(std::vector<CuckooBucket>* buckets) {
bucket_found = true; bucket_found = true;
break; break;
} else { } else {
if (ucomp_->Compare(user_key, is_last_level_file_ if (ucomp_->Compare(user_key,
? Slice(kvs_[(*buckets)[hash_val].vector_idx].first) GetUserKey((*buckets)[hash_val].vector_idx)) == 0) {
: ExtractUserKey(
kvs_[(*buckets)[hash_val].vector_idx].first)) == 0) {
return Status::NotSupported("Same key is being inserted again."); return Status::NotSupported("Same key is being inserted again.");
} }
hash_vals.push_back(hash_val); hash_vals.push_back(hash_val);
@ -183,10 +198,10 @@ Status CuckooTableBuilder::Finish() {
std::vector<CuckooBucket> buckets; std::vector<CuckooBucket> buckets;
Status s; Status s;
std::string unused_bucket; std::string unused_bucket;
if (!kvs_.empty()) { if (num_entries_ > 0) {
// Calculate the real hash size if module hash is enabled. // Calculate the real hash size if module hash is enabled.
if (use_module_hash_) { if (use_module_hash_) {
hash_table_size_ = kvs_.size() / max_hash_table_ratio_; hash_table_size_ = num_entries_ / max_hash_table_ratio_;
} }
s = MakeHashTable(&buckets); s = MakeHashTable(&buckets);
if (!s.ok()) { if (!s.ok()) {
@ -224,14 +239,13 @@ Status CuckooTableBuilder::Finish() {
AppendInternalKey(&unused_bucket, ikey); AppendInternalKey(&unused_bucket, ikey);
} }
} }
properties_.num_entries = kvs_.size(); properties_.num_entries = num_entries_;
properties_.fixed_key_len = unused_bucket.size(); properties_.fixed_key_len = key_size_;
uint32_t value_length = kvs_.empty() ? 0 : kvs_[0].second.size();
uint32_t bucket_size = value_length + properties_.fixed_key_len;
properties_.user_collected_properties[ properties_.user_collected_properties[
CuckooTablePropertyNames::kValueLength].assign( CuckooTablePropertyNames::kValueLength].assign(
reinterpret_cast<const char*>(&value_length), sizeof(value_length)); reinterpret_cast<const char*>(&value_size_), sizeof(value_size_));
uint64_t bucket_size = key_size_ + value_size_;
unused_bucket.resize(bucket_size, 'a'); unused_bucket.resize(bucket_size, 'a');
// Write the table. // Write the table.
uint32_t num_added = 0; uint32_t num_added = 0;
@ -240,9 +254,9 @@ Status CuckooTableBuilder::Finish() {
s = file_->Append(Slice(unused_bucket)); s = file_->Append(Slice(unused_bucket));
} else { } else {
++num_added; ++num_added;
s = file_->Append(kvs_[bucket.vector_idx].first); s = file_->Append(GetKey(bucket.vector_idx));
if (s.ok()) { if (s.ok()) {
s = file_->Append(kvs_[bucket.vector_idx].second); s = file_->Append(GetValue(bucket.vector_idx));
} }
} }
if (!s.ok()) { if (!s.ok()) {
@ -251,7 +265,7 @@ Status CuckooTableBuilder::Finish() {
} }
assert(num_added == NumEntries()); assert(num_added == NumEntries());
properties_.raw_key_size = num_added * properties_.fixed_key_len; properties_.raw_key_size = num_added * properties_.fixed_key_len;
properties_.raw_value_size = num_added * value_length; properties_.raw_value_size = num_added * value_size_;
uint64_t offset = buckets.size() * bucket_size; uint64_t offset = buckets.size() * bucket_size;
properties_.data_size = offset; properties_.data_size = offset;
@ -330,19 +344,18 @@ void CuckooTableBuilder::Abandon() {
} }
uint64_t CuckooTableBuilder::NumEntries() const { uint64_t CuckooTableBuilder::NumEntries() const {
return kvs_.size(); return num_entries_;
} }
uint64_t CuckooTableBuilder::FileSize() const { uint64_t CuckooTableBuilder::FileSize() const {
if (closed_) { if (closed_) {
return file_->GetFileSize(); return file_->GetFileSize();
} else if (kvs_.size() == 0) { } else if (num_entries_ == 0) {
return 0; return 0;
} }
if (use_module_hash_) { if (use_module_hash_) {
return (kvs_[0].first.size() + kvs_[0].second.size()) * kvs_.size() / return (key_size_ + value_size_) * num_entries_ / max_hash_table_ratio_;
max_hash_table_ratio_;
} else { } else {
// Account for buckets being a power of two. // Account for buckets being a power of two.
// As elements are added, file size remains constant for a while and // As elements are added, file size remains constant for a while and
@ -350,11 +363,10 @@ uint64_t CuckooTableBuilder::FileSize() const {
// only after it exceeds the file limit, we account for the extra element // only after it exceeds the file limit, we account for the extra element
// being added here. // being added here.
uint64_t expected_hash_table_size = hash_table_size_; uint64_t expected_hash_table_size = hash_table_size_;
if (expected_hash_table_size < (kvs_.size() + 1) / max_hash_table_ratio_) { if (expected_hash_table_size < (num_entries_ + 1) / max_hash_table_ratio_) {
expected_hash_table_size *= 2; expected_hash_table_size *= 2;
} }
return (kvs_[0].first.size() + kvs_[0].second.size()) * return (key_size_ + value_size_) * expected_hash_table_size - 1;
expected_hash_table_size - 1;
} }
} }
@ -390,7 +402,7 @@ bool CuckooTableBuilder::MakeSpaceForKey(
// of the method. We store this number into the nodes that we explore in // of the method. We store this number into the nodes that we explore in
// current method call. // current method call.
// It is unlikely for the increment operation to overflow because the maximum // It is unlikely for the increment operation to overflow because the maximum
// no. of times this will be called is <= max_num_hash_func_ + kvs_.size(). // no. of times this will be called is <= max_num_hash_func_ + num_entries_.
for (uint32_t hash_cnt = 0; hash_cnt < num_hash_func_; ++hash_cnt) { for (uint32_t hash_cnt = 0; hash_cnt < num_hash_func_; ++hash_cnt) {
uint64_t bucket_id = hash_vals[hash_cnt]; uint64_t bucket_id = hash_vals[hash_cnt];
(*buckets)[bucket_id].make_space_for_key_call_id = (*buckets)[bucket_id].make_space_for_key_call_id =
@ -408,9 +420,7 @@ bool CuckooTableBuilder::MakeSpaceForKey(
CuckooBucket& curr_bucket = (*buckets)[curr_node.bucket_id]; CuckooBucket& curr_bucket = (*buckets)[curr_node.bucket_id];
for (uint32_t hash_cnt = 0; for (uint32_t hash_cnt = 0;
hash_cnt < num_hash_func_ && !null_found; ++hash_cnt) { hash_cnt < num_hash_func_ && !null_found; ++hash_cnt) {
uint64_t child_bucket_id = CuckooHash( uint64_t child_bucket_id = CuckooHash(GetUserKey(curr_bucket.vector_idx),
(is_last_level_file_ ? kvs_[curr_bucket.vector_idx].first :
ExtractUserKey(Slice(kvs_[curr_bucket.vector_idx].first))),
hash_cnt, use_module_hash_, hash_table_size_, identity_as_first_hash_, hash_cnt, use_module_hash_, hash_table_size_, identity_as_first_hash_,
get_slice_hash_); get_slice_hash_);
// Iterate inside Cuckoo Block. // Iterate inside Cuckoo Block.

View File

@ -75,6 +75,10 @@ class CuckooTableBuilder: public TableBuilder {
uint64_t* bucket_id); uint64_t* bucket_id);
Status MakeHashTable(std::vector<CuckooBucket>* buckets); Status MakeHashTable(std::vector<CuckooBucket>* buckets);
inline Slice GetKey(uint64_t idx) const;
inline Slice GetUserKey(uint64_t idx) const;
inline Slice GetValue(uint64_t idx) const;
uint32_t num_hash_func_; uint32_t num_hash_func_;
WritableFile* file_; WritableFile* file_;
const double max_hash_table_ratio_; const double max_hash_table_ratio_;
@ -83,10 +87,17 @@ class CuckooTableBuilder: public TableBuilder {
const uint32_t cuckoo_block_size_; const uint32_t cuckoo_block_size_;
uint64_t hash_table_size_; uint64_t hash_table_size_;
bool is_last_level_file_; bool is_last_level_file_;
Status status_;
std::vector<std::pair<std::string, std::string>> kvs_;
TableProperties properties_;
bool has_seen_first_key_; bool has_seen_first_key_;
uint64_t key_size_;
uint64_t value_size_;
// A list of fixed-size key-value pairs concatenating into a string.
// Use GetKey(), GetUserKey(), and GetValue() to retrieve a specific
// key / value given an index
std::string kvs_;
// Number of key-value pairs stored in kvs_
uint64_t num_entries_;
Status status_;
TableProperties properties_;
const Comparator* ucomp_; const Comparator* ucomp_;
bool use_module_hash_; bool use_module_hash_;
bool identity_as_first_hash_; bool identity_as_first_hash_;