Skip to content

Commit 8ec880e

Browse files
committed
feat: add lru
Signed-off-by: Jason <libevent@yeah.net>
1 parent d93c775 commit 8ec880e

9 files changed

Lines changed: 718 additions & 46 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,5 @@ dependency-reduced-pom.xml
8282

8383
# Vibe coding
8484
CLAUDE.md
85+
.workbuddy
86+
CMakeUserPresets.json

cpp/benchmarks/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,4 @@ endmacro()
7171
add_benchmark(arrow_chunk_reader_benchmark SRCS arrow_chunk_reader_benchmark.cc)
7272
add_benchmark(label_filter_benchmark SRCS label_filter_benchmark.cc)
7373
add_benchmark(graph_info_benchmark SRCS graph_info_benchmark.cc)
74+
add_benchmark(lru_cache_benchmark SRCS lru_cache_benchmark.cc)

cpp/benchmarks/benchmark_util.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,20 @@ class BenchmarkFixture : public ::benchmark::Fixture {
4646
}
4747
path_ = std::string(c_root) + "/ldbc_sample/parquet/ldbc_sample.graph.yml";
4848
auto maybe_graph_info = GraphInfo::Load(path_);
49+
if (!maybe_graph_info.has_value()) {
50+
throw std::runtime_error(
51+
"Failed to load graph info: " +
52+
maybe_graph_info.error().message());
53+
}
4954
graph_info_ = maybe_graph_info.value();
5055

5156
second_path_ = std::string(c_root) + "/ldbc/parquet/ldbc.graph.yml";
5257
auto second_maybe_graph_info = GraphInfo::Load(second_path_);
53-
second_graph_info_ = second_maybe_graph_info.value();
58+
if (second_maybe_graph_info.has_value()) {
59+
second_graph_info_ = second_maybe_graph_info.value();
60+
}
61+
// If second_path_ is not available, second_graph_info_ remains nullptr.
62+
// Benchmarks that need it will skip gracefully.
5463
}
5564

5665
void TearDown(const ::benchmark::State& state) override {}
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include <random>
21+
#include <string>
22+
#include <vector>
23+
24+
#include "benchmark/benchmark.h"
25+
26+
#include "./benchmark_util.h"
27+
#include "graphar/api/arrow_reader.h"
28+
#include "graphar/fwd.h"
29+
#include "graphar/lru_cache.h"
30+
31+
namespace graphar {
32+
33+
// ============================================================================
34+
// Raw LRU Cache benchmarks
35+
// ============================================================================
36+
37+
// Benchmark: LRU Cache Put operation with varying cache sizes
38+
static void BM_LRUCachePut(benchmark::State& state) {
39+
const int64_t capacity = state.range(0);
40+
const int64_t num_ops = state.range(1);
41+
LRUCache<int64_t, std::string> cache(capacity);
42+
43+
int64_t i = 0;
44+
for (auto _ : state) {
45+
cache.Put(i % num_ops, "value_" + std::to_string(i % num_ops));
46+
++i;
47+
}
48+
state.SetItemsProcessed(state.iterations());
49+
}
50+
BENCHMARK(BM_LRUCachePut)
51+
->Args({4, 1000})
52+
->Args({4, 10000})
53+
->Args({16, 1000})
54+
->Args({16, 10000})
55+
->Args({64, 1000})
56+
->Args({64, 10000});
57+
58+
// Benchmark: LRU Cache Get with 100% hit rate
59+
static void BM_LRUCacheGetHit(benchmark::State& state) {
60+
const int64_t capacity = state.range(0);
61+
LRUCache<int64_t, std::string> cache(capacity);
62+
for (int64_t i = 0; i < capacity; ++i) {
63+
cache.Put(i, "value_" + std::to_string(i));
64+
}
65+
66+
int64_t i = 0;
67+
for (auto _ : state) {
68+
auto* v = cache.Get(i % capacity);
69+
benchmark::DoNotOptimize(v);
70+
++i;
71+
}
72+
state.SetItemsProcessed(state.iterations());
73+
}
74+
BENCHMARK(BM_LRUCacheGetHit)->Arg(4)->Arg(16)->Arg(64)->Arg(256);
75+
76+
// Benchmark: LRU Cache Get with 100% miss rate
77+
static void BM_LRUCacheGetMiss(benchmark::State& state) {
78+
const int64_t capacity = state.range(0);
79+
LRUCache<int64_t, std::string> cache(capacity);
80+
for (int64_t i = 0; i < capacity; ++i) {
81+
cache.Put(i, "value_" + std::to_string(i));
82+
}
83+
84+
int64_t i = capacity;
85+
for (auto _ : state) {
86+
auto* v = cache.Get(i);
87+
benchmark::DoNotOptimize(v);
88+
++i;
89+
}
90+
state.SetItemsProcessed(state.iterations());
91+
}
92+
BENCHMARK(BM_LRUCacheGetMiss)->Arg(4)->Arg(16)->Arg(64)->Arg(256);
93+
94+
// Benchmark: LRU Cache Mixed operations (80% reads, 20% writes)
95+
static void BM_LRUCacheMixed(benchmark::State& state) {
96+
const int64_t capacity = state.range(0);
97+
const int64_t key_space = state.range(1);
98+
LRUCache<int64_t, std::string> cache(capacity);
99+
100+
// Pre-fill to 50% capacity
101+
for (int64_t i = 0; i < capacity / 2; ++i) {
102+
cache.Put(i, "value_" + std::to_string(i));
103+
}
104+
105+
int64_t i = capacity / 2;
106+
for (auto _ : state) {
107+
int64_t key = i % key_space;
108+
if (key % 5 == 0) {
109+
// 20% writes
110+
cache.Put(key, "updated_" + std::to_string(key));
111+
} else {
112+
// 80% reads
113+
auto* v = cache.Get(key);
114+
benchmark::DoNotOptimize(v);
115+
}
116+
++i;
117+
}
118+
state.SetItemsProcessed(state.iterations());
119+
}
120+
BENCHMARK(BM_LRUCacheMixed)
121+
->Args({4, 100})
122+
->Args({16, 100})
123+
->Args({64, 1000})
124+
->Args({256, 1000});
125+
126+
// ============================================================================
127+
// Chunk Reader LRU Cache benchmarks
128+
// These benchmarks measure the performance impact of the LRU cache within
129+
// the chunk readers. The cache is always compiled in; whether it is
130+
// effective is controlled by the cache capacity (set via FilterOptions or
131+
// the reader constructor). When capacity is 0 or 1, caching is
132+
// effectively disabled (every access reads from the filesystem).
133+
// ============================================================================
134+
135+
// Benchmark: VertexPropertyArrowChunkReader - repeated access to same chunk
136+
BENCHMARK_DEFINE_F(BenchmarkFixture, VertexPropertyChunkReaderCacheHit)
137+
(::benchmark::State& state) { // NOLINT
138+
auto gp = graph_info_->GetVertexInfo("person")->GetPropertyGroup("firstName");
139+
auto maybe_reader =
140+
VertexPropertyArrowChunkReader::Make(graph_info_, "person", gp);
141+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
142+
auto reader = maybe_reader.value();
143+
144+
for (auto _ : state) {
145+
// Seek back to chunk 0 repeatedly - should hit cache on subsequent calls
146+
auto st = reader->seek(0);
147+
SKIP_WITH_ERROR_STATUS(state, st);
148+
auto chunk_result = reader->GetChunk();
149+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
150+
}
151+
state.SetItemsProcessed(state.iterations());
152+
}
153+
154+
// Benchmark: VertexPropertyArrowChunkReader - sequential scan (interleaved
155+
// access)
156+
BENCHMARK_DEFINE_F(BenchmarkFixture,
157+
VertexPropertyChunkReaderSequentialScanWithRepeat)
158+
(::benchmark::State& state) { // NOLINT
159+
auto gp = graph_info_->GetVertexInfo("person")->GetPropertyGroup("firstName");
160+
auto maybe_reader =
161+
VertexPropertyArrowChunkReader::Make(graph_info_, "person", gp);
162+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
163+
auto reader = maybe_reader.value();
164+
165+
// Read first 4 chunks, then repeat - exercises LRU with capacity=4
166+
for (auto _ : state) {
167+
for (int64_t chunk = 0; chunk < 4; ++chunk) {
168+
auto st = reader->seek(static_cast<IdType>(chunk));
169+
SKIP_WITH_ERROR_STATUS(state, st);
170+
auto chunk_result = reader->GetChunk();
171+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
172+
}
173+
// Second pass through same chunks - should be all cache hits
174+
for (int64_t chunk = 0; chunk < 4; ++chunk) {
175+
auto st = reader->seek(static_cast<IdType>(chunk));
176+
SKIP_WITH_ERROR_STATUS(state, st);
177+
auto chunk_result = reader->GetChunk();
178+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
179+
}
180+
}
181+
state.SetItemsProcessed(state.iterations() * 8);
182+
}
183+
184+
// Benchmark: AdjListArrowChunkReader - repeated seek to same vertex
185+
BENCHMARK_DEFINE_F(BenchmarkFixture, AdjListChunkReaderCacheHit)
186+
(::benchmark::State& state) { // NOLINT
187+
auto maybe_reader = AdjListArrowChunkReader::Make(
188+
graph_info_, "person", "knows", "person", AdjListType::ordered_by_source);
189+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
190+
auto reader = maybe_reader.value();
191+
192+
for (auto _ : state) {
193+
// Seek to the same source vertex repeatedly - should hit cache
194+
auto st = reader->seek_src(0);
195+
SKIP_WITH_ERROR_STATUS(state, st);
196+
auto chunk_result = reader->GetChunk();
197+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
198+
}
199+
state.SetItemsProcessed(state.iterations());
200+
}
201+
202+
// Benchmark: AdjListArrowChunkReader - iterate through vertex chunks and
203+
// re-visit the first one
204+
BENCHMARK_DEFINE_F(BenchmarkFixture,
205+
AdjListChunkReaderIterateThenRepeat)
206+
(::benchmark::State& state) { // NOLINT
207+
auto maybe_reader = AdjListArrowChunkReader::Make(
208+
graph_info_, "person", "knows", "person", AdjListType::ordered_by_source);
209+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
210+
auto reader = maybe_reader.value();
211+
212+
for (auto _ : state) {
213+
// Visit vertex chunks 0-3 sequentially
214+
for (IdType v = 0; v < 4; ++v) {
215+
auto st = reader->seek_src(v);
216+
SKIP_WITH_ERROR_STATUS(state, st);
217+
auto chunk_result = reader->GetChunk();
218+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
219+
}
220+
// Re-visit chunk 0 - should be evicted and cause cache miss,
221+
// demonstrating LRU eviction behavior with capacity=4
222+
{
223+
auto st = reader->seek_src(0);
224+
SKIP_WITH_ERROR_STATUS(state, st);
225+
auto chunk_result = reader->GetChunk();
226+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
227+
}
228+
}
229+
state.SetItemsProcessed(state.iterations() * 5);
230+
}
231+
232+
// Benchmark: AdjListOffsetArrowChunkReader - repeated offset reads
233+
BENCHMARK_DEFINE_F(BenchmarkFixture, AdjListOffsetChunkReaderCacheHit)
234+
(::benchmark::State& state) { // NOLINT
235+
auto maybe_reader = AdjListOffsetArrowChunkReader::Make(
236+
graph_info_, "person", "knows", "person",
237+
AdjListType::ordered_by_source);
238+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
239+
auto reader = maybe_reader.value();
240+
241+
for (auto _ : state) {
242+
auto st = reader->seek(0);
243+
SKIP_WITH_ERROR_STATUS(state, st);
244+
auto chunk_result = reader->GetChunk();
245+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
246+
}
247+
state.SetItemsProcessed(state.iterations());
248+
}
249+
250+
// Benchmark: AdjListPropertyArrowChunkReader - repeated property reads
251+
BENCHMARK_DEFINE_F(BenchmarkFixture, AdjListPropertyChunkReaderCacheHit)
252+
(::benchmark::State& state) { // NOLINT
253+
auto maybe_reader = AdjListPropertyArrowChunkReader::Make(
254+
graph_info_, "person", "knows", "person", "creationDate",
255+
AdjListType::ordered_by_source);
256+
SKIP_WITH_ERROR_STATUS(state, maybe_reader.status());
257+
auto reader = maybe_reader.value();
258+
259+
for (auto _ : state) {
260+
auto st = reader->seek_src(0);
261+
SKIP_WITH_ERROR_STATUS(state, st);
262+
auto chunk_result = reader->GetChunk();
263+
SKIP_WITH_ERROR_STATUS(state, chunk_result.status());
264+
}
265+
state.SetItemsProcessed(state.iterations());
266+
}
267+
268+
// ============================================================================
269+
// Register all benchmarks
270+
// ============================================================================
271+
272+
BENCHMARK_REGISTER_F(BenchmarkFixture, VertexPropertyChunkReaderCacheHit);
273+
BENCHMARK_REGISTER_F(BenchmarkFixture,
274+
VertexPropertyChunkReaderSequentialScanWithRepeat);
275+
BENCHMARK_REGISTER_F(BenchmarkFixture, AdjListChunkReaderCacheHit);
276+
BENCHMARK_REGISTER_F(BenchmarkFixture,
277+
AdjListChunkReaderIterateThenRepeat);
278+
BENCHMARK_REGISTER_F(BenchmarkFixture, AdjListOffsetChunkReaderCacheHit);
279+
BENCHMARK_REGISTER_F(BenchmarkFixture, AdjListPropertyChunkReaderCacheHit);
280+
281+
} // namespace graphar

0 commit comments

Comments
 (0)