blob: ef1bb8af1221a86ada7012bae69dbbab79aa6f35 [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 <algorithm>
#include <cstddef>
#include <cstdint>
#include <set>
#include <string>
#include <vector>
#include <benchmark/benchmark.h>
#include "compute/delta/DeltaDeletionVectorReader.h"
#include "compute/delta/RoaringBitmapArray.h"
#include "velox/common/base/Exceptions.h"
#include "velox/common/memory/Memory.h"
using gluten::delta::DeltaDeletionVectorReader;
using gluten::delta::RoaringBitmapArray;
using namespace facebook::velox;
namespace {
enum class RowIndexPattern {
kContiguous,
kSparse,
kClustered,
kMultiBucket,
};
enum class PartialDistribution {
kContiguous,
kRoundRobin,
};
struct RowIndexSummary {
uint64_t rowSpan{0};
size_t bucketCount{0};
double densityPercent{0};
};
std::vector<uint64_t> makeRowIndexes(size_t rowCount, RowIndexPattern pattern) {
std::vector<uint64_t> rows;
rows.reserve(rowCount);
for (size_t i = 0; i < rowCount; ++i) {
switch (pattern) {
case RowIndexPattern::kContiguous:
rows.push_back(i);
break;
case RowIndexPattern::kSparse:
rows.push_back(i * 97);
break;
case RowIndexPattern::kClustered:
rows.push_back((i / 64) * 4096 + (i % 64));
break;
case RowIndexPattern::kMultiBucket:
rows.push_back((static_cast<uint64_t>(i % 4) << 32) + (i / 4));
break;
}
}
return rows;
}
RowIndexSummary summarizeRowIndexes(const std::vector<uint64_t>& rows) {
if (rows.empty()) {
return {};
}
const auto [minIt, maxIt] = std::minmax_element(rows.begin(), rows.end());
std::set<uint32_t> buckets;
for (const auto row : rows) {
buckets.insert(static_cast<uint32_t>(row >> 32));
}
const auto rowSpan = *maxIt - *minIt + 1;
return RowIndexSummary{
rowSpan, buckets.size(), static_cast<double>(rows.size()) * 100.0 / static_cast<double>(rowSpan)};
}
std::string buildPayload(const std::vector<uint64_t>& rows, bool optimize) {
RoaringBitmapArray bitmap;
for (const auto row : rows) {
bitmap.addSafe(row);
}
return bitmap.serializeToString(optimize);
}
std::vector<std::string> buildPartialPayloads(
const std::vector<uint64_t>& rows,
size_t partialCount,
bool optimize,
PartialDistribution distribution) {
std::vector<RoaringBitmapArray> partials(partialCount);
for (size_t i = 0; i < rows.size(); ++i) {
const auto partialIndex = distribution == PartialDistribution::kRoundRobin
? i % partialCount
: std::min(i * partialCount / rows.size(), partialCount - 1);
partials[partialIndex].addSafe(rows[i]);
}
std::vector<std::string> payloads;
payloads.reserve(partialCount);
for (const auto& partial : partials) {
payloads.push_back(partial.serializeToString(optimize));
}
return payloads;
}
std::vector<uint64_t> makeProbeRows(const std::vector<uint64_t>& rows) {
const auto hitProbeCount = std::min<size_t>(rows.size(), 4096);
std::vector<uint64_t> probes;
probes.reserve(hitProbeCount * 2);
if (hitProbeCount == 0) {
return probes;
}
const auto stride = std::max<size_t>(rows.size() / hitProbeCount, 1);
for (size_t i = 0; i < rows.size() && probes.size() < hitProbeCount * 2; i += stride) {
probes.push_back(rows[i]);
probes.push_back(rows.back() + 4096 + probes.size());
}
return probes;
}
void setCounters(
benchmark::State& state,
size_t rowCount,
size_t payloadBytes,
RowIndexSummary summary,
size_t partialCount = 0) {
state.counters["rows"] = benchmark::Counter(rowCount);
state.counters["payload_bytes"] = benchmark::Counter(payloadBytes);
state.counters["payload_bytes_per_row"] = benchmark::Counter(static_cast<double>(payloadBytes) / rowCount);
state.counters["row_span"] = benchmark::Counter(summary.rowSpan);
state.counters["bucket_count"] = benchmark::Counter(summary.bucketCount);
state.counters["density_pct"] = benchmark::Counter(summary.densityPercent);
if (partialCount > 0) {
state.counters["partials"] = benchmark::Counter(partialCount);
}
}
void BM_BuildAndSerialize(benchmark::State& state, RowIndexPattern pattern) {
const auto rows = makeRowIndexes(state.range(0), pattern);
const auto summary = summarizeRowIndexes(rows);
size_t payloadBytes = 0;
uint64_t cardinality = 0;
for (auto _ : state) {
RoaringBitmapArray bitmap;
for (const auto row : rows) {
bitmap.addSafe(row);
}
const auto payload = bitmap.serializeToString(true);
payloadBytes = payload.size();
cardinality = bitmap.cardinality();
VELOX_CHECK_EQ(cardinality, rows.size());
benchmark::DoNotOptimize(payload);
}
state.SetItemsProcessed(state.iterations() * rows.size());
state.SetBytesProcessed(state.iterations() * rows.size() * sizeof(uint64_t));
setCounters(state, rows.size(), payloadBytes, summary);
state.counters["cardinality"] = benchmark::Counter(cardinality);
}
void BM_DeserializeAndProbe(benchmark::State& state, RowIndexPattern pattern) {
const auto rows = makeRowIndexes(state.range(0), pattern);
const auto summary = summarizeRowIndexes(rows);
const auto payload = buildPayload(rows, true);
const auto probes = makeProbeRows(rows);
uint64_t hits = 0;
for (auto _ : state) {
RoaringBitmapArray bitmap;
bitmap.deserialize(payload.data(), payload.size());
VELOX_CHECK_EQ(bitmap.cardinality(), rows.size());
uint64_t localHits = 0;
for (const auto probe : probes) {
localHits += bitmap.containsSafe(probe) ? 1 : 0;
}
hits = localHits;
benchmark::DoNotOptimize(hits);
}
state.SetItemsProcessed(state.iterations() * probes.size());
state.SetBytesProcessed(state.iterations() * payload.size());
setCounters(state, rows.size(), payload.size(), summary);
state.counters["probes"] = benchmark::Counter(probes.size());
state.counters["hits"] = benchmark::Counter(hits);
}
void BM_MergePartials(benchmark::State& state, RowIndexPattern pattern, PartialDistribution distribution) {
const auto rows = makeRowIndexes(state.range(0), pattern);
const auto summary = summarizeRowIndexes(rows);
const auto partialCount = static_cast<size_t>(state.range(1));
const auto payloads = buildPartialPayloads(rows, partialCount, false, distribution);
size_t mergedPayloadBytes = 0;
uint64_t cardinality = 0;
for (auto _ : state) {
RoaringBitmapArray merged;
for (const auto& payload : payloads) {
RoaringBitmapArray partial;
partial.deserialize(payload.data(), payload.size());
merged.merge(partial);
}
const auto mergedPayload = merged.serializeToString(true);
mergedPayloadBytes = mergedPayload.size();
cardinality = merged.cardinality();
VELOX_CHECK_EQ(cardinality, rows.size());
benchmark::DoNotOptimize(mergedPayload);
}
state.SetItemsProcessed(state.iterations() * rows.size());
setCounters(state, rows.size(), mergedPayloadBytes, summary, partialCount);
state.counters["cardinality"] = benchmark::Counter(cardinality);
}
} // namespace
BENCHMARK_CAPTURE(BM_BuildAndSerialize, Contiguous_1M, RowIndexPattern::kContiguous)
->Arg(1 << 20)
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(BM_BuildAndSerialize, Sparse_1M, RowIndexPattern::kSparse)
->Arg(1 << 20)
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(BM_BuildAndSerialize, Clustered_1M, RowIndexPattern::kClustered)
->Arg(1 << 20)
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(BM_BuildAndSerialize, MultiBucket_256K, RowIndexPattern::kMultiBucket)
->Arg(1 << 18)
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(BM_DeserializeAndProbe, Contiguous_1M, RowIndexPattern::kContiguous)
->Arg(1 << 20)
->Unit(benchmark::kMicrosecond);
BENCHMARK_CAPTURE(BM_DeserializeAndProbe, Sparse_1M, RowIndexPattern::kSparse)
->Arg(1 << 20)
->Unit(benchmark::kMicrosecond);
BENCHMARK_CAPTURE(BM_DeserializeAndProbe, MultiBucket_256K, RowIndexPattern::kMultiBucket)
->Arg(1 << 18)
->Unit(benchmark::kMicrosecond);
BENCHMARK_CAPTURE(
BM_MergePartials,
Contiguous_1M_64Partials,
RowIndexPattern::kContiguous,
PartialDistribution::kContiguous)
->Args({1 << 20, 64})
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(
BM_MergePartials,
Contiguous_1M_64RoundRobinPartials,
RowIndexPattern::kContiguous,
PartialDistribution::kRoundRobin)
->Args({1 << 20, 64})
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(BM_MergePartials, Sparse_1M_64Partials, RowIndexPattern::kSparse, PartialDistribution::kContiguous)
->Args({1 << 20, 64})
->Unit(benchmark::kMillisecond);
BENCHMARK_CAPTURE(
BM_MergePartials,
MultiBucket_256K_64Partials,
RowIndexPattern::kMultiBucket,
PartialDistribution::kContiguous)
->Args({1 << 18, 64})
->Unit(benchmark::kMillisecond);
// Benchmark for applyDeletionFilter: measures the hot path where a batch of
// rows is checked against the deletion vector bitmap.
// deletionPercent: fraction of rows in the total file that are deleted.
// batchSize: number of rows per batch (typical Velox batch size).
void BM_ApplyDeletionFilter(benchmark::State& state, double deletionPercent) {
const auto batchSize = static_cast<uint64_t>(state.range(0));
const uint64_t totalFileRows = 1000000; // 1M row file
const auto numDeleted = static_cast<uint64_t>(totalFileRows * deletionPercent / 100.0);
// Build a DV with deletions spread across the file.
RoaringBitmapArray bitmap;
const uint64_t stride = numDeleted > 0 ? totalFileRows / numDeleted : 0;
for (uint64_t i = 0; i < numDeleted; ++i) {
bitmap.addSafe(i * stride);
}
const auto payload = bitmap.serializeToString(true);
// Load the DV reader.
DeltaDeletionVectorReader reader;
reader.loadSerializedDeletionVector(std::string_view(payload.data(), payload.size()));
// Allocate the output bitmap buffer.
auto pool = memory::memoryManager()->addLeafPool();
auto deleteBitmap = AlignedBuffer::allocate<uint64_t>(bits::nwords(batchSize), pool.get());
// Simulate scanning through the file in batches.
const uint64_t numBatches = totalFileRows / batchSize;
// Only count rows actually processed (drop tail < batchSize).
const uint64_t rowsProcessed = numBatches * batchSize;
uint64_t totalDeletedFound = 0;
for (auto _ : state) {
totalDeletedFound = 0;
for (uint64_t batch = 0; batch < numBatches; ++batch) {
reader.applyDeletionFilter(batch * batchSize, batchSize, deleteBitmap);
// Count bits set to prevent dead-code elimination.
// Padding bits in the last word are safe: applyDeletionFilter memsets
// the entire bitmap to zero before setting only in-range bits.
auto* raw = deleteBitmap->as<uint64_t>();
for (uint64_t w = 0; w < bits::nwords(batchSize); ++w) {
totalDeletedFound += __builtin_popcountll(raw[w]);
}
}
benchmark::DoNotOptimize(totalDeletedFound);
}
state.SetItemsProcessed(state.iterations() * rowsProcessed);
state.counters["batch_size"] = benchmark::Counter(batchSize);
state.counters["deletion_pct"] = benchmark::Counter(deletionPercent);
state.counters["deleted_found"] = benchmark::Counter(totalDeletedFound);
state.counters["total_batches"] = benchmark::Counter(numBatches);
}
// Sparse deletions (1%) - the common case for MERGE/UPDATE operations.
BENCHMARK_CAPTURE(BM_ApplyDeletionFilter, Sparse_1pct, 1.0)->Arg(4096)->Unit(benchmark::kMillisecond);
// Moderate deletions (10%).
BENCHMARK_CAPTURE(BM_ApplyDeletionFilter, Moderate_10pct, 10.0)->Arg(4096)->Unit(benchmark::kMillisecond);
// Dense deletions (50%).
BENCHMARK_CAPTURE(BM_ApplyDeletionFilter, Dense_50pct, 50.0)->Arg(4096)->Unit(benchmark::kMillisecond);
// Very dense deletions (90%).
BENCHMARK_CAPTURE(BM_ApplyDeletionFilter, VeryDense_90pct, 90.0)->Arg(4096)->Unit(benchmark::kMillisecond);
// Sparse with large batch (typical Velox max batch).
BENCHMARK_CAPTURE(BM_ApplyDeletionFilter, Sparse_1pct_LargeBatch, 1.0)->Arg(10000)->Unit(benchmark::kMillisecond);
int main(int argc, char** argv) {
memory::MemoryManager::testingSetInstance(memory::MemoryManager::Options{});
benchmark::Initialize(&argc, argv);
benchmark::RunSpecifiedBenchmarks();
benchmark::Shutdown();
return 0;
}