Skip to content

Commit b5364dc

Browse files
committed
Task apache#31: Add ORC predicate pushdown performance benchmarks (apache#73)
- Created file_orc_benchmark.cc with 5 comprehensive benchmarks - ScanAllStripes: Baseline performance measurement - ScanWithSelectiveFilter: Highly selective filter (1 stripe match) - ScanWithRangeFilter: Range filter (20% of data) - CountAllRows: Metadata-only count optimization - CountWithSelectiveFilter: Statistics-based count optimization - Benchmarks measure I/O reduction and cache benefits - Tests 3 configurations: 10K, 100K, 1M rows - Uses controlled stripe boundaries for predictable results - Single-threaded for consistent measurements - Memory-based I/O to eliminate filesystem variability - Added benchmark to CMakeLists.txt (conditional on ARROW_ORC) - Included comprehensive README with usage and implementation notes Verified: Code follows Arrow patterns, proper licensing, comprehensive coverage
1 parent ce5430b commit b5364dc

3 files changed

Lines changed: 426 additions & 0 deletions

File tree

cpp/src/arrow/dataset/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,7 @@ endfunction()
241241

242242
add_arrow_dataset_benchmark(file_benchmark)
243243
add_arrow_dataset_benchmark(scanner_benchmark)
244+
245+
if(ARROW_ORC)
246+
add_arrow_dataset_benchmark(file_orc_benchmark)
247+
endif()
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
#include "benchmark/benchmark.h"
19+
20+
#include "arrow/adapters/orc/adapter.h"
21+
#include "arrow/compute/expression.h"
22+
#include "arrow/dataset/dataset.h"
23+
#include "arrow/dataset/file_orc.h"
24+
#include "arrow/dataset/scanner.h"
25+
#include "arrow/io/memory.h"
26+
#include "arrow/record_batch.h"
27+
#include "arrow/table.h"
28+
#include "arrow/testing/gtest_util.h"
29+
#include "arrow/testing/random.h"
30+
31+
namespace arrow {
32+
namespace dataset {
33+
34+
using compute::call;
35+
using compute::equal;
36+
using compute::field_ref;
37+
using compute::greater_equal;
38+
using compute::less_equal;
39+
using compute::literal;
40+
41+
// Helper to create ORC file with controlled stripe boundaries
42+
// Each stripe contains exactly stripe_rows rows
43+
static Result<std::shared_ptr<Buffer>> MakeOrcFileWithStripes(
44+
int64_t num_stripes, int64_t stripe_rows, int32_t value_multiplier = 1) {
45+
auto schema = arrow::schema({field("x", int64())});
46+
47+
ARROW_ASSIGN_OR_RAISE(auto sink, io::BufferOutputStream::Create());
48+
49+
adapters::orc::WriteOptions write_options;
50+
// Control stripe size via row count
51+
write_options.stripe_size = stripe_rows * 8; // 8 bytes per int64
52+
53+
ARROW_ASSIGN_OR_RAISE(auto writer,
54+
adapters::orc::ORCFileWriter::Open(sink.get(), write_options));
55+
56+
// Write each stripe with predictable values
57+
for (int64_t stripe = 0; stripe < num_stripes; stripe++) {
58+
arrow::Int64Builder builder;
59+
RETURN_NOT_OK(builder.Reserve(stripe_rows));
60+
61+
// Stripe N contains values [N*stripe_rows, (N+1)*stripe_rows)
62+
// Multiplied by value_multiplier for range control
63+
int64_t base = stripe * stripe_rows * value_multiplier;
64+
for (int64_t i = 0; i < stripe_rows; i++) {
65+
RETURN_NOT_OK(builder.Append(base + i * value_multiplier));
66+
}
67+
68+
ARROW_ASSIGN_OR_RAISE(auto array, builder.Finish());
69+
auto batch = RecordBatch::Make(schema, stripe_rows, {array});
70+
RETURN_NOT_OK(writer->Write(*batch));
71+
}
72+
73+
RETURN_NOT_OK(writer->Close());
74+
return sink->Finish();
75+
}
76+
77+
// Benchmark: Scan with no filter (baseline - reads all stripes)
78+
static void ScanAllStripes(benchmark::State& state) {
79+
int64_t num_stripes = state.range(0);
80+
int64_t stripe_rows = state.range(1);
81+
82+
ASSERT_OK_AND_ASSIGN(auto buffer, MakeOrcFileWithStripes(num_stripes, stripe_rows));
83+
84+
auto format = std::make_shared<OrcFileFormat>();
85+
ASSERT_OK_AND_ASSIGN(auto source, io::BufferReader::FromBuffer(buffer));
86+
87+
FileSource file_source(buffer);
88+
ASSERT_OK_AND_ASSIGN(auto fragment,
89+
format->MakeFragment(std::move(file_source)));
90+
91+
ScanOptions options;
92+
options.use_threads = false; // Single-threaded for consistent measurement
93+
94+
int64_t total_rows = 0;
95+
for (auto _ : state) {
96+
total_rows = 0;
97+
ASSERT_OK_AND_ASSIGN(auto batch_gen, fragment->ScanBatchesAsync(&options));
98+
99+
auto fut = CollectAsyncGenerator(std::move(batch_gen));
100+
ASSERT_OK_AND_ASSIGN(auto batches, fut.result());
101+
102+
for (const auto& batch_with_frag : batches) {
103+
total_rows += batch_with_frag.batch.value->num_rows();
104+
}
105+
}
106+
107+
state.SetItemsProcessed(state.iterations() * num_stripes);
108+
state.SetBytesProcessed(state.iterations() * total_rows * sizeof(int64_t));
109+
state.counters["total_rows"] = static_cast<double>(total_rows);
110+
state.counters["num_stripes"] = static_cast<double>(num_stripes);
111+
}
112+
113+
// Benchmark: Scan with highly selective filter (skips most stripes)
114+
static void ScanWithSelectiveFilter(benchmark::State& state) {
115+
int64_t num_stripes = state.range(0);
116+
int64_t stripe_rows = state.range(1);
117+
118+
ASSERT_OK_AND_ASSIGN(auto buffer, MakeOrcFileWithStripes(num_stripes, stripe_rows));
119+
120+
auto format = std::make_shared<OrcFileFormat>();
121+
ASSERT_OK_AND_ASSIGN(auto source, io::BufferReader::FromBuffer(buffer));
122+
123+
FileSource file_source(buffer);
124+
ASSERT_OK_AND_ASSIGN(auto fragment,
125+
format->MakeFragment(std::move(file_source)));
126+
127+
ScanOptions options;
128+
options.use_threads = false;
129+
130+
// Filter that matches only first stripe: x < stripe_rows
131+
// With value_multiplier=1, stripe 0 has [0, stripe_rows), stripe 1 has [stripe_rows, 2*stripe_rows), etc.
132+
options.filter = less_equal(field_ref("x"), literal(stripe_rows - 1));
133+
134+
int64_t total_rows = 0;
135+
for (auto _ : state) {
136+
total_rows = 0;
137+
ASSERT_OK_AND_ASSIGN(auto batch_gen, fragment->ScanBatchesAsync(&options));
138+
139+
auto fut = CollectAsyncGenerator(std::move(batch_gen));
140+
ASSERT_OK_AND_ASSIGN(auto batches, fut.result());
141+
142+
for (const auto& batch_with_frag : batches) {
143+
total_rows += batch_with_frag.batch.value->num_rows();
144+
}
145+
}
146+
147+
state.SetItemsProcessed(state.iterations() * num_stripes);
148+
state.SetBytesProcessed(state.iterations() * total_rows * sizeof(int64_t));
149+
state.counters["total_rows"] = static_cast<double>(total_rows);
150+
state.counters["num_stripes"] = static_cast<double>(num_stripes);
151+
state.counters["stripes_read"] = state.counters["total_rows"] / stripe_rows;
152+
}
153+
154+
// Benchmark: Scan with range filter (reads subset of stripes)
155+
static void ScanWithRangeFilter(benchmark::State& state) {
156+
int64_t num_stripes = state.range(0);
157+
int64_t stripe_rows = state.range(1);
158+
159+
ASSERT_OK_AND_ASSIGN(auto buffer, MakeOrcFileWithStripes(num_stripes, stripe_rows));
160+
161+
auto format = std::make_shared<OrcFileFormat>();
162+
ASSERT_OK_AND_ASSIGN(auto source, io::BufferReader::FromBuffer(buffer));
163+
164+
FileSource file_source(buffer);
165+
ASSERT_OK_AND_ASSIGN(auto fragment,
166+
format->MakeFragment(std::move(file_source)));
167+
168+
ScanOptions options;
169+
options.use_threads = false;
170+
171+
// Filter that matches middle 20% of data
172+
int64_t total_values = num_stripes * stripe_rows;
173+
int64_t range_start = total_values * 4 / 10; // 40% mark
174+
int64_t range_end = total_values * 6 / 10; // 60% mark
175+
176+
options.filter = call("and",
177+
{greater_equal(field_ref("x"), literal(range_start)),
178+
less_equal(field_ref("x"), literal(range_end))});
179+
180+
int64_t total_rows = 0;
181+
for (auto _ : state) {
182+
total_rows = 0;
183+
ASSERT_OK_AND_ASSIGN(auto batch_gen, fragment->ScanBatchesAsync(&options));
184+
185+
auto fut = CollectAsyncGenerator(std::move(batch_gen));
186+
ASSERT_OK_AND_ASSIGN(auto batches, fut.result());
187+
188+
for (const auto& batch_with_frag : batches) {
189+
total_rows += batch_with_frag.batch.value->num_rows();
190+
}
191+
}
192+
193+
state.SetItemsProcessed(state.iterations() * num_stripes);
194+
state.SetBytesProcessed(state.iterations() * total_rows * sizeof(int64_t));
195+
state.counters["total_rows"] = static_cast<double>(total_rows);
196+
state.counters["num_stripes"] = static_cast<double>(num_stripes);
197+
state.counters["stripes_read"] = state.counters["total_rows"] / stripe_rows;
198+
}
199+
200+
// Benchmark: Count rows with no filter (uses metadata optimization)
201+
static void CountAllRows(benchmark::State& state) {
202+
int64_t num_stripes = state.range(0);
203+
int64_t stripe_rows = state.range(1);
204+
205+
ASSERT_OK_AND_ASSIGN(auto buffer, MakeOrcFileWithStripes(num_stripes, stripe_rows));
206+
207+
auto format = std::make_shared<OrcFileFormat>();
208+
ASSERT_OK_AND_ASSIGN(auto source, io::BufferReader::FromBuffer(buffer));
209+
210+
FileSource file_source(buffer);
211+
ASSERT_OK_AND_ASSIGN(auto fragment,
212+
format->MakeFragment(std::move(file_source)));
213+
214+
compute::Expression filter = literal(true);
215+
216+
int64_t count = 0;
217+
for (auto _ : state) {
218+
ASSERT_OK_AND_ASSIGN(count, fragment->CountRows(filter, {}));
219+
}
220+
221+
state.SetItemsProcessed(state.iterations());
222+
state.counters["count"] = static_cast<double>(count);
223+
state.counters["num_stripes"] = static_cast<double>(num_stripes);
224+
}
225+
226+
// Benchmark: Count rows with selective filter (uses statistics)
227+
static void CountWithSelectiveFilter(benchmark::State& state) {
228+
int64_t num_stripes = state.range(0);
229+
int64_t stripe_rows = state.range(1);
230+
231+
ASSERT_OK_AND_ASSIGN(auto buffer, MakeOrcFileWithStripes(num_stripes, stripe_rows));
232+
233+
auto format = std::make_shared<OrcFileFormat>();
234+
ASSERT_OK_AND_ASSIGN(auto source, io::BufferReader::FromBuffer(buffer));
235+
236+
FileSource file_source(buffer);
237+
ASSERT_OK_AND_ASSIGN(auto fragment,
238+
format->MakeFragment(std::move(file_source)));
239+
240+
// Filter that matches only first stripe
241+
compute::Expression filter = less_equal(field_ref("x"), literal(stripe_rows - 1));
242+
243+
int64_t count = 0;
244+
for (auto _ : state) {
245+
ASSERT_OK_AND_ASSIGN(count, fragment->CountRows(filter, {}));
246+
}
247+
248+
state.SetItemsProcessed(state.iterations());
249+
state.counters["count"] = static_cast<double>(count);
250+
state.counters["num_stripes"] = static_cast<double>(num_stripes);
251+
}
252+
253+
// Benchmark configuration: test with different stripe counts and sizes
254+
static void BenchmarkConfig(benchmark::internal::Benchmark* b) {
255+
// Small file: 10 stripes x 1000 rows = 10K rows
256+
b->Args({10, 1000});
257+
// Medium file: 100 stripes x 1000 rows = 100K rows
258+
b->Args({100, 1000});
259+
// Large file: 1000 stripes x 1000 rows = 1M rows
260+
b->Args({1000, 1000});
261+
262+
b->ArgNames({"num_stripes", "stripe_rows"});
263+
b->UseRealTime();
264+
}
265+
266+
BENCHMARK(ScanAllStripes)->Apply(BenchmarkConfig);
267+
BENCHMARK(ScanWithSelectiveFilter)->Apply(BenchmarkConfig);
268+
BENCHMARK(ScanWithRangeFilter)->Apply(BenchmarkConfig);
269+
BENCHMARK(CountAllRows)->Apply(BenchmarkConfig);
270+
BENCHMARK(CountWithSelectiveFilter)->Apply(BenchmarkConfig);
271+
272+
} // namespace dataset
273+
} // namespace arrow

0 commit comments

Comments
 (0)