blob: a15fc043eb9c2f4b815b517da9b31dbacf0fd3b1 [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.
#pragma once
#include <benchmark/benchmark.h>
#include <gen_cpp/segment_v2.pb.h>
#include <cstdint>
#include <cstring>
#include <memory>
#include <random>
#include <string>
#include <vector>
#include "storage/cache/page_cache.h"
#include "storage/segment/binary_plain_page_v2.h"
#include "storage/segment/binary_plain_page_v2_pre_decoder.h"
#include "storage/segment/binary_plain_page_v3.h"
#include "storage/segment/binary_plain_page_v3_pre_decoder.h"
#include "storage/segment/options.h"
#include "storage/segment/page_builder.h"
#include "storage/types.h"
#include "util/block_compression.h"
#include "util/faststring.h"
#include "util/slice.h"
namespace doris {
namespace segment_v2 {
// Build a fixed corpus of strings of `value_len` bytes each. We reuse the same
// corpus across V2 and V3 so the only variable left is the on-disk layout. A
// deterministic RNG keeps results comparable across runs.
inline std::vector<std::string> make_corpus(size_t num_elems, size_t value_len) {
std::mt19937 rng(0xC0FFEEu);
std::uniform_int_distribution<int> dist('a', 'z');
std::vector<std::string> corpus;
corpus.reserve(num_elems);
for (size_t i = 0; i < num_elems; ++i) {
std::string s(value_len, '\0');
for (size_t j = 0; j < value_len; ++j) {
s[j] = static_cast<char>(dist(rng));
}
corpus.emplace_back(std::move(s));
}
return corpus;
}
template <template <FieldType> class BuilderT>
inline OwnedSlice build_page(const std::vector<std::string>& corpus) {
std::vector<Slice> slices;
slices.reserve(corpus.size());
for (const auto& s : corpus) {
slices.emplace_back(s);
}
PageBuilderOptions opts;
// Disable the size-bound check so the whole corpus lands in one page.
opts.data_page_size = 0;
opts.dict_page_size = 0;
PageBuilder* raw = nullptr;
Status st = BuilderT<FieldType::OLAP_FIELD_TYPE_VARCHAR>::create(&raw, opts);
CHECK(st.ok()) << st;
std::unique_ptr<PageBuilder> builder(raw);
size_t count = slices.size();
st = builder->add(reinterpret_cast<const uint8_t*>(slices.data()), &count);
CHECK(st.ok()) << st;
CHECK_EQ(count, slices.size());
OwnedSlice out;
st = builder->finish(&out);
CHECK(st.ok()) << st;
return out;
}
// Per-fixture: build the input page once outside the timed loop, then in each
// iteration restore the input Slice (since decode() rewrites it to point at the
// freshly-allocated V1 page) and measure only the decode call. We also report
// per-element throughput so V2 vs V3 are easy to compare across (N, len).
template <template <FieldType> class BuilderT, class PreDecoderT>
inline void run_pre_decode_bm(benchmark::State& state) {
const size_t num_elems = static_cast<size_t>(state.range(0));
const size_t value_len = static_cast<size_t>(state.range(1));
auto corpus = make_corpus(num_elems, value_len);
OwnedSlice owned = build_page<BuilderT>(corpus);
const Slice original = owned.slice();
PreDecoderT pre_decoder;
for (auto _ : state) {
Slice page_slice = original;
std::unique_ptr<DataPage> decoded_page;
Status st = pre_decoder.decode(&decoded_page, &page_slice, /*size_of_tail=*/0,
/*use_cache=*/false, PageTypePB::DATA_PAGE,
/*file_path=*/std::string());
CHECK(st.ok()) << st;
benchmark::DoNotOptimize(page_slice);
benchmark::DoNotOptimize(decoded_page);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(num_elems));
state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(original.size));
state.counters["page_bytes"] = static_cast<double>(original.size);
state.counters["ns_per_elem"] = benchmark::Counter(
static_cast<double>(num_elems),
benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert);
}
inline void BM_BinaryPlainPageV2_PreDecode(benchmark::State& state) {
run_pre_decode_bm<BinaryPlainPageV2Builder, BinaryPlainPageV2PreDecoder<false>>(state);
}
inline void BM_BinaryPlainPageV3_PreDecode(benchmark::State& state) {
run_pre_decode_bm<BinaryPlainPageV3Builder, BinaryPlainPageV3PreDecoder<false>>(state);
}
// (num_elems, value_len) grid. Picks representative sizes: a "many small"
// case (8 byte values like compact dictionary keys), a typical varchar case
// (32, 128 bytes), and a "large value" case (1024 bytes). num_elems range
// covers small / medium / page-sized pages.
static void V2V3PreDecodeArgs(benchmark::internal::Benchmark* b) {
for (int n : {256, 1024, 4096, 16384}) {
for (int len : {8, 32, 128, 1024}) {
b->Args({n, len});
}
}
}
BENCHMARK(BM_BinaryPlainPageV2_PreDecode)->Apply(V2V3PreDecodeArgs);
BENCHMARK(BM_BinaryPlainPageV3_PreDecode)->Apply(V2V3PreDecodeArgs);
// ---------------------------------------------------------------------------
// Fixed-page-size variants: pin to a production page size so we measure
// pre-decode cost at realistic byte counts. num_elems is derived from
// value_len so each input page lands at ~target_bytes.
// - 64 KiB matches STORAGE_PAGE_SIZE_DEFAULT_VALUE (default data page)
// - 256 KiB matches STORAGE_DICT_PAGE_SIZE_DEFAULT_VALUE (dict / large)
// ---------------------------------------------------------------------------
inline constexpr size_t kBenchPage64KiB = 64 * 1024;
inline constexpr size_t kBenchPage256KiB = 256 * 1024;
// Pick num_elems so that (varint_len + value_len) * num_elems ~= target_bytes.
// Varint cost: 1 byte for value_len < 128, 2 bytes for value_len < 16384.
inline size_t elems_for_target(size_t target_bytes, size_t value_len) {
const size_t varint_bytes = value_len < 128 ? 1 : 2;
const size_t per_entry = varint_bytes + value_len;
return target_bytes / per_entry;
}
template <template <FieldType> class BuilderT, class PreDecoderT, size_t TargetBytes>
inline void run_pre_decode_fixed_page_bm(benchmark::State& state) {
const size_t value_len = static_cast<size_t>(state.range(0));
const size_t num_elems = elems_for_target(TargetBytes, value_len);
auto corpus = make_corpus(num_elems, value_len);
OwnedSlice owned = build_page<BuilderT>(corpus);
const Slice original = owned.slice();
PreDecoderT pre_decoder;
for (auto _ : state) {
Slice page_slice = original;
std::unique_ptr<DataPage> decoded_page;
Status st = pre_decoder.decode(&decoded_page, &page_slice, /*size_of_tail=*/0,
/*use_cache=*/false, PageTypePB::DATA_PAGE,
/*file_path=*/std::string());
CHECK(st.ok()) << st;
benchmark::DoNotOptimize(page_slice);
benchmark::DoNotOptimize(decoded_page);
benchmark::ClobberMemory();
}
state.SetItemsProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(num_elems));
state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(original.size));
state.counters["num_elems"] = static_cast<double>(num_elems);
state.counters["page_bytes"] = static_cast<double>(original.size);
state.counters["ns_per_elem"] = benchmark::Counter(
static_cast<double>(num_elems),
benchmark::Counter::kIsIterationInvariantRate | benchmark::Counter::kInvert);
}
inline void BM_BinaryPlainPageV2_PreDecode_64KiB(benchmark::State& state) {
run_pre_decode_fixed_page_bm<BinaryPlainPageV2Builder, BinaryPlainPageV2PreDecoder<false>,
kBenchPage64KiB>(state);
}
inline void BM_BinaryPlainPageV3_PreDecode_64KiB(benchmark::State& state) {
run_pre_decode_fixed_page_bm<BinaryPlainPageV3Builder, BinaryPlainPageV3PreDecoder<false>,
kBenchPage64KiB>(state);
}
inline void BM_BinaryPlainPageV2_PreDecode_256KiB(benchmark::State& state) {
run_pre_decode_fixed_page_bm<BinaryPlainPageV2Builder, BinaryPlainPageV2PreDecoder<false>,
kBenchPage256KiB>(state);
}
inline void BM_BinaryPlainPageV3_PreDecode_256KiB(benchmark::State& state) {
run_pre_decode_fixed_page_bm<BinaryPlainPageV3Builder, BinaryPlainPageV3PreDecoder<false>,
kBenchPage256KiB>(state);
}
// value_len sweep. Each row produces a page near the target byte count;
// num_elems is reported via the `num_elems` counter so the (N, len)
// relationship is visible alongside the timing.
static void V2V3PreDecodeFixedPageArgs(benchmark::internal::Benchmark* b) {
for (int len : {8, 16, 32, 64, 128, 256, 512, 1024, 4096}) {
b->Args({len});
}
}
BENCHMARK(BM_BinaryPlainPageV2_PreDecode_64KiB)->Apply(V2V3PreDecodeFixedPageArgs);
BENCHMARK(BM_BinaryPlainPageV3_PreDecode_64KiB)->Apply(V2V3PreDecodeFixedPageArgs);
BENCHMARK(BM_BinaryPlainPageV2_PreDecode_256KiB)->Apply(V2V3PreDecodeFixedPageArgs);
BENCHMARK(BM_BinaryPlainPageV3_PreDecode_256KiB)->Apply(V2V3PreDecodeFixedPageArgs);
// ===========================================================================
// On-disk size comparison: encode a page with V2 vs V3, then ZSTD-compress the
// encoded page (mirroring the segment write path, which compresses each page
// before writing) and report the raw and ZSTD-compressed byte counts.
//
// Both V2 and V3 store the same bytes (only the layout differs), so this isolates the
// effect of the V3 layout (contiguous data + contiguous varint lengths) on ZSTD's ratio.
// Two corpora:
// - VARCHAR (variable length, no padding).
// - CHAR (fixed length, '\0'-padded as OlapColumnDataConvertorChar does): both V2 and V3
// keep the padding on disk, so this measures the layout effect on highly compressible
// padded data.
// ===========================================================================
// Build a CHAR corpus: each value is exactly `value_len` bytes (padded), with a
// random logical content length in [1, value_len] followed by '\0' padding.
inline std::vector<std::string> make_padded_char_corpus(size_t num_elems, size_t value_len) {
std::mt19937 rng(0xC0FFEEu);
std::uniform_int_distribution<int> ch('a', 'z');
std::uniform_int_distribution<size_t> len_dist(1, value_len);
std::vector<std::string> corpus;
corpus.reserve(num_elems);
for (size_t i = 0; i < num_elems; ++i) {
const size_t logical = len_dist(rng);
std::string s(value_len, '\0'); // trailing bytes stay '\0' (the padding)
for (size_t j = 0; j < logical; ++j) {
s[j] = static_cast<char>(ch(rng));
}
corpus.emplace_back(std::move(s));
}
return corpus;
}
template <template <FieldType> class BuilderT, FieldType Type>
inline OwnedSlice build_page_typed(const std::vector<Slice>& slices) {
PageBuilderOptions opts;
opts.data_page_size = 0; // single page, no size-bound check
opts.dict_page_size = 0;
PageBuilder* raw = nullptr;
Status st = BuilderT<Type>::create(&raw, opts);
CHECK(st.ok()) << st;
std::unique_ptr<PageBuilder> builder(raw);
size_t count = slices.size();
st = builder->add(reinterpret_cast<const uint8_t*>(slices.data()), &count);
CHECK(st.ok()) << st;
CHECK_EQ(count, slices.size());
OwnedSlice out;
st = builder->finish(&out);
CHECK(st.ok()) << st;
return out;
}
inline BlockCompressionCodec* zstd_codec() {
BlockCompressionCodec* codec = nullptr;
Status st = get_block_compression_codec(segment_v2::CompressionTypePB::ZSTD, &codec);
CHECK(st.ok()) << st;
CHECK(codec != nullptr);
return codec;
}
// Build the page once, ZSTD-compress it in the timed loop, and report raw /
// compressed byte counts plus the ratio. CharPadding selects the padded CHAR
// corpus (and the CHAR builder specialization) vs the plain VARCHAR corpus.
template <template <FieldType> class BuilderT, FieldType Type, bool CharPadding>
inline void run_zstd_size_bm(benchmark::State& state) {
const size_t num_elems = static_cast<size_t>(state.range(0));
const size_t value_len = static_cast<size_t>(state.range(1));
std::vector<std::string> corpus = CharPadding ? make_padded_char_corpus(num_elems, value_len)
: make_corpus(num_elems, value_len);
std::vector<Slice> slices;
slices.reserve(corpus.size());
for (const auto& s : corpus) {
slices.emplace_back(s.data(), s.size()); // full (padded) width for CHAR
}
OwnedSlice page = build_page_typed<BuilderT, Type>(slices);
const Slice raw = page.slice();
BlockCompressionCodec* codec = zstd_codec();
faststring compressed;
Status st = codec->compress(raw, &compressed);
CHECK(st.ok()) << st;
const size_t zstd_bytes = compressed.size();
for (auto _ : state) {
compressed.clear();
st = codec->compress(raw, &compressed);
CHECK(st.ok()) << st;
benchmark::DoNotOptimize(compressed);
benchmark::ClobberMemory();
}
state.SetBytesProcessed(static_cast<int64_t>(state.iterations()) *
static_cast<int64_t>(raw.size));
state.counters["num_elems"] = static_cast<double>(num_elems);
state.counters["raw_bytes"] = static_cast<double>(raw.size);
state.counters["zstd_bytes"] = static_cast<double>(zstd_bytes);
state.counters["zstd_ratio"] =
static_cast<double>(raw.size) / static_cast<double>(std::max<size_t>(zstd_bytes, 1));
}
inline void BM_ZstdSize_V2_Varchar(benchmark::State& state) {
run_zstd_size_bm<BinaryPlainPageV2Builder, FieldType::OLAP_FIELD_TYPE_VARCHAR, false>(state);
}
inline void BM_ZstdSize_V3_Varchar(benchmark::State& state) {
run_zstd_size_bm<BinaryPlainPageV3Builder, FieldType::OLAP_FIELD_TYPE_VARCHAR, false>(state);
}
inline void BM_ZstdSize_V2_Char(benchmark::State& state) {
run_zstd_size_bm<BinaryPlainPageV2Builder, FieldType::OLAP_FIELD_TYPE_CHAR, true>(state);
}
inline void BM_ZstdSize_V3_Char(benchmark::State& state) {
run_zstd_size_bm<BinaryPlainPageV3Builder, FieldType::OLAP_FIELD_TYPE_CHAR, true>(state);
}
// (num_elems, value_len) grid.
static void V2V3ZstdSizeArgs(benchmark::internal::Benchmark* b) {
for (int n : {1024, 16384}) {
for (int len : {8, 32, 128, 1024}) {
b->Args({n, len});
}
}
}
BENCHMARK(BM_ZstdSize_V2_Varchar)->Apply(V2V3ZstdSizeArgs);
BENCHMARK(BM_ZstdSize_V3_Varchar)->Apply(V2V3ZstdSizeArgs);
BENCHMARK(BM_ZstdSize_V2_Char)->Apply(V2V3ZstdSizeArgs);
BENCHMARK(BM_ZstdSize_V3_Char)->Apply(V2V3ZstdSizeArgs);
} // namespace segment_v2
} // namespace doris