rocksdb/table/data_block_hash_index.h
Fenggang Wu 19ec44fd39 Improve point-lookup performance using a data block hash index (#4174)
Summary:
Add hash index support to data blocks, which helps to reduce the CPU utilization of point-lookup operations. This feature is backward compatible with the data block created without the hash index. It is disabled by default unless `BlockBasedTableOptions::data_block_index_type` is set to `data_block_index_type = kDataBlockBinaryAndHash.`

The DB size would be bigger with the hash index option as a hash table is added at the end of each data block. If the hash utilization ratio is 1:1, the space overhead is one byte per key. The hash table utilization ratio is adjustable using `BlockBasedTableOptions::data_block_hash_table_util_ratio`. A lower utilization ratio will improve more on the point-lookup efficiency, but take more space too.
Pull Request resolved: https://github.com/facebook/rocksdb/pull/4174

Differential Revision: D8965914

Pulled By: fgwu

fbshipit-source-id: 1c6bae5d1fc39c80282d8890a72e9e67bc247198
2018-08-15 14:30:03 -07:00

133 lines
4.8 KiB
C++

// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include <string>
#include <vector>
#include "rocksdb/slice.h"
namespace rocksdb {
// This is an experimental feature aiming to reduce the CPU utilization of
// point-lookup within a data-block. It is only used in data blocks, and not
// in meta-data blocks or per-table index blocks.
//
// It only used to support BlockBasedTable::Get().
//
// A serialized hash index is appended to the data-block. The new block data
// format is as follows:
//
// DATA_BLOCK: [RI RI RI ... RI RI_IDX HASH_IDX FOOTER]
//
// RI: Restart Interval (the same as the default data-block format)
// RI_IDX: Restart Interval index (the same as the default data-block format)
// HASH_IDX: The new data-block hash index feature.
// FOOTER: A 32bit block footer, which is the NUM_RESTARTS with the MSB as
// the flag indicating if this hash index is in use. Note that
// given a data block < 32KB, the MSB is never used. So we can
// borrow the MSB as the hash index flag. Therefore, this format is
// compatible with the legacy data-blocks with num_restarts < 32768,
// as the MSB is 0.
//
// The format of the data-block hash index is as follows:
//
// HASH_IDX: [B B B ... B NUM_BUCK]
//
// B: bucket, an array of restart index. Each buckets is uint8_t.
// NUM_BUCK: Number of buckets, which is the length of the bucket array.
//
// We reserve two special flag:
// kNoEntry=255,
// kCollision=254.
//
// Therefore, the max number of restarts this hash index can supoport is 253.
//
// Buckets are initialized to be kNoEntry.
//
// When storing a key in the hash index, the key is first hashed to a bucket.
// If there the bucket is empty (kNoEntry), the restart index is stored in
// the bucket. If there is already a restart index there, we will update the
// existing restart index to a collision marker (kCollision). If the
// the bucket is already marked as collision, we do not store the restart
// index either.
//
// During query process, a key is first hashed to a bucket. Then we examine if
// the buckets store nothing (kNoEntry) or the bucket had a collision
// (kCollision). If either of those happens, we get the restart index of
// the key and will directly go to the restart interval to search the key.
//
// Note that we only support blocks with #restart_interval < 254. If a block
// has more restart interval than that, hash index will not be create for it.
const uint8_t kNoEntry = 255;
const uint8_t kCollision = 254;
const uint8_t kMaxRestartSupportedByHashIndex = 253;
// Because we use uint16_t address, we only support block no more than 64KB
const size_t kMaxBlockSizeSupportedByHashIndex = 1u << 16;
const double kDefaultUtilRatio = 0.75;
class DataBlockHashIndexBuilder {
public:
DataBlockHashIndexBuilder() : util_ratio_(0), valid_(false) {}
void Initialize(double util_ratio) {
if (util_ratio <= 0) {
util_ratio = kDefaultUtilRatio; // sanity check
}
util_ratio_ = util_ratio;
valid_ = true;
}
inline bool Valid() const { return valid_ && util_ratio_ > 0; }
void Add(const Slice& key, const size_t restart_index);
void Finish(std::string& buffer);
void Reset();
inline size_t EstimateSize() const {
uint16_t estimated_num_buckets = static_cast<uint16_t>(
static_cast<double>(hash_and_restart_pairs_.size()) / util_ratio_);
// Maching the num_buckets number in DataBlockHashIndexBuilder::Finish.
estimated_num_buckets |= 1;
return sizeof(uint16_t) +
static_cast<size_t>(estimated_num_buckets * sizeof(uint8_t));
}
private:
double util_ratio_;
// Now the only usage for `valid_` is to mark false when the inserted
// restart_index is larger than supported. In this case HashIndex is not
// appended to the block content.
bool valid_;
std::vector<std::pair<uint32_t, uint8_t>> hash_and_restart_pairs_;
friend class DataBlockHashIndex_DataBlockHashTestSmall_Test;
};
class DataBlockHashIndex {
public:
DataBlockHashIndex() : num_buckets_(0) {}
void Initialize(const char* data, uint16_t size, uint16_t* map_offset);
uint8_t Lookup(const char* data, uint32_t map_offset, const Slice& key) const;
inline bool Valid() { return num_buckets_ != 0; }
private:
// To make the serialized hash index compact and to save the space overhead,
// here all the data fields persisted in the block are in uint16 format.
// We find that a uint16 is large enough to index every offset of a 64KiB
// block.
// So in other words, DataBlockHashIndex does not support block size equal
// or greater then 64KiB.
uint16_t num_buckets_;
};
} // namespace rocksdb