| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| #include "runtime/bufferpool/suballocator.h" | |
| #include <new> | |
| #include "runtime/bufferpool/reservation-tracker.h" | |
| #include "util/bit-util.h" | |
| #include "common/names.h" | |
| namespace impala { | |
| constexpr int Suballocator::LOG_MAX_ALLOCATION_BYTES; | |
| constexpr int64_t Suballocator::MAX_ALLOCATION_BYTES; | |
| constexpr int Suballocator::LOG_MIN_ALLOCATION_BYTES; | |
| constexpr int64_t Suballocator::MIN_ALLOCATION_BYTES; | |
| const int Suballocator::NUM_FREE_LISTS; | |
| Suballocator::Suballocator( | |
| BufferPool* pool, BufferPool::ClientHandle* client, int64_t min_buffer_len) | |
| : pool_(pool), client_(client), min_buffer_len_(min_buffer_len), allocated_(0) {} | |
| Suballocator::~Suballocator() { | |
| // All allocations should be free and buffers deallocated. | |
| DCHECK_EQ(allocated_, 0); | |
| for (int i = 0; i < NUM_FREE_LISTS; ++i) { | |
| DCHECK(free_lists_[i] == nullptr); | |
| } | |
| } | |
| Status Suballocator::Allocate(int64_t bytes, unique_ptr<Suballocation>* result) { | |
| DCHECK_GE(bytes, 0); | |
| if (UNLIKELY(bytes > MAX_ALLOCATION_BYTES)) { | |
| return Status(Substitute("Requested memory allocation of $0 bytes, larger than max " | |
| "supported of $1 bytes", | |
| bytes, MAX_ALLOCATION_BYTES)); | |
| } | |
| unique_ptr<Suballocation> free_node; | |
| bytes = max(bytes, MIN_ALLOCATION_BYTES); | |
| const int target_list_idx = ComputeListIndex(bytes); | |
| for (int i = target_list_idx; i < NUM_FREE_LISTS; ++i) { | |
| free_node = PopFreeListHead(i); | |
| if (free_node != nullptr) break; | |
| } | |
| if (free_node == nullptr) { | |
| // Unable to find free allocation, need to get more memory from buffer pool. | |
| RETURN_IF_ERROR(AllocateBuffer(bytes, &free_node)); | |
| if (free_node == nullptr) { | |
| *result = nullptr; | |
| return Status::OK(); | |
| } | |
| } | |
| // Free node may be larger than required. | |
| const int free_list_idx = ComputeListIndex(free_node->len_); | |
| if (free_list_idx != target_list_idx) { | |
| RETURN_IF_ERROR(SplitToSize(move(free_node), bytes, &free_node)); | |
| DCHECK(free_node != nullptr); | |
| } | |
| free_node->in_use_ = true; | |
| allocated_ += free_node->len_; | |
| *result = move(free_node); | |
| return Status::OK(); | |
| } | |
| int Suballocator::ComputeListIndex(int64_t bytes) const { | |
| return BitUtil::Log2CeilingNonZero64(bytes) - LOG_MIN_ALLOCATION_BYTES; | |
| } | |
| Status Suballocator::AllocateBuffer(int64_t bytes, unique_ptr<Suballocation>* result) { | |
| DCHECK_LE(bytes, MAX_ALLOCATION_BYTES); | |
| const int64_t buffer_len = max(min_buffer_len_, BitUtil::RoundUpToPowerOfTwo(bytes)); | |
| if (!client_->IncreaseReservationToFit(buffer_len)) { | |
| *result = nullptr; | |
| return Status::OK(); | |
| } | |
| unique_ptr<Suballocation> free_node; | |
| RETURN_IF_ERROR(Suballocation::Create(&free_node)); | |
| RETURN_IF_ERROR(pool_->AllocateBuffer(client_, buffer_len, &free_node->buffer_)); | |
| free_node->data_ = free_node->buffer_.data(); | |
| free_node->len_ = buffer_len; | |
| *result = move(free_node); | |
| return Status::OK(); | |
| } | |
| Status Suballocator::SplitToSize(unique_ptr<Suballocation> free_node, | |
| int64_t target_bytes, unique_ptr<Suballocation>* result) { | |
| DCHECK(!free_node->in_use_); | |
| DCHECK_GT(free_node->len_, target_bytes); | |
| const int free_list_idx = ComputeListIndex(free_node->len_); | |
| const int target_list_idx = ComputeListIndex(target_bytes); | |
| // Preallocate nodes to avoid handling allocation failures during splitting. | |
| // Need two nodes per level for the left and right children. | |
| const int num_nodes = (free_list_idx - target_list_idx) * 2; | |
| constexpr int MAX_NUM_NODES = NUM_FREE_LISTS * 2; | |
| unique_ptr<Suballocation> nodes[MAX_NUM_NODES]; | |
| for (int i = 0; i < num_nodes; ++i) { | |
| if (!Suballocation::Create(&nodes[i]).ok()) { | |
| // Add the free node to the free list to restore the allocator to an internally | |
| // consistent state. | |
| AddToFreeList(move(free_node)); | |
| return Status("Failed to allocate list node in Suballocator"); | |
| } | |
| } | |
| // Iteratively split from the current size down to the target size. We will return | |
| // the leftmost descendant node. | |
| int next_node = 0; | |
| for (int i = free_list_idx; i > target_list_idx; --i) { | |
| DCHECK_EQ(free_node->len_, 1LL << (i + LOG_MIN_ALLOCATION_BYTES)); | |
| unique_ptr<Suballocation> left_child = move(nodes[next_node++]); | |
| unique_ptr<Suballocation> right_child = move(nodes[next_node++]); | |
| DCHECK_LE(next_node, num_nodes); | |
| const int64_t child_len = free_node->len_ / 2; | |
| left_child->data_ = free_node->data_; | |
| right_child->data_ = free_node->data_ + child_len; | |
| left_child->len_ = right_child->len_ = child_len; | |
| left_child->buddy_ = right_child.get(); | |
| right_child->buddy_ = left_child.get(); | |
| free_node->in_use_ = true; | |
| left_child->parent_ = move(free_node); | |
| AddToFreeList(move(right_child)); | |
| free_node = move(left_child); | |
| } | |
| *result = move(free_node); | |
| return Status::OK(); | |
| } | |
| void Suballocator::Free(unique_ptr<Suballocation> allocation) { | |
| if (allocation == nullptr) return; | |
| DCHECK(allocation->in_use_); | |
| allocation->in_use_ = false; | |
| allocated_ -= allocation->len_; | |
| // Iteratively coalesce buddies until the buddy is in use or we get to the root. | |
| // This ensures that all buddies in the free lists are coalesced. I.e. we do not | |
| // have two buddies in the same free list. | |
| unique_ptr<Suballocation> curr_allocation = move(allocation); | |
| while (curr_allocation->buddy_ != nullptr) { | |
| if (curr_allocation->buddy_->in_use_) { | |
| // If the buddy is not free we can't coalesce, just add it to free list. | |
| AddToFreeList(move(curr_allocation)); | |
| return; | |
| } | |
| unique_ptr<Suballocation> buddy = RemoveFromFreeList(curr_allocation->buddy_); | |
| curr_allocation = CoalesceBuddies(move(curr_allocation), move(buddy)); | |
| } | |
| // Reached root, which is an entire free buffer. We are not using it, so free up memory. | |
| DCHECK(curr_allocation->buffer_.is_open()); | |
| pool_->FreeBuffer(client_, &curr_allocation->buffer_); | |
| curr_allocation.reset(); | |
| } | |
| void Suballocator::AddToFreeList(unique_ptr<Suballocation> node) { | |
| DCHECK(!node->in_use_); | |
| int list_idx = ComputeListIndex(node->len_); | |
| if (free_lists_[list_idx] != nullptr) { | |
| free_lists_[list_idx]->prev_free_ = node.get(); | |
| } | |
| node->next_free_ = move(free_lists_[list_idx]); | |
| DCHECK(node->prev_free_ == nullptr); | |
| free_lists_[list_idx] = move(node); | |
| } | |
| unique_ptr<Suballocation> Suballocator::RemoveFromFreeList(Suballocation* node) { | |
| DCHECK(node != nullptr); | |
| const int list_idx = ComputeListIndex(node->len_); | |
| if (node->next_free_ != nullptr) { | |
| node->next_free_->prev_free_ = node->prev_free_; | |
| } | |
| unique_ptr<Suballocation>* ptr_from_prev = node->prev_free_ == nullptr ? | |
| &free_lists_[list_idx] : | |
| &node->prev_free_->next_free_; | |
| node->prev_free_ = nullptr; | |
| unique_ptr<Suballocation> result = move(*ptr_from_prev); | |
| *ptr_from_prev = move(node->next_free_); | |
| return result; | |
| } | |
| unique_ptr<Suballocation> Suballocator::PopFreeListHead(int list_idx) { | |
| if (free_lists_[list_idx] == nullptr) return nullptr; | |
| unique_ptr<Suballocation> result = move(free_lists_[list_idx]); | |
| DCHECK(result->prev_free_ == nullptr); | |
| if (result->next_free_ != nullptr) { | |
| result->next_free_->prev_free_ = nullptr; | |
| } | |
| free_lists_[list_idx] = move(result->next_free_); | |
| return result; | |
| } | |
| unique_ptr<Suballocation> Suballocator::CoalesceBuddies( | |
| unique_ptr<Suballocation> b1, unique_ptr<Suballocation> b2) { | |
| DCHECK(!b1->in_use_); | |
| DCHECK(!b2->in_use_); | |
| DCHECK_EQ(b1->buddy_, b2.get()); | |
| DCHECK_EQ(b2->buddy_, b1.get()); | |
| // Only the left child's parent should be present. | |
| DCHECK((b1->parent_ != nullptr) ^ (b2->parent_ != nullptr)); | |
| unique_ptr<Suballocation> parent = | |
| b1->parent_ != nullptr ? move(b1->parent_) : move(b2->parent_); | |
| parent->in_use_ = false; | |
| return parent; | |
| } | |
| Status Suballocation::Create(unique_ptr<Suballocation>* new_suballocation) { | |
| // Allocate from system allocator for simplicity. We don't expect this to be | |
| // performance critical or to be used for small allocations where CPU/memory | |
| // overhead of these allocations might be a consideration. | |
| new_suballocation->reset(new (nothrow) Suballocation()); | |
| if (*new_suballocation == nullptr) { | |
| return Status(TErrorCode::MEM_ALLOC_FAILED, sizeof(Suballocation)); | |
| } | |
| return Status::OK(); | |
| } | |
| } |