mirror of
https://github.com/apache/doris.git
synced 2026-11-04 07:31:17 +00:00
V3 layout: |data1..dataN|varuint_len1..varuint_lenN|data_block_size(u32)|num_elems(u32)| Benchmark (median of 10 reps): page pre-decode is ~1.0–3.6x faster than V2 (largest for short values), and the contiguous layout compresses ~1–11% smaller after ZSTD.
369 lines
15 KiB
C++
369 lines
15 KiB
C++
// 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
|