blob: 620b9e229b75d571da660a9fc11cddb4f7c03248 [file]
// 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 "exec/topn-node.h"
#include <algorithm>
#include <sstream>
#include "codegen/llvm-codegen.h"
#include "exec/exec-node-util.h"
#include "exprs/scalar-expr-evaluator.h"
#include "exprs/scalar-expr.h"
#include "exprs/slot-ref.h"
#include "runtime/descriptors.h"
#include "runtime/fragment-state.h"
#include "runtime/mem-pool.h"
#include "runtime/mem-tracker.h"
#include "runtime/row-batch.h"
#include "runtime/runtime-state.h"
#include "runtime/sorter.h"
#include "runtime/sorter-internal.h" // For TupleSorter
#include "runtime/tuple-row.h"
#include "runtime/tuple.h"
#include "util/debug-util.h"
#include "util/runtime-profile-counters.h"
#include "util/tuple-row-compare.h"
#include "gen-cpp/Exprs_types.h"
#include "gen-cpp/PlanNodes_types.h"
#include "common/names.h"
using namespace impala;
// Soft limit on the number of partitions in an instance of a partitioned top-n
// node to avoid potential scalability problems with a large number of heaps in
// the std::map.
DEFINE_int32(partitioned_topn_in_mem_partitions_limit, 1000, "(Experimental) Soft limit "
"on the number of in-memory partitions in an instance of the partitioned top-n "
"operator.");
// Soft limit on the aggregate size of heaps. If heaps exceed this, we will evict some
// from memory.
DEFINE_int64(partitioned_topn_soft_limit_bytes, 64L * 1024L * 1024L, "(Experimental) "
"Soft limit on the number of in-memory partitions in an instance of the "
"partitioned top-n operator.");
Status TopNPlanNode::Init(const TPlanNode& tnode, FragmentState* state) {
const TSortInfo& tsort_info = tnode.sort_node.sort_info;
RETURN_IF_ERROR(PlanNode::Init(tnode, state));
RETURN_IF_ERROR(ScalarExpr::Create(
tsort_info.ordering_exprs, *row_descriptor_, state, &ordering_exprs_));
DCHECK(tsort_info.__isset.sort_tuple_slot_exprs);
output_tuple_desc_ = row_descriptor_->tuple_descriptors()[0];
RETURN_IF_ERROR(ScalarExpr::Create(tsort_info.sort_tuple_slot_exprs,
*children_[0]->row_descriptor_, state, &output_tuple_exprs_));
ordering_comparator_config_ =
state->obj_pool()->Add(new TupleRowComparatorConfig(tsort_info, ordering_exprs_));
if (is_partitioned()) {
DCHECK(tnode.sort_node.__isset.partition_exprs);
RETURN_IF_ERROR(ScalarExpr::Create(
tnode.sort_node.partition_exprs, *row_descriptor_, state, &partition_exprs_));
// We need a TSortInfo for internal use in the sorted map. Initialize with
// arbitrary parameters.
TSortInfo* tpartition_sort_info = state->obj_pool()->Add(new TSortInfo);
tpartition_sort_info->sorting_order = TSortingOrder::LEXICAL;
tpartition_sort_info->is_asc_order.resize(partition_exprs_.size(), true);
tpartition_sort_info->nulls_first.resize(partition_exprs_.size(), false);
partition_comparator_config_ = state->obj_pool()->Add(
new TupleRowComparatorConfig(*tpartition_sort_info, partition_exprs_));
DCHECK(tnode.sort_node.__isset.intra_partition_sort_info);
const TSortInfo& intra_part_sort_info = tnode.sort_node.intra_partition_sort_info;
// Set up the intra-partition comparator.
RETURN_IF_ERROR(ScalarExpr::Create(intra_part_sort_info.ordering_exprs,
*row_descriptor_, state, &intra_partition_ordering_exprs_));
intra_partition_comparator_config_ =
state->obj_pool()->Add(new TupleRowComparatorConfig(
intra_part_sort_info, intra_partition_ordering_exprs_));
// Construct SlotRefs that simply copy the output tuple to itself.
for (const SlotDescriptor* slot_desc : output_tuple_desc_->slots()) {
SlotRef* slot_ref = state->obj_pool()->Add(SlotRef::TypeSafeCreate(slot_desc));
noop_tuple_exprs_.push_back(slot_ref);
RETURN_IF_ERROR(slot_ref->Init(*row_descriptor_, true, state));
}
}
DCHECK_EQ(conjuncts_.size(), 0) << "TopNNode should never have predicates to evaluate.";
state->CheckAndAddCodegenDisabledMessage(codegen_status_msgs_);
return Status::OK();
}
void TopNPlanNode::Close() {
ScalarExpr::Close(ordering_exprs_);
ScalarExpr::Close(partition_exprs_);
ScalarExpr::Close(intra_partition_ordering_exprs_);
ScalarExpr::Close(output_tuple_exprs_);
ScalarExpr::Close(noop_tuple_exprs_);
PlanNode::Close();
}
Status TopNPlanNode::CreateExecNode(RuntimeState* state, ExecNode** node) const {
ObjectPool* pool = state->obj_pool();
*node = pool->Add(new TopNNode(pool, *this, state->desc_tbl()));
return Status::OK();
}
/// In the TopNNode constructor if 'pnode.partition_comparator_config_' is NULL, we use
/// this dummy comparator to avoid 'partition_cmp_' becoming a null pointer. This is
/// needed because 'partition_cmp_' is wrapped in a
/// 'ComparatorWrapper<TupleRowComparator>' that takes a reference to the underlying
/// comparator, so we cannot pass in a null pointer or dereference it.
class DummyTupleRowComparator: public TupleRowComparator {
public:
DummyTupleRowComparator()
: TupleRowComparator(dummy_scalar_exprs_, dummy_codegend_compare_fn_) {
}
private:
static const std::vector<ScalarExpr*> dummy_scalar_exprs_;
static const CodegenFnPtr<TupleRowComparatorConfig::CompareFn>
dummy_codegend_compare_fn_;
int CompareInterpreted(const TupleRow* lhs, const TupleRow* rhs) const override {
// This function should never be called as this is a dummy comparator.
DCHECK(false);
return std::less<const TupleRow*>{}(lhs, rhs);
}
};
/// Initialise vector length to 0 so no buffer needs to be allocated.
const std::vector<ScalarExpr*> DummyTupleRowComparator::dummy_scalar_exprs_{0};
const CodegenFnPtr<TupleRowComparatorConfig::CompareFn>
DummyTupleRowComparator::dummy_codegend_compare_fn_{};
TopNNode::TopNNode(
ObjectPool* pool, const TopNPlanNode& pnode, const DescriptorTbl& descs)
: ExecNode(pool, pnode, descs),
offset_(pnode.offset()),
output_tuple_exprs_(pnode.output_tuple_exprs_),
output_tuple_desc_(pnode.output_tuple_desc_),
order_cmp_(new TupleRowLexicalComparator(*pnode.ordering_comparator_config_)),
partition_cmp_(pnode.partition_comparator_config_ == nullptr ?
static_cast<TupleRowComparator*>(new DummyTupleRowComparator()) :
new TupleRowLexicalComparator(*pnode.partition_comparator_config_)),
intra_partition_order_cmp_(pnode.intra_partition_comparator_config_ == nullptr ?
nullptr :
new TupleRowLexicalComparator(*pnode.intra_partition_comparator_config_)),
tuple_pool_(nullptr),
codegend_insert_batch_fn_(pnode.codegend_insert_batch_fn_),
partition_heaps_(ComparatorWrapper<TupleRowComparator>(*partition_cmp_)) {
runtime_profile()->AddInfoString("SortType", "TopN");
}
Status TopNNode::Prepare(RuntimeState* state) {
SCOPED_TIMER(runtime_profile_->total_time_counter());
DCHECK(output_tuple_desc_ != nullptr);
RETURN_IF_ERROR(ExecNode::Prepare(state));
tuple_pool_.reset(new MemPool(mem_tracker()));
RETURN_IF_ERROR(ScalarExprEvaluator::Create(output_tuple_exprs_, state, pool_,
expr_perm_pool(), expr_results_pool(), &output_tuple_expr_evals_));
insert_batch_timer_ = ADD_TIMER(runtime_profile(), "InsertBatchTime");
tuple_pool_reclaim_counter_ = ADD_COUNTER(runtime_profile(), "TuplePoolReclamations",
TUnit::UNIT);
if (is_partitioned()) {
num_partitions_counter_ = ADD_COUNTER(runtime_profile(), "NumPartitions",
TUnit::UNIT);
in_mem_heap_created_counter_ = ADD_COUNTER(runtime_profile(), "InMemoryHeapsCreated",
TUnit::UNIT);
in_mem_heap_evicted_counter_ = ADD_COUNTER(runtime_profile(), "InMemoryHeapsEvicted",
TUnit::UNIT);
in_mem_heap_rows_filtered_counter_ = ADD_COUNTER(runtime_profile(),
"InMemoryHeapsRowsFiltered", TUnit::UNIT);
}
// Set up heaps and sorters for the partitioned and non-partitioned cases.
const TopNPlanNode& pnode = static_cast<const TopNPlanNode&>(plan_node_);
if (is_partitioned()) {
DCHECK_GT(per_partition_limit(), 0)
<< "Planner should not generate partitioned top-n with 0 limit";
// Partitioned Top-N needs the external sorter.
sorter_.reset(new Sorter(*pnode.ordering_comparator_config_, pnode.noop_tuple_exprs_,
&row_descriptor_, mem_tracker(), buffer_pool_client(),
resource_profile_.spillable_buffer_size, runtime_profile(), state, label(),
true, pnode.codegend_sort_helper_fn_));
RETURN_IF_ERROR(sorter_->Prepare(pool_));
DCHECK_GE(resource_profile_.min_reservation, sorter_->ComputeMinReservation());
} else {
heap_.reset(new Heap(*order_cmp_, pnode.heap_capacity(), pnode.include_ties()));
}
return Status::OK();
}
void TopNPlanNode::Codegen(FragmentState* state) {
DCHECK(state->ShouldCodegen());
PlanNode::Codegen(state);
if (IsNodeCodegenDisabled()) return;
LlvmCodeGen* codegen = state->codegen();
DCHECK(codegen != NULL);
llvm::Function* compare_fn = nullptr;
llvm::Function* intra_partition_compare_fn = nullptr;
Status codegen_status = ordering_comparator_config_->Codegen(state, &compare_fn);
if (codegen_status.ok() && is_partitioned()) {
codegen_status =
Sorter::TupleSorter::Codegen(state, compare_fn, output_tuple_desc_->byte_size(),
&codegend_sort_helper_fn_);
}
if (codegen_status.ok() && is_partitioned()) {
// TODO: IMPALA-10228: replace comparisons in std::map.
codegen_status = partition_comparator_config_->Codegen(state);
}
if (codegen_status.ok() && is_partitioned()) {
codegen_status =
intra_partition_comparator_config_->Codegen(state, &intra_partition_compare_fn);
}
if (codegen_status.ok()) {
llvm::Function* insert_batch_fn = codegen->GetFunction(is_partitioned() ?
IRFunction::TOPN_NODE_INSERT_BATCH_PARTITIONED :
IRFunction::TOPN_NODE_INSERT_BATCH_UNPARTITIONED, true);
DCHECK(insert_batch_fn != NULL);
// Generate two MaterializeExprs() functions, one with no pool that
// does a shallow copy (used in partitioned and unpartitioned modes) and
// one with 'tuple_pool_' that does a deep copy of the data.
DCHECK(output_tuple_desc_ != NULL);
llvm::Function* materialize_exprs_tuple_pool_fn = nullptr;
llvm::Function* materialize_exprs_no_pool_fn = nullptr;
if (!is_partitioned()) {
codegen_status = Tuple::CodegenMaterializeExprs(codegen, false,
*output_tuple_desc_, output_tuple_exprs_,
true, &materialize_exprs_tuple_pool_fn);
}
if (codegen_status.ok()) {
codegen_status = Tuple::CodegenMaterializeExprs(codegen, false,
*output_tuple_desc_, output_tuple_exprs_,
false, &materialize_exprs_no_pool_fn);
}
if (codegen_status.ok()) {
int replaced;
if (!is_partitioned()) {
replaced = codegen->ReplaceCallSites(insert_batch_fn,
materialize_exprs_tuple_pool_fn, Tuple::MATERIALIZE_EXPRS_SYMBOL);
DCHECK_REPLACE_COUNT(replaced, 1) << LlvmCodeGen::Print(insert_batch_fn);
}
replaced = codegen->ReplaceCallSites(insert_batch_fn,
materialize_exprs_no_pool_fn, Tuple::MATERIALIZE_EXPRS_NULL_POOL_SYMBOL);
DCHECK_REPLACE_COUNT(replaced, 1) << LlvmCodeGen::Print(insert_batch_fn);
if (is_partitioned()) {
// The total number of calls to tuple_row_less_than_->Compare() is 3 in
// PriorityQueue (called from 2 places), 1 in
// TopNNode::Heap::InsertMaterializedTuple() and 3 in
// TopNNode::Heap::InsertTupleWithTieHandling()
// Each Less(Tuple*, Tuple*) indirectly calls Compare() once.
replaced = codegen->ReplaceCallSites(insert_batch_fn,
intra_partition_compare_fn, TupleRowComparator::COMPARE_SYMBOL);
DCHECK_REPLACE_COUNT(replaced, 10) << LlvmCodeGen::Print(insert_batch_fn);
} else {
// The total number of calls to tuple_row_less_than_->Compare() is 3 in
// PriorityQueue (called from 2 places), 1 in TopNNode::Heap::InsertTupleRow()
// and 3 in TopNNode::Heap::InsertTupleWithTieHandling
// Each Less(Tuple*, Tuple*) indirectly calls Compare() once.
replaced = codegen->ReplaceCallSites(insert_batch_fn,
compare_fn, TupleRowComparator::COMPARE_SYMBOL);
DCHECK_REPLACE_COUNT(replaced, 10) << LlvmCodeGen::Print(insert_batch_fn);
}
replaced = codegen->ReplaceCallSitesWithValue(insert_batch_fn,
codegen->GetI64Constant(heap_capacity()), "heap_capacity");
DCHECK_REPLACE_COUNT(replaced, 2)
<< LlvmCodeGen::Print(insert_batch_fn);
replaced = codegen->ReplaceCallSitesWithBoolConst(
insert_batch_fn, include_ties(), "include_ties");
DCHECK_REPLACE_COUNT(replaced, 1)
<< LlvmCodeGen::Print(insert_batch_fn);
int tuple_byte_size = output_tuple_desc_->byte_size();
replaced = codegen->ReplaceCallSitesWithValue(insert_batch_fn,
codegen->GetI32Constant(tuple_byte_size), "tuple_byte_size");
DCHECK_REPLACE_COUNT(replaced, 3)
<< LlvmCodeGen::Print(insert_batch_fn);
insert_batch_fn = codegen->FinalizeFunction(insert_batch_fn);
DCHECK(insert_batch_fn != NULL);
codegen->AddFunctionToJit(insert_batch_fn, &codegend_insert_batch_fn_);
}
}
AddCodegenStatus(codegen_status);
}
Status TopNNode::Open(RuntimeState* state) {
SCOPED_TIMER(runtime_profile_->total_time_counter());
ScopedOpenEventAdder ea(this);
RETURN_IF_ERROR(ExecNode::Open(state));
RETURN_IF_CANCELLED(state);
RETURN_IF_ERROR(QueryMaintenance(state));
RETURN_IF_ERROR(child(0)->Open(state));
RETURN_IF_ERROR(
order_cmp_->Open(pool_, state, expr_perm_pool(), expr_results_pool()));
RETURN_IF_ERROR(ScalarExprEvaluator::Open(output_tuple_expr_evals_, state));
if (is_partitioned()) {
// Set up state required by partitioned top-N implementation. Claim reservation
// after the child has been opened to reduce the peak reservation requirement.
if (!buffer_pool_client()->is_registered()) {
RETURN_IF_ERROR(ClaimBufferReservation(state));
}
RETURN_IF_ERROR(
partition_cmp_->Open(pool_, state, expr_perm_pool(), expr_results_pool()));
RETURN_IF_ERROR(intra_partition_order_cmp_->Open(
pool_, state, expr_perm_pool(), expr_results_pool()));
RETURN_IF_ERROR(sorter_->Open());
}
// Allocate memory for a temporary tuple.
tmp_tuple_ = reinterpret_cast<Tuple*>(
tuple_pool_->Allocate(output_tuple_desc_->byte_size()));
// Limit of 0, no need to fetch anything from children.
const TopNPlanNode& pnode = static_cast<const TopNPlanNode&>(plan_node_);
if (pnode.heap_capacity() != 0) {
RowBatch batch(child(0)->row_desc(), state->batch_size(), mem_tracker());
bool eos;
do {
batch.Reset();
RETURN_IF_ERROR(child(0)->GetNext(state, &batch, &eos));
{
SCOPED_TIMER(insert_batch_timer_);
TopNPlanNode::InsertBatchFn insert_batch_fn = codegend_insert_batch_fn_.load();
if (insert_batch_fn != nullptr) {
insert_batch_fn(this, state, &batch);
} else if (is_partitioned()) {
InsertBatchPartitioned(state, &batch);
} else {
InsertBatchUnpartitioned(state, &batch);
}
DCHECK(is_partitioned() || heap_->DCheckConsistency());
if (is_partitioned()) {
if (partition_heaps_.size() > FLAGS_partitioned_topn_in_mem_partitions_limit ||
tuple_pool_->total_reserved_bytes() >
FLAGS_partitioned_topn_soft_limit_bytes) {
RETURN_IF_ERROR(EvictPartitions(state, /*evict_final=*/false));
}
} else if (rows_to_reclaim_ > 2 * unpartitioned_capacity()) {
RETURN_IF_ERROR(ReclaimTuplePool(state));
}
}
RETURN_IF_CANCELLED(state);
RETURN_IF_ERROR(QueryMaintenance(state));
} while (!eos);
}
RETURN_IF_ERROR(PrepareForOutput(state));
// Unless we are inside a subplan expecting to call Open()/GetNext() on the child
// again, the child can be closed at this point.
if (!IsInSubplan()) child(0)->Close(state);
return Status::OK();
}
Status TopNNode::GetNext(RuntimeState* state, RowBatch* row_batch, bool* eos) {
SCOPED_TIMER(runtime_profile_->total_time_counter());
ScopedGetNextEventAdder ea(this, eos);
RETURN_IF_ERROR(ExecDebugAction(TExecNodePhase::GETNEXT, state));
return is_partitioned() ? GetNextPartitioned(state, row_batch, eos)
: GetNextUnpartitioned(state, row_batch, eos);
}
Status TopNNode::GetNextUnpartitioned(
RuntimeState* state, RowBatch* row_batch, bool* eos) {
DCHECK(!is_partitioned());
RETURN_IF_CANCELLED(state);
RETURN_IF_ERROR(QueryMaintenance(state));
while (!row_batch->AtCapacity() && (get_next_iter_ != sorted_top_n_.end())) {
if (num_rows_skipped_ < offset_) {
++get_next_iter_;
++num_rows_skipped_;
continue;
}
int row_idx = row_batch->AddRow();
TupleRow* dst_row = row_batch->GetRow(row_idx);
Tuple* src_tuple = *get_next_iter_;
TupleRow* src_row = reinterpret_cast<TupleRow*>(&src_tuple);
row_batch->CopyRow(src_row, dst_row);
++get_next_iter_;
row_batch->CommitLastRow();
IncrementNumRowsReturned(1);
}
*eos = get_next_iter_ == sorted_top_n_.end();
// Transfer ownership of tuple data to output batch.
// TODO: To improve performance for small inputs when this node is run multiple times
// inside a subplan, we might choose to only selectively transfer, e.g., when the
// block(s) in the pool are all full or when the pool has reached a certain size.
if (*eos) row_batch->tuple_data_pool()->AcquireData(tuple_pool_.get(), false);
COUNTER_SET(rows_returned_counter_, rows_returned());
return Status::OK();
}
Status TopNNode::GetNextPartitioned(
RuntimeState* state, RowBatch* batch, bool* eos) {
DCHECK(is_partitioned());
*eos = false;
while (!batch->AtCapacity()) {
RETURN_IF_CANCELLED(state);
RETURN_IF_ERROR(QueryMaintenance(state));
if (sort_out_batch_pos_ >= sort_out_batch_->num_rows()) {
// Output rows will reference tuples from sorter output batches - make sure memory
// is transferred correctly.
sort_out_batch_->TransferResourceOwnership(batch);
sort_out_batch_->Reset();
sort_out_batch_pos_ = 0;
if (batch->AtCapacity()) break;
bool sorter_eos = false;
RETURN_IF_ERROR(sorter_->GetNext(sort_out_batch_.get(), &sorter_eos));
if (sorter_eos && sort_out_batch_->num_rows() == 0) {
sort_out_batch_->TransferResourceOwnership(batch);
*eos = true;
break;
}
}
// Copy rows within the partition limits from 'sort_out_batch_' to 'batch'.
// NOTE: this loop could be codegen'd, but is unlikely to be the bottleneck for
// most partitioned top-N queries.
while (sort_out_batch_pos_ < sort_out_batch_->num_rows()) {
TupleRow* curr_row = sort_out_batch_->GetRow(sort_out_batch_pos_);
++sort_out_batch_pos_;
// If 'num_rows_returned_from_partition_' > 0, then 'prev_row' is the previous row
// returned from the current partition.
TupleRow* prev_row = reinterpret_cast<TupleRow*>(&tmp_tuple_);
bool add_row = false;
if (num_rows_returned_from_partition_ > 0
&& partition_cmp_->Compare(curr_row, prev_row) == 0) {
// Return rows up to the limit plus any ties that match the last returned row.
if (num_rows_returned_from_partition_ < per_partition_limit()
|| (include_ties() &&
intra_partition_order_cmp_->Compare(curr_row, prev_row) == 0)) {
add_row = true;
++num_rows_returned_from_partition_;
}
} else {
// New partition.
DCHECK_GT(per_partition_limit(), 0);
COUNTER_ADD(num_partitions_counter_, 1);
add_row = true;
num_rows_returned_from_partition_ = 1;
}
if (add_row) {
Tuple* out_tuple = curr_row->GetTuple(0);
tmp_tuple_ = out_tuple;
TupleRow* out_row = batch->GetRow(batch->AddRow());
out_row->SetTuple(0, out_tuple);
batch->CommitLastRow();
IncrementNumRowsReturned(1);
if (batch->AtCapacity()) break;
}
}
}
DCHECK(*eos || batch->AtCapacity());
if (num_rows_returned_from_partition_ == 0) {
// tmp_tuple_ references a previous partition, if anything. Make it clear that it's
// invalid.
tmp_tuple_ = nullptr;
} else if (num_rows_returned_from_partition_ > 0) {
// 'tmp_tuple_' is part of the current partition. Deep copy so that it doesn't
// reference memory that is attached to the output row batch.
Tuple* prev_tmp_tuple = tmp_tuple_;
unique_ptr<MemPool> temp_pool(new MemPool(mem_tracker()));
RETURN_IF_ERROR(InitTmpTuple(state, temp_pool.get()));
prev_tmp_tuple->DeepCopy(tmp_tuple_, *output_tuple_desc_, temp_pool.get());
tuple_pool_->FreeAll();
tuple_pool_ = move(temp_pool);
}
COUNTER_SET(rows_returned_counter_, rows_returned());
return Status::OK();
}
Status TopNNode::Reset(RuntimeState* state, RowBatch* row_batch) {
if (is_partitioned()) {
partition_heaps_.clear();
sorter_->Reset();
sort_out_batch_.reset();
sort_out_batch_pos_ = 0;
num_rows_returned_from_partition_ = 0;
} else {
heap_->Reset();
}
tmp_tuple_ = nullptr;
num_rows_skipped_ = 0;
// Transfer ownership of tuple data to output batch.
row_batch->tuple_data_pool()->AcquireData(tuple_pool_.get(), false);
// We deliberately do not free the tuple_pool_ here to allow selective transferring
// of resources in the future.
return ExecNode::Reset(state, row_batch);
}
void TopNNode::Close(RuntimeState* state) {
if (is_closed()) return;
if (heap_ != nullptr) heap_->Close();
for (auto& entry : partition_heaps_) {
DCHECK(entry.second != nullptr);
if (entry.second != nullptr) entry.second->Close();
}
if (tuple_pool_.get() != nullptr) tuple_pool_->FreeAll();
if (order_cmp_.get() != nullptr) order_cmp_->Close(state);
if (partition_cmp_.get() != nullptr) partition_cmp_->Close(state);
if (intra_partition_order_cmp_.get() != nullptr) {
intra_partition_order_cmp_->Close(state);
}
if (sorter_ != nullptr) sorter_->Close(state);
sort_out_batch_.reset();
ScalarExprEvaluator::Close(output_tuple_expr_evals_, state);
ExecNode::Close(state);
}
Status TopNNode::EvictPartitions(RuntimeState* state, bool evict_final) {
DCHECK(is_partitioned());
vector<unique_ptr<Heap>> heaps_to_evict;
if (evict_final) {
// Move all the partitions to 'sorter_' in preparation for the final sort. Partitions
// are evicted in the order of the partition key to reduce the amount of shuffling
// that the final sort will do to rearrange partitions.
for (auto& entry : partition_heaps_) {
heaps_to_evict.push_back(move(entry.second));
}
partition_heaps_.clear();
} else {
heaps_to_evict = SelectPartitionsToEvict();
}
// Only count heap eviction if they are as a result of memory pressure.
if (!evict_final) COUNTER_ADD(in_mem_heap_evicted_counter_, heaps_to_evict.size());
RowBatch batch(row_desc(), state->batch_size(), mem_tracker());
for (auto& heap : heaps_to_evict) {
DCHECK(heap->DCheckConsistency());
// Extract partition entries from the heap in sorted order to reduce amount of sorting
// required in final sort. This sorting is not required for correctness since
// 'sorter_' will do a full sort later.
heap->PrepareForOutput(*this, &sorted_top_n_);
for (int64_t i = 0; i < sorted_top_n_.size(); ++i) {
TupleRow* row = batch.GetRow(batch.AddRow());
row->SetTuple(0, sorted_top_n_[i]);
batch.CommitLastRow();
if (batch.AtCapacity() || i == sorted_top_n_.size() - 1) {
RETURN_IF_ERROR(sorter_->AddBatch(&batch));
batch.Reset();
}
}
sorted_top_n_.clear();
}
heaps_to_evict.clear();
// ReclaimTuplePool() can now reclaim memory that is not used by in-memory partitions.
RETURN_IF_ERROR(ReclaimTuplePool(state));
return Status::OK();
}
vector<unique_ptr<TopNNode::Heap>> TopNNode::SelectPartitionsToEvict() {
// Evict a subset of heaps to free enough memory to continue.
// The goal of this approach is to try to maximize rows filtered out, while only
// adding O(1) amortized cost per input row. Rematerializing all the heaps (required
// to free memory) is O(m) work, where m is the total number of tuples in the heaps.
// If we clear out O(m) tuples, that means we will have to process at least O(m) input
// rows before another eviction, so the amortized overhead of eviction per input row
// is O(m) / O(m) = O(1).
//
// We evict heaps starting with the heaps that were least effective at filtering
// input. We evict 25% of heap tuples so that we achieve O(1) amortized time but
// retain effectively filtering heaps as much as possible. We break ties, which
// are most likely heaps that have not filtered input since the last eviction,
// based on whether they are growing and likely to start filtering in the near
// future.
// TODO: it's possible that we could free up memory without evicting any heaps
// just by reclaiming unreferenced variable-length data. We do not do that yet
// because we don't know if it will reclaim enough memory. Evicting some heaps
// is guaranteed to be effective.
vector<PartitionHeapMap::iterator> sorted_heaps;
int64_t total_tuples = 0;
sorted_heaps.reserve(partition_heaps_.size());
for (auto it = partition_heaps_.begin(); it != partition_heaps_.end(); ++it) {
total_tuples += it->second->num_tuples();
sorted_heaps.push_back(it);
}
sort(sorted_heaps.begin(), sorted_heaps.end(),
[](const PartitionHeapMap::iterator& left,
const PartitionHeapMap::iterator& right) {
int64_t left_discarded = left->second->num_tuples_discarded();
int64_t right_discarded = right->second->num_tuples_discarded();
if (left_discarded != right_discarded) {
return left_discarded < right_discarded;
}
return left->second->num_tuples_added_since_eviction() <
right->second->num_tuples_added_since_eviction();
});
vector<unique_ptr<Heap>> result;
int64_t num_tuples_evicted = 0;
for (auto it : sorted_heaps) {
if (num_tuples_evicted < total_tuples / 4) {
result.push_back(move(it->second));
partition_heaps_.erase(it);
num_tuples_evicted += result.back()->num_tuples();
} else {
// Reset counters on surviving heaps so that statistics are accurate about
// recent filtering.
it->second->ResetStats(*this);
}
}
return result;
}
Status TopNNode::PrepareForOutput(RuntimeState* state) {
if (is_partitioned()) {
// Dump all rows into the sorter and sort by partition, so that we can iterate
// through the rows and build heaps partition-by-partition.
RETURN_IF_ERROR(EvictPartitions(state, /*evict_final=*/true));
DCHECK(partition_heaps_.empty());
RETURN_IF_ERROR(sorter_->InputDone());
sort_out_batch_.reset(
new RowBatch(row_desc(), state->batch_size(), mem_tracker()));
} else {
DCHECK(heap_->DCheckConsistency());
heap_->PrepareForOutput(*this, &sorted_top_n_);
get_next_iter_ = sorted_top_n_.begin();
}
return Status::OK();
}
void TopNNode::Heap::PrepareForOutput(
const TopNNode& RESTRICT node, vector<Tuple*>* sorted_top_n) RESTRICT {
ResetStats(node); // Ensure all counters are updated.
// Reverse the order of the tuples in the priority queue
sorted_top_n->resize(num_tuples());
int64_t index = sorted_top_n->size() - 1;
/// Any ties with the min will be the last elements in 'sorted_top_n'.
while (!overflowed_ties_.empty()) {
(*sorted_top_n)[index] = overflowed_ties_.back();
overflowed_ties_.pop_back();
--index;
}
while (!priority_queue_.Empty()) {
(*sorted_top_n)[index] = priority_queue_.Pop();
--index;
}
}
void TopNNode::Heap::ResetStats(const TopNNode& RESTRICT node) {
RuntimeProfile::Counter* counter = node.in_mem_heap_rows_filtered_counter_;
if (counter != nullptr) COUNTER_ADD(counter, num_tuples_discarded_);
num_tuples_discarded_ = 0;
num_tuples_at_last_eviction_ = num_tuples();
}
bool TopNNode::Heap::DCheckConsistency() {
DCHECK_LE(num_tuples(), capacity_ + overflowed_ties_.size())
<< num_tuples() << " > " << capacity_ << " + " << overflowed_ties_.size();
if (!overflowed_ties_.empty()) {
DCHECK(include_ties_);
DCHECK_EQ(capacity_, priority_queue_.Size())
<< "Ties should only be present if heap is at capacity";
}
return true;
}
Status TopNNode::ReclaimTuplePool(RuntimeState* state) {
COUNTER_ADD(tuple_pool_reclaim_counter_, 1);
unique_ptr<MemPool> temp_pool(new MemPool(mem_tracker()));
if (is_partitioned()) {
vector<unique_ptr<Heap>> rematerialized_heaps;
for (auto& entry : partition_heaps_) {
RETURN_IF_ERROR(entry.second->RematerializeTuples(this, state, temp_pool.get()));
DCHECK(entry.second->DCheckConsistency());
}
// The second loop is needed for IMPALA-11631. We only move heaps from partition_heap_
// to rematerialized_heaps once all have been rematerialized. Otherwise, in case of
// an error, we may call Close() on a nullptr or leak the memory by not explicitly
// calling Close() on the heap pointer. Maybe better to add Close() in the Heap
// destructor later.
for (auto& entry : partition_heaps_) {
// The key references memory in 'tuple_pool_'. Replace it with a rematerialized
// tuple.
rematerialized_heaps.push_back(move(entry.second));
}
partition_heaps_.clear();
for (auto& heap_ptr : rematerialized_heaps) {
const Tuple* key_tuple = heap_ptr->top();
partition_heaps_.emplace(key_tuple, move(heap_ptr));
}
} else {
RETURN_IF_ERROR(heap_->RematerializeTuples(this, state, temp_pool.get()));
DCHECK(heap_->DCheckConsistency());
}
rows_to_reclaim_ = 0;
RETURN_IF_ERROR(InitTmpTuple(state, temp_pool.get()));
tuple_pool_->FreeAll();
tuple_pool_ = move(temp_pool);
return Status::OK();
}
Status TopNNode::InitTmpTuple(RuntimeState* state, MemPool* pool) {
tmp_tuple_ = reinterpret_cast<Tuple*>(pool->TryAllocate(
output_tuple_desc_->byte_size()));
if (UNLIKELY(tmp_tuple_ == nullptr)) {
return pool->mem_tracker()->MemLimitExceeded(state,
"Failed to allocate memory in TopNNode::ReclaimTuplePool.",
output_tuple_desc_->byte_size());
}
return Status::OK();
}
void TopNNode::DebugString(int indentation_level, stringstream* out) const {
*out << string(indentation_level * 2, ' ');
const TopNPlanNode& pnode = static_cast<const TopNPlanNode&>(plan_node_);
const TSortInfo& tsort_info = pnode.tnode_->sort_node.sort_info;
*out << "TopNNode(" << ScalarExpr::DebugString(pnode.ordering_exprs_);
for (int i = 0; i < tsort_info.is_asc_order.size(); ++i) {
*out << (i > 0 ? " " : "") << (tsort_info.is_asc_order[i] ? "asc" : "desc")
<< " nulls " << (tsort_info.nulls_first[i] ? "first" : "last");
}
ExecNode::DebugString(indentation_level, out);
*out << ")";
}
TopNNode::Heap::Heap(const TupleRowComparator& c, int64_t capacity, bool include_ties) :
capacity_(capacity), include_ties_(include_ties), priority_queue_(c) {}
void TopNNode::Heap::Reset() {
priority_queue_.Clear();
overflowed_ties_.clear();
}
void TopNNode::Heap::Close() {
priority_queue_.Clear();
overflowed_ties_.clear();
}
template <class T>
Status TopNNode::Heap::RematerializeTuplesHelper(TopNNode* node,
RuntimeState* state, MemPool* new_pool, T begin_it, T end_it) {
const TupleDescriptor& tuple_desc = *node->output_tuple_desc_;
int tuple_size = tuple_desc.byte_size();
for (T it = begin_it; it != end_it; ++it) {
Tuple* insert_tuple = reinterpret_cast<Tuple*>(new_pool->TryAllocate(tuple_size));
if (UNLIKELY(insert_tuple == nullptr)) {
return new_pool->mem_tracker()->MemLimitExceeded(state,
"Failed to allocate memory in TopNNode::ReclaimTuplePool.", tuple_size);
}
(*it)->DeepCopy(insert_tuple, tuple_desc, new_pool);
*it = insert_tuple;
}
return Status::OK();
}
Status TopNNode::Heap::RematerializeTuples(TopNNode* node,
RuntimeState* state, MemPool* new_pool) {
RETURN_IF_ERROR(RematerializeTuplesHelper(
node, state, new_pool, priority_queue_.Begin(), priority_queue_.End()));
RETURN_IF_ERROR(RematerializeTuplesHelper(
node, state, new_pool, overflowed_ties_.begin(), overflowed_ties_.end()));
return Status::OK();
}
template class impala::PriorityQueue<Tuple*, TupleRowComparator>;
template class impala::PriorityQueueIterator<Tuple*, TupleRowComparator>;