Files
doris/be/benchmark/selection_compaction_bench.cpp
HappenLee 9c8f7b2d2e [improvement](be) Compact selected row indices with mask lookups (#69818)
`SegmentIterator` expands partially selected SIMD masks one set bit at a
time, creating a serial dependency for every selected row. Expand x86
masks with more than two selected rows using a constexpr eight-row
lookup table and fixed-width stores. Keep the existing bit iteration for
sparse masks and other architectures. Empty/full masks, scalar tails,
row order and downstream column materialization retain their existing
behavior.

The output selector already has one slot per input row. At a complete
SIMD block, the number of emitted indices is at most the input position,
so the fixed-width stores fit within that allocation; only the returned
prefix is consumed. The lookup table has constant initialization and
introduces no dynamic allocation or mutable shared state.

Add unit coverage and a standalone benchmark that measures the original
loop and the actual production helper. On an Intel Xeon Platinum 8457C,
with 4096-row batches, random 50% selection and Clang 20.1.8 `-O3
-march=native`:

| Benchmark | Before (ns/input row) | After (ns/input row) | Speedup |
| --- | ---: | ---: | ---: |
| Row-index generation | 0.693280 | 0.119248 | 5.81x |
| Predicate comparison + selection + 8-byte copy | 1.170464 | 0.643518 |
1.82x |

Performance tradeoff: Clang 16.0.6 with `-march=x86-64-v3` regresses
from 0.104319 to 0.119946 ns/input row at random 1% selection (about 15%
slower). The Clang 20 native 1% case is approximately unchanged. These
are standalone measurements; complete SQL query speedup has not been
measured.

### Release note

None

### Check List (For Author)

- Test
    - [ ] Regression test
    - [ ] Unit Test
    - [x] Manual test (details below)

Validation completed:

- Standalone benchmark with native and AVX2 compiler settings.
- ASan + UBSan verification of all 256 byte masks, 65536 mixed masks,
scalar tails, row order, output bounds, near-uint16 row limits and
payload copies.
- clang-format 16.0.6, repository build-hygiene checks and focused
clang-tidy for the helper, test source and standalone benchmark.
- Prepared this PR as one commit on the latest master; the helper, test
and benchmark source blobs are identical to the verified versions.

Unit tests added: empty/full/single-bit masks, exhaustive byte and mixed
masks, large row bases and consecutive appends. They have **not executed
through the standard BE test runner**. Both `./run-be-ut.sh -j 48 --run
--filter=SelectionTest.*:BitsTest.*` and `BUILD_TYPE=ASAN ./build.sh
--be -j 48` were attempted in the development workspace, but compilation
failed in unrelated existing local `ColumnVector` edits at
`column_vector.h:95` before the tests could execute. Full BE build and
full `SegmentIterator` static analysis remain incomplete. No SQL or ARM
execution.

Standalone reproduction (x86, from the repository root):

```bash
c++ -O3 -DNDEBUG -std=c++20 -march=native -Ibe/src \
    be/benchmark/selection_compaction_bench.cpp -o /tmp/selection_compaction_bench
/tmp/selection_compaction_bench --verify-only
/tmp/selection_compaction_bench --pattern random --rate 50 --mode select \
    --repeats 9 --min-ms 60
/tmp/selection_compaction_bench --pattern random --rate 50 --input values \
    --mode copy8 --repeats 9 --min-ms 80
`
2026-10-30 15:44:06 +08:00

670 lines
28 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.
// Standalone benchmark; no Doris libraries or Google Benchmark are required.
// Build from the repository root:
// c++ -O3 -DNDEBUG -std=c++20 -march=x86-64-v3 -Ibe/src be/benchmark/selection_compaction_bench.cpp -o /tmp/selection_compaction_bench
// Run: taskset -c <cpu> /tmp/selection_compaction_bench --help
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
#include "util/simd/bits.h"
#include "util/simd/selection.h"
namespace selection_compaction_bench {
using RowId = uint16_t;
using Clock = std::chrono::steady_clock;
using Select = size_t (*)(const int32_t*, size_t, RowId*);
using Compact = uint16_t (*)(const uint8_t*, uint16_t, RowId*);
constexpr int32_t THRESHOLD = 100;
constexpr size_t COMPARISON_BATCH = 64;
constexpr RowId CANARY = 0xabcd;
struct SelectionTable {
std::array<std::array<RowId, 8>, 256> positions {};
std::array<uint8_t, 256> counts {};
constexpr SelectionTable() {
for (size_t mask = 0; mask < 256; ++mask) {
for (size_t bit = 0; bit < 8; ++bit) {
if ((mask & (1U << bit)) != 0) {
positions[mask][counts[mask]++] = static_cast<RowId>(bit);
}
}
}
}
};
constexpr SelectionTable SELECTION_TABLE;
static_assert(doris::simd::bits_mask_length() == 32,
"The lookup candidate in this benchmark implements the x86 32-row mask layout");
inline uint16_t append_indices32(uint32_t mask, uint16_t base, RowId* output) {
uint16_t written = 0;
for (unsigned offset = 0; offset < 32; offset += 8) {
const auto part = static_cast<uint8_t>(mask >> offset);
RowId indices[8];
for (unsigned lane = 0; lane < 8; ++lane) {
indices[lane] =
static_cast<RowId>(base + offset + SELECTION_TABLE.positions[part][lane]);
}
// At most base + offset rows have been emitted, so eight stores fit in the input size.
std::memcpy(output + written, indices, sizeof(indices));
written += SELECTION_TABLE.counts[part];
}
return written;
}
// SPARSE_LIMIT=-1 is the original SegmentIterator compaction loop; -2 uses the production
// append helper. Zero selects lookup for every partial mask. Positive limits retain ctz for
// sparse partial masks. Zero/full-mask handling and the scalar tail stay identical.
template <int SPARSE_LIMIT>
__attribute__((noinline)) uint16_t doris_compact(const uint8_t* flags, uint16_t selected_size,
RowId* output) {
uint16_t new_size = 0;
uint16_t sel_pos = 0;
const uint16_t sel_end = sel_pos + selected_size;
static constexpr size_t SIMD_BYTES = doris::simd::bits_mask_length();
const uint16_t sel_end_simd = sel_pos + selected_size / SIMD_BYTES * SIMD_BYTES;
while (sel_pos < sel_end_simd) {
auto mask = doris::simd::bytes_mask_to_bits_mask(flags + sel_pos);
if (mask == 0) {
// No selected rows.
} else if (mask == doris::simd::bits_mask_all()) {
for (uint16_t i = 0; i < SIMD_BYTES; ++i) {
output[new_size++] = sel_pos + i;
}
} else {
if constexpr (SPARSE_LIMIT == -2) {
if (__builtin_popcount(mask) > 2) {
new_size +=
doris::simd::append_selected_indices(mask, sel_pos, output + new_size);
} else {
doris::simd::iterate_through_bits_mask(
[&](const int bit_pos) {
output[new_size++] = sel_pos + static_cast<uint16_t>(bit_pos);
},
mask);
}
} else if constexpr (SPARSE_LIMIT == -1) {
doris::simd::iterate_through_bits_mask(
[&](const int bit_pos) {
output[new_size++] = sel_pos + static_cast<uint16_t>(bit_pos);
},
mask);
} else {
if (SPARSE_LIMIT > 0 && __builtin_popcount(mask) <= SPARSE_LIMIT) {
doris::simd::iterate_through_bits_mask(
[&](const int bit_pos) {
output[new_size++] = sel_pos + static_cast<uint16_t>(bit_pos);
},
mask);
} else {
new_size += append_indices32(mask, sel_pos, output + new_size);
}
}
}
sel_pos += SIMD_BYTES;
}
for (; sel_pos < sel_end; ++sel_pos) {
if (flags[sel_pos]) {
output[new_size++] = sel_pos;
}
}
return new_size;
}
__attribute__((noinline)) void evaluate(const int32_t* __restrict values, size_t rows,
uint8_t* __restrict flags) {
for (size_t i = 0; i < rows; ++i) {
flags[i] = values[i] < THRESHOLD;
}
}
__attribute__((noinline)) size_t branched(const int32_t* __restrict values, size_t rows,
RowId* __restrict output) {
size_t count = 0;
for (size_t i = 0; i < rows; ++i) {
if (values[i] < THRESHOLD) {
output[count++] = static_cast<RowId>(i);
}
}
return count;
}
__attribute__((noinline)) size_t branchless(const int32_t* __restrict values, size_t rows,
RowId* __restrict output) {
size_t count = 0;
for (size_t i = 0; i < rows; ++i) {
output[count] = static_cast<RowId>(i);
count += values[i] < THRESHOLD;
}
return count;
}
inline uint8_t pack_eight(const uint8_t* matches) {
uint64_t word;
std::memcpy(&word, matches, sizeof(word));
#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
word = __builtin_bswap64(word);
#endif
return static_cast<uint8_t>((word * UINT64_C(0x0102040810204080)) >> 56);
}
template <bool LOOKUP>
__attribute__((noinline)) size_t batched(const int32_t* __restrict values, size_t rows,
RowId* __restrict output) {
size_t count = 0;
size_t base = 0;
for (; base + COMPARISON_BATCH <= rows; base += COMPARISON_BATCH) {
uint8_t matches[COMPARISON_BATCH];
for (size_t i = 0; i < COMPARISON_BATCH; ++i) {
matches[i] = values[base + i] < THRESHOLD;
}
if constexpr (LOOKUP) {
for (size_t group = 0; group < COMPARISON_BATCH; group += 8) {
const auto mask = pack_eight(matches + group);
RowId positions[8];
for (size_t lane = 0; lane < 8; ++lane) {
positions[lane] = static_cast<RowId>(base + group +
SELECTION_TABLE.positions[mask][lane]);
}
// Always write eight entries; only counts[mask] entries become valid.
// count <= base + group, so a full group fits in rows output entries.
std::memcpy(output + count, positions, sizeof(positions));
count += SELECTION_TABLE.counts[mask];
}
} else {
uint64_t mask = 0;
for (size_t group = 0; group < COMPARISON_BATCH; group += 8) {
mask |= static_cast<uint64_t>(pack_eight(matches + group)) << group;
}
while (mask != 0) {
output[count++] = static_cast<RowId>(base + __builtin_ctzll(mask));
mask &= mask - 1;
}
}
}
for (; base < rows; ++base) {
if (values[base] < THRESHOLD) {
output[count++] = static_cast<RowId>(base);
}
}
return count;
}
enum class Algorithm {
BRANCHED,
BRANCHLESS,
LOOKUP,
CTZ,
FUSED_COPY,
DORIS_CTZ,
DORIS_LOOKUP,
DORIS_HYBRID1,
DORIS_HYBRID2,
DORIS_HYBRID4,
DORIS_HYBRID8,
DORIS_HYBRID16,
DORIS_PRODUCTION
};
constexpr std::array<Select, 4> SELECTORS {branched, branchless, batched<true>, batched<false>};
constexpr std::array<Compact, 8> COMPACTORS {doris_compact<-1>, doris_compact<0>, doris_compact<1>,
doris_compact<2>, doris_compact<4>, doris_compact<8>,
doris_compact<16>, doris_compact<-2>};
constexpr std::array<const char*, 13> NAMES {
"branched", "branchless", "batch_lookup", "batch_ctz", "fused_copy",
"doris_ctz", "doris_lookup", "doris_hybrid1", "doris_hybrid2", "doris_hybrid4",
"doris_hybrid8", "doris_hybrid16", "doris_production"};
template <size_t WORDS>
using Payload = std::array<uint64_t, WORDS>;
template <size_t WORDS>
__attribute__((noinline)) size_t fused_copy(const int32_t* __restrict values,
const Payload<WORDS>* __restrict payload, size_t rows,
Payload<WORDS>* __restrict output) {
size_t count = 0;
for (size_t i = 0; i < rows; ++i) {
if (values[i] < THRESHOLD) {
output[count++] = payload[i];
}
}
return count;
}
struct Options {
size_t rows = 1U << 20;
size_t batch = 4096;
size_t repeats = 7;
double min_ms = 20;
int rate = -1;
std::string pattern = "all";
std::string mode = "all";
std::string suite = "doris";
std::string input = "flags";
bool verify_only = false;
};
template <size_t WORDS>
struct Data {
std::vector<int32_t> values;
std::vector<uint8_t> flags;
std::vector<Payload<WORDS>> payload;
size_t batch;
bool evaluate_values;
Data(size_t rows, size_t batch_size, int rate, const std::string& pattern,
bool with_evaluation = false)
: values(rows), flags(rows), batch(batch_size), evaluate_values(with_evaluation) {
std::mt19937 random(0x51ec710);
for (size_t base = 0; base < rows; base += batch) {
const auto count = std::min(batch, rows - base);
const auto passing = (count * rate + 50) / 100;
for (size_t i = 0; i < count; ++i) {
values[base + i] = i < passing ? static_cast<int32_t>(random() % 100)
: static_cast<int32_t>(100 + random() % 10000);
}
if (pattern == "random") {
std::shuffle(values.begin() + base, values.begin() + base + count, random);
}
}
for (size_t i = 0; i < rows; ++i) {
flags[i] = values[i] < THRESHOLD;
}
if constexpr (WORDS != 0) {
payload.resize(rows);
for (size_t i = 0; i < rows; ++i) {
for (size_t word = 0; word < WORDS; ++word) {
payload[i][word] = random() ^ (static_cast<uint64_t>(i) << 32) ^ word;
}
}
}
}
};
template <size_t WORDS>
struct Scratch {
std::vector<RowId> selection;
std::vector<uint8_t> flags;
std::vector<Payload<WORDS>> copied;
explicit Scratch(size_t batch) : selection(batch + 8, CANARY), flags(batch) {
if constexpr (WORDS != 0) {
copied.resize(batch + 1);
}
}
};
template <size_t WORDS, Algorithm ALGORITHM>
uint64_t run(const Data<WORDS>& data, Scratch<WORDS>& scratch, size_t passes) {
uint64_t total = 0;
for (size_t pass = 0; pass < passes; ++pass) {
for (size_t base = 0; base < data.values.size(); base += data.batch) {
const auto rows = std::min(data.batch, data.values.size() - base);
size_t count;
if constexpr (ALGORITHM == Algorithm::FUSED_COPY) {
count = fused_copy<WORDS>(data.values.data() + base, data.payload.data() + base,
rows, scratch.copied.data());
} else {
if constexpr (ALGORITHM >= Algorithm::DORIS_CTZ) {
const uint8_t* flags = data.flags.data() + base;
if (data.evaluate_values) {
evaluate(data.values.data() + base, rows, scratch.flags.data());
flags = scratch.flags.data();
}
count = COMPACTORS[static_cast<size_t>(ALGORITHM) -
static_cast<size_t>(Algorithm::DORIS_CTZ)](
flags, static_cast<uint16_t>(rows), scratch.selection.data());
} else {
count = SELECTORS[static_cast<size_t>(ALGORITHM)](
data.values.data() + base, rows, scratch.selection.data());
}
if constexpr (WORDS != 0) {
for (size_t i = 0; i < count; ++i) {
scratch.copied[i] = data.payload[base + scratch.selection[i]];
}
}
}
total += count;
// Make every batch's stores observable without timing a checksum over the output.
asm volatile(""
:
: "g"(scratch.selection.data()), "g"(scratch.copied.data())
: "memory");
}
}
asm volatile("" : : "g"(total) : "memory");
return total;
}
void verify_compaction(const uint8_t* flags, size_t rows) {
std::vector<RowId> expected;
for (size_t i = 0; i < rows; ++i) {
if (flags[i]) {
expected.push_back(static_cast<RowId>(i));
}
}
for (auto compact : COMPACTORS) {
std::vector<RowId> output(rows + 8, CANARY);
const auto count = compact(flags, static_cast<uint16_t>(rows), output.data());
if (count != expected.size() ||
!std::equal(expected.begin(), expected.end(), output.begin()) ||
!std::all_of(output.begin() + rows, output.end(),
[](RowId x) { return x == CANARY; })) {
throw std::runtime_error("Doris compaction result, row order, or bounds mismatch");
}
}
}
void verify_selection(const int32_t* values, size_t rows) {
std::vector<uint8_t> flags(rows);
evaluate(values, rows, flags.data());
verify_compaction(flags.data(), rows);
std::vector<RowId> expected;
for (size_t i = 0; i < rows; ++i) {
if (values[i] < THRESHOLD) {
expected.push_back(static_cast<RowId>(i));
}
}
for (auto select : SELECTORS) {
std::vector<RowId> output(rows + 8, CANARY);
const auto count = select(values, rows, output.data());
if (count != expected.size() ||
!std::equal(expected.begin(), expected.end(), output.begin()) ||
!std::all_of(output.begin() + rows, output.end(),
[](RowId x) { return x == CANARY; })) {
throw std::runtime_error("Selector result, row order, or output bounds mismatch");
}
}
}
template <size_t WORDS>
void verify(const Data<WORDS>& data) {
Scratch<WORDS> scratch(data.batch);
for (size_t base = 0; base < data.values.size(); base += data.batch) {
const auto rows = std::min(data.batch, data.values.size() - base);
verify_selection(data.values.data() + base, rows);
if constexpr (WORDS != 0) {
std::vector<Payload<WORDS>> expected;
for (size_t i = 0; i < rows; ++i) {
if (data.values[base + i] < THRESHOLD) {
expected.push_back(data.payload[base + i]);
}
}
for (auto select : SELECTORS) {
const auto count =
select(data.values.data() + base, rows, scratch.selection.data());
for (size_t i = 0; i < count; ++i) {
scratch.copied[i] = data.payload[base + scratch.selection[i]];
}
if (!std::equal(expected.begin(), expected.end(), scratch.copied.begin())) {
throw std::runtime_error("Selected payload mismatch");
}
}
evaluate(data.values.data() + base, rows, scratch.flags.data());
if (!std::equal(scratch.flags.begin(), scratch.flags.begin() + rows,
data.flags.begin() + base)) {
throw std::runtime_error("Predicate flags mismatch");
}
for (auto compact : COMPACTORS) {
const auto count = compact(scratch.flags.data(), static_cast<uint16_t>(rows),
scratch.selection.data());
for (size_t i = 0; i < count; ++i) {
scratch.copied[i] = data.payload[base + scratch.selection[i]];
}
if (count != expected.size() ||
!std::equal(expected.begin(), expected.end(), scratch.copied.begin())) {
throw std::runtime_error("Doris selected payload mismatch");
}
}
const auto count =
fused_copy<WORDS>(data.values.data() + base, data.payload.data() + base, rows,
scratch.copied.data());
if (count != expected.size() ||
!std::equal(expected.begin(), expected.end(), scratch.copied.begin())) {
throw std::runtime_error("Fused payload mismatch");
}
}
}
}
void self_test() {
verify_selection(nullptr, 0);
for (size_t mask = 0; mask < 256; ++mask) {
std::array<int32_t, 64> values {};
for (size_t i = 0; i < values.size(); ++i) {
values[i] = (mask & (1U << (i % 8))) != 0 ? 0 : THRESHOLD;
}
verify_selection(values.data(), values.size());
}
for (uint32_t mask = 0; mask < 65536; ++mask) {
std::array<uint8_t, 32> flags {};
for (unsigned bit = 0; bit < 16; ++bit) {
flags[bit] = (mask >> bit) & 1;
flags[16 + bit] = ((~mask) >> (15 - bit)) & 1;
}
verify_compaction(flags.data(), flags.size());
}
for (const auto rows : {1, 7, 8, 9, 31, 63, 64, 65, 127, 128, 129, 4095, 4096, 4097, 65535}) {
Data<0> data(rows, rows, 50, "random");
verify(data);
}
for (const auto rate : {0, 1, 10, 90, 99, 100}) {
verify(Data<0>(65535, 65535, rate, "random"));
}
for (const auto batch : {7, 64, 65, 256}) {
for (const auto rate : {0, 1, 10, 50, 90, 99, 100}) {
for (const auto* pattern : {"random", "clustered"}) {
verify(Data<1>(1103, batch, rate, pattern));
verify(Data<8>(1103, batch, rate, pattern));
}
}
}
std::cerr << "Correctness: all 256 table masks, 65536 mixed masks, tails, row order, "
"bounds, and payloads passed\n";
}
template <size_t WORDS>
void benchmark(const Options& options, int rate, const std::string& pattern) {
Data<WORDS> data(options.rows, options.batch, rate, pattern, options.input == "values");
verify(data);
Scratch<WORDS> scratch(options.batch);
using Runner = uint64_t (*)(const Data<WORDS>&, Scratch<WORDS>&, size_t);
std::vector<Runner> runners;
size_t first_algorithm;
if (options.suite == "doris") {
first_algorithm = static_cast<size_t>(Algorithm::DORIS_CTZ);
runners = {run<WORDS, Algorithm::DORIS_CTZ>, run<WORDS, Algorithm::DORIS_LOOKUP>,
run<WORDS, Algorithm::DORIS_HYBRID1>, run<WORDS, Algorithm::DORIS_HYBRID2>,
run<WORDS, Algorithm::DORIS_HYBRID4>, run<WORDS, Algorithm::DORIS_HYBRID8>,
run<WORDS, Algorithm::DORIS_HYBRID16>, run<WORDS, Algorithm::DORIS_PRODUCTION>};
} else {
first_algorithm = 0;
runners = {run<WORDS, Algorithm::BRANCHED>, run<WORDS, Algorithm::BRANCHLESS>,
run<WORDS, Algorithm::LOOKUP>, run<WORDS, Algorithm::CTZ>};
if constexpr (WORDS != 0) {
runners.push_back(run<WORDS, Algorithm::FUSED_COPY>);
}
}
std::vector<size_t> passes(runners.size(), 1);
std::vector<std::vector<double>> samples(runners.size());
const auto target_seconds = options.min_ms / 1000;
for (size_t algorithm = 0; algorithm < runners.size(); ++algorithm) {
double seconds;
do {
const auto start = Clock::now();
runners[algorithm](data, scratch, passes[algorithm]);
seconds = std::chrono::duration<double>(Clock::now() - start).count();
if (seconds < target_seconds / 4) {
passes[algorithm] *= 2;
}
} while (seconds < target_seconds / 4);
passes[algorithm] = std::max<size_t>(
1, static_cast<size_t>(std::ceil(passes[algorithm] * target_seconds / seconds)));
}
// Interleave algorithms, changing their order on every repetition.
std::vector<size_t> order(runners.size());
std::iota(order.begin(), order.end(), 0);
std::mt19937 random(0xbec123);
for (size_t repetition = 0; repetition < options.repeats; ++repetition) {
std::shuffle(order.begin(), order.end(), random);
for (auto algorithm : order) {
const auto start = Clock::now();
runners[algorithm](data, scratch, passes[algorithm]);
const auto ns = std::chrono::duration<double, std::nano>(Clock::now() - start).count();
samples[algorithm].push_back(ns / (passes[algorithm] * options.rows));
}
}
for (auto& sample : samples) {
std::sort(sample.begin(), sample.end());
}
const double baseline = samples[0][options.repeats / 2];
for (size_t algorithm = 0; algorithm < runners.size(); ++algorithm) {
const auto& sample = samples[algorithm];
const double median = sample[options.repeats / 2];
std::cout << pattern << ',' << options.batch << ',' << options.rows << ',' << rate << ','
<< (WORDS == 0 ? "select"
: WORDS == 1 ? "copy8"
: "copy64")
<< ',' << NAMES[first_algorithm + algorithm] << ',' << median << ','
<< baseline / median << ',' << sample.front() << ',' << sample.back() << ','
<< passes[algorithm] << ','
<< (options.suite == "scalar" ? "values" : options.input) << '\n';
}
std::cout.flush();
}
Options parse_options(int argc, char** argv) {
Options options;
for (int i = 1; i < argc; ++i) {
const std::string key = argv[i];
if (key == "--help") {
std::cout << "Options: --rows N --batch N --repeats N --min-ms M --rate 0..100\n"
" --pattern random|clustered|all --mode select|copy8|copy64|all\n"
" --suite doris|scalar --input flags|values\n"
" --verify-only\n"
"Defaults: 1048576 rows, 4096 batch, 7 repeats, 20 ms/sample, all cases.\n"
"Row IDs are uint16; values are int32; copy modes include selection.\n"
"Default suite=doris: current SIMD/ctz, lookup, and hybrid candidates.\n"
"Default input=flags: predicate evaluation is outside timing.\n"
"input=values includes one int32 comparison pass before compaction.\n"
"suite=scalar retains the earlier scalar-versus-batch experiment.\n"
"clustered places passing values first in every batch.\n";
std::exit(0);
}
if (key == "--verify-only") {
options.verify_only = true;
continue;
}
if (i + 1 == argc) {
throw std::invalid_argument("Missing option value: " + key);
}
const std::string value = argv[++i];
if (key == "--rows") {
options.rows = std::stoull(value);
} else if (key == "--batch") {
options.batch = std::stoull(value);
} else if (key == "--repeats") {
options.repeats = std::stoull(value);
} else if (key == "--min-ms") {
options.min_ms = std::stod(value);
} else if (key == "--rate") {
options.rate = std::stoi(value);
} else if (key == "--pattern") {
options.pattern = value;
} else if (key == "--mode") {
options.mode = value;
} else if (key == "--suite") {
options.suite = value;
} else if (key == "--input") {
options.input = value;
} else {
throw std::invalid_argument("Unknown option: " + key);
}
}
if (options.rows == 0 || options.batch == 0 || options.batch > 65535 || options.repeats < 3 ||
options.repeats % 2 == 0 || !std::isfinite(options.min_ms) || options.min_ms <= 0 ||
options.rate < -1 || options.rate > 100 ||
(options.pattern != "random" && options.pattern != "clustered" &&
options.pattern != "all") ||
(options.mode != "select" && options.mode != "copy8" && options.mode != "copy64" &&
options.mode != "all") ||
(options.suite != "doris" && options.suite != "scalar") ||
(options.input != "flags" && options.input != "values")) {
throw std::invalid_argument("Invalid options; use --help for supported values");
}
return options;
}
} // namespace selection_compaction_bench
int main(int argc, char** argv) {
using namespace selection_compaction_bench;
try {
const auto options = parse_options(argc, argv);
self_test();
if (options.verify_only) {
return 0;
}
std::cerr << "Compiler: " << __VERSION__ << "; rows=" << options.rows
<< "; batch=" << options.batch << "; repeats=" << options.repeats
<< "; min_ms=" << options.min_ms << "; suite=" << options.suite
<< "; input=" << options.input << '\n';
std::cout << std::fixed << std::setprecision(6)
<< "pattern,batch,rows,pass_percent,mode,algorithm,ns_per_input_row,"
"speedup_vs_baseline,min_ns_per_row,max_ns_per_row,passes,input\n";
for (const auto* pattern : {"random", "clustered"}) {
if (options.pattern != "all" && options.pattern != pattern) {
continue;
}
const auto rates = options.rate == -1 ? std::vector<int> {0, 1, 10, 50, 90, 99, 100}
: std::vector<int> {options.rate};
for (auto rate : rates) {
if (options.mode == "all" || options.mode == "select") {
benchmark<0>(options, rate, pattern);
}
if (options.mode == "all" || options.mode == "copy8") {
benchmark<1>(options, rate, pattern);
}
if (options.mode == "all" || options.mode == "copy64") {
benchmark<8>(options, rate, pattern);
}
}
}
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
return 0;
}