|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +#pragma once |
| 21 | + |
| 22 | +#include <algorithm> |
| 23 | +#include <cstddef> |
| 24 | +#include <cstdint> |
| 25 | +#include <cstring> |
| 26 | +#include <limits> |
| 27 | +#include <memory> |
| 28 | +#include <optional> |
| 29 | +#include <type_traits> |
| 30 | +#include <utility> |
| 31 | + |
| 32 | +#include "paimon/common/memory/bytes_utils.h" |
| 33 | +#include "paimon/fs/file_system.h" |
| 34 | +#include "paimon/macros.h" |
| 35 | +#include "paimon/result.h" |
| 36 | + |
| 37 | +namespace paimon { |
| 38 | + |
| 39 | +template <typename Key, typename Value> |
| 40 | +class DenseHashTableFileReader; |
| 41 | + |
| 42 | +/// A fixed-capacity, linear-probing hash table for already-hashed unsigned keys. |
| 43 | +/// Adapted from: |
| 44 | +/// https://github.com/alibaba/havenask/blob/main/aios/storage/indexlib/index/common/hash_table/DenseHashTable.h |
| 45 | +/// All key and value bit patterns are supported. |
| 46 | +/// Mutation requires external synchronization; mounted hash tables are read-only. |
| 47 | +/// |
| 48 | +/// The native layout is [uint64 bucket_count, uint64 key_count], followed by fixed-size |
| 49 | +/// [Key, Value] buckets and two special-key buckets. Integers use host byte order and fields have |
| 50 | +/// no padding. Dumped buffers require matching key/value types and endianness when read. This is |
| 51 | +/// a container layout, not a versioned Paimon index format. |
| 52 | +template <typename Key, typename Value> |
| 53 | +class DenseHashTable { |
| 54 | + static_assert(std::is_same_v<Key, uint32_t> || std::is_same_v<Key, uint64_t>); |
| 55 | + static_assert(std::is_integral_v<Value> && !std::is_same_v<Value, bool>); |
| 56 | + |
| 57 | + public: |
| 58 | + static Result<std::unique_ptr<DenseHashTable>> Create(uint64_t capacity, int32_t occupancy_pct, |
| 59 | + const std::shared_ptr<MemoryPool>& pool) { |
| 60 | + if (!pool) { |
| 61 | + return Status::Invalid("dense hash table requires a memory pool"); |
| 62 | + } |
| 63 | + PAIMON_ASSIGN_OR_RAISE(uint64_t bucket_count, BucketCount(capacity, occupancy_pct)); |
| 64 | + PAIMON_ASSIGN_OR_RAISE(uint64_t bytes, RequiredBytes(bucket_count)); |
| 65 | + auto hash_table = std::unique_ptr<DenseHashTable>(new DenseHashTable()); |
| 66 | + hash_table->data_ = AllocateBytesKeepingPoolAlive(bytes, pool); |
| 67 | + hash_table->header_ = Header{bucket_count, 0}; |
| 68 | + hash_table->capacity_ = capacity; |
| 69 | + hash_table->writable_ = true; |
| 70 | + hash_table->WriteHeader(); |
| 71 | + for (uint64_t index = 0; index < bucket_count; ++index) { |
| 72 | + hash_table->WriteBucket(index, Bucket{kEmptyKey, Value{}}); |
| 73 | + } |
| 74 | + hash_table->WriteBucket(bucket_count, Bucket{0, Value{}}); |
| 75 | + hash_table->WriteBucket(bucket_count + 1, Bucket{0, Value{}}); |
| 76 | + return hash_table; |
| 77 | + } |
| 78 | + |
| 79 | + /// Mounts a shared buffer without copying. Its bytes must not be mutated while mounted. |
| 80 | + /// The returned hash table retains the buffer; its allocator must also outlive the buffer. |
| 81 | + static Result<std::unique_ptr<DenseHashTable>> MountForRead(std::shared_ptr<const Bytes> data) { |
| 82 | + if (!data || data->size() < kHeaderSize) { |
| 83 | + return Status::Invalid("dense hash table buffer is shorter than its header"); |
| 84 | + } |
| 85 | + Header header; |
| 86 | + std::memcpy(&header, data->data(), kHeaderSize); |
| 87 | + PAIMON_RETURN_NOT_OK(ValidateHeader(header, data->size())); |
| 88 | + auto hash_table = std::unique_ptr<DenseHashTable>(new DenseHashTable()); |
| 89 | + hash_table->data_ = std::move(data); |
| 90 | + hash_table->header_ = header; |
| 91 | + hash_table->capacity_ = header.bucket_count + 2; |
| 92 | + for (uint64_t index = header.bucket_count; index < header.bucket_count + 2; ++index) { |
| 93 | + if (hash_table->ReadBucket(index).key > 1) { |
| 94 | + return Status::Invalid("invalid dense hash table special bucket"); |
| 95 | + } |
| 96 | + } |
| 97 | + return hash_table; |
| 98 | + } |
| 99 | + |
| 100 | + DenseHashTable(const DenseHashTable&) = delete; |
| 101 | + DenseHashTable& operator=(const DenseHashTable&) = delete; |
| 102 | + |
| 103 | + /// New keys fail once capacity is reached. Updating an existing key still succeeds. |
| 104 | + Status Insert(Key key, Value value) { |
| 105 | + if (!writable_) { |
| 106 | + return Status::Invalid("cannot insert into a read-only dense hash table"); |
| 107 | + } |
| 108 | + if (key == kEmptyKey || key == kOtherSpecialKey) { |
| 109 | + const uint64_t index = header_.bucket_count + (key == kOtherSpecialKey); |
| 110 | + return SetBucket(index, Bucket{1, value}, ReadBucket(index).key == 0); |
| 111 | + } |
| 112 | + uint64_t index = key % header_.bucket_count; |
| 113 | + for (uint64_t probe = 0; probe < header_.bucket_count; ++probe) { |
| 114 | + const Bucket bucket = ReadBucket(index); |
| 115 | + if (bucket.key == kEmptyKey || bucket.key == key) { |
| 116 | + return SetBucket(index, Bucket{key, value}, bucket.key == kEmptyKey); |
| 117 | + } |
| 118 | + index = (index + 1) % header_.bucket_count; |
| 119 | + } |
| 120 | + return Status::Invalid("dense hash table is full"); |
| 121 | + } |
| 122 | + |
| 123 | + std::optional<Value> Find(Key key) const { |
| 124 | + if (key == kEmptyKey || key == kOtherSpecialKey) { |
| 125 | + const Bucket bucket = ReadBucket(header_.bucket_count + (key == kOtherSpecialKey)); |
| 126 | + return bucket.key == 0 ? std::nullopt : std::optional<Value>(bucket.value); |
| 127 | + } |
| 128 | + uint64_t index = key % header_.bucket_count; |
| 129 | + for (uint64_t probe = 0; probe < header_.bucket_count; ++probe) { |
| 130 | + const Bucket bucket = ReadBucket(index); |
| 131 | + if (bucket.key == kEmptyKey) { |
| 132 | + return std::nullopt; |
| 133 | + } |
| 134 | + if (bucket.key == key) { |
| 135 | + return bucket.value; |
| 136 | + } |
| 137 | + index = (index + 1) % header_.bucket_count; |
| 138 | + } |
| 139 | + return std::nullopt; |
| 140 | + } |
| 141 | + |
| 142 | + /// Writes the container layout at the stream's current position. The caller owns flush/close. |
| 143 | + Status WriteTo(OutputStream* output) const { |
| 144 | + if (!output) { |
| 145 | + return Status::Invalid("dense hash table output stream is null"); |
| 146 | + } |
| 147 | + const int64_t length = static_cast<int64_t>(data_->size()); |
| 148 | + PAIMON_ASSIGN_OR_RAISE(int64_t written, output->Write(data_->data(), length)); |
| 149 | + if (written != length) { |
| 150 | + return Status::IOError("short write for dense hash table"); |
| 151 | + } |
| 152 | + return Status::OK(); |
| 153 | + } |
| 154 | + |
| 155 | + uint64_t Size() const { |
| 156 | + return header_.key_count; |
| 157 | + } |
| 158 | + |
| 159 | + /// Returns the writable key limit, or the physical bucket limit for a read-only mount. |
| 160 | + uint64_t Capacity() const { |
| 161 | + return capacity_; |
| 162 | + } |
| 163 | + |
| 164 | + uint64_t MemoryUsage() const { |
| 165 | + return data_->size(); |
| 166 | + } |
| 167 | + |
| 168 | + private: |
| 169 | + friend class DenseHashTableFileReader<Key, Value>; |
| 170 | + |
| 171 | + struct Header { |
| 172 | + uint64_t bucket_count; |
| 173 | + uint64_t key_count; |
| 174 | + }; |
| 175 | + |
| 176 | + struct Bucket { |
| 177 | + Key key; |
| 178 | + Value value; |
| 179 | + }; |
| 180 | + |
| 181 | + static constexpr Key kEmptyKey = static_cast<Key>(0xFFFFFFFFFFFFFEFFULL); |
| 182 | + static constexpr Key kOtherSpecialKey = static_cast<Key>(0xFFFFFFFFFFFFFDFFULL); |
| 183 | + static constexpr uint64_t kHeaderSize = sizeof(Header); |
| 184 | + static constexpr uint64_t kBucketSize = sizeof(Key) + sizeof(Value); |
| 185 | + static constexpr uint64_t kMaxBytes = std::numeric_limits<int64_t>::max(); |
| 186 | + |
| 187 | + DenseHashTable() = default; |
| 188 | + |
| 189 | + static Result<uint64_t> BucketCount(uint64_t capacity, int32_t occupancy_pct) { |
| 190 | + if (occupancy_pct <= 0 || occupancy_pct > 80) { |
| 191 | + return Status::Invalid("dense hash table occupancy must be between 1 and 80"); |
| 192 | + } |
| 193 | + const uint64_t quotient = capacity / occupancy_pct; |
| 194 | + if (quotient > kMaxBytes / 100) { |
| 195 | + return Status::Invalid("dense hash table capacity overflow"); |
| 196 | + } |
| 197 | + const uint64_t remainder = |
| 198 | + (capacity % occupancy_pct * 100 + occupancy_pct - 1) / occupancy_pct; |
| 199 | + return std::max<uint64_t>(1, quotient * 100 + remainder); |
| 200 | + } |
| 201 | + |
| 202 | + static Result<uint64_t> RequiredBytes(uint64_t bucket_count) { |
| 203 | + if (bucket_count == 0 || bucket_count > (kMaxBytes - kHeaderSize) / kBucketSize - 2) { |
| 204 | + return Status::Invalid("invalid dense hash table bucket count"); |
| 205 | + } |
| 206 | + return kHeaderSize + (bucket_count + 2) * kBucketSize; |
| 207 | + } |
| 208 | + |
| 209 | + static Status ValidateHeader(const Header& header, uint64_t length) { |
| 210 | + PAIMON_ASSIGN_OR_RAISE(uint64_t bytes, RequiredBytes(header.bucket_count)); |
| 211 | + if (header.key_count > header.bucket_count + 2 || bytes != length) { |
| 212 | + return Status::Invalid( |
| 213 | + "dense hash table header does not match its length or key count"); |
| 214 | + } |
| 215 | + return Status::OK(); |
| 216 | + } |
| 217 | + |
| 218 | + static Bucket DecodeBucket(const char* data) { |
| 219 | + Bucket bucket{}; |
| 220 | + std::memcpy(&bucket.key, data, sizeof(Key)); |
| 221 | + std::memcpy(&bucket.value, data + sizeof(Key), sizeof(Value)); |
| 222 | + return bucket; |
| 223 | + } |
| 224 | + |
| 225 | + Bucket ReadBucket(uint64_t index) const { |
| 226 | + return DecodeBucket(data_->data() + kHeaderSize + index * kBucketSize); |
| 227 | + } |
| 228 | + |
| 229 | + void WriteBucket(uint64_t index, const Bucket& bucket) { |
| 230 | + char* data = data_->data() + kHeaderSize + index * kBucketSize; |
| 231 | + std::memcpy(data, &bucket.key, sizeof(Key)); |
| 232 | + std::memcpy(data + sizeof(Key), &bucket.value, sizeof(Value)); |
| 233 | + } |
| 234 | + |
| 235 | + void WriteHeader() { |
| 236 | + std::memcpy(data_->data(), &header_, kHeaderSize); |
| 237 | + } |
| 238 | + |
| 239 | + Status SetBucket(uint64_t index, const Bucket& bucket, bool is_new) { |
| 240 | + if (is_new && Size() >= capacity_) { |
| 241 | + return Status::Invalid("dense hash table is full"); |
| 242 | + } |
| 243 | + WriteBucket(index, bucket); |
| 244 | + if (is_new) { |
| 245 | + ++header_.key_count; |
| 246 | + WriteHeader(); |
| 247 | + } |
| 248 | + return Status::OK(); |
| 249 | + } |
| 250 | + |
| 251 | + std::shared_ptr<const Bytes> data_; |
| 252 | + Header header_{}; |
| 253 | + uint64_t capacity_ = 0; |
| 254 | + bool writable_ = false; |
| 255 | +}; |
| 256 | + |
| 257 | +} // namespace paimon |
0 commit comments