Skip to content

Commit f23d690

Browse files
authored
Merge pull request #17 from poyrazK/feature/optimize-insertion
perf: Zero-copy binary refactor and CLOCK replacer
2 parents 8bc4289 + 9ff15da commit f23d690

14 files changed

Lines changed: 541 additions & 361 deletions

File tree

.github/workflows/ci.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,66 @@ jobs:
140140
with:
141141
name: cloudsql-bin-${{ matrix.compiler }}
142142
path: build/cloudSQL
143+
144+
performance-benchmarks:
145+
needs: style-check
146+
runs-on: ubuntu-latest
147+
steps:
148+
- uses: actions/checkout@v4
149+
150+
- name: Install dependencies
151+
run: |
152+
sudo apt-get update
153+
sudo apt-get install -y cmake clang ninja-build ccache python3
154+
155+
- name: Configure CMake (Release)
156+
run: |
157+
mkdir build
158+
cd build
159+
cmake .. -G Ninja \
160+
-DCMAKE_BUILD_TYPE=Release \
161+
-DBUILD_BENCHMARKS=ON \
162+
-DBUILD_TESTS=OFF
163+
164+
- name: Build Benchmarks
165+
run: |
166+
cd build
167+
ninja storage_bench execution_bench network_bench
168+
169+
- name: Restore Performance Baseline
170+
id: restore-baseline
171+
uses: actions/cache/restore@v4
172+
with:
173+
path: build/baseline.json
174+
key: perf-baseline-${{ runner.os }}-main
175+
176+
- name: Run Benchmarks
177+
run: |
178+
cd build
179+
./storage_bench --benchmark_format=json > storage.json
180+
./execution_bench --benchmark_format=json > execution.json
181+
./network_bench --benchmark_format=json > network.json
182+
183+
# Merge results into one current.json
184+
python3 -c "import json; s=json.load(open('storage.json')); e=json.load(open('execution.json')); n=json.load(open('network.json')); s['benchmarks'].extend(e['benchmarks']); s['benchmarks'].extend(n['benchmarks']); json.dump(s, open('current.json', 'w'))"
185+
186+
- name: Check for Performance Regressions
187+
run: |
188+
if [ -f build/baseline.json ]; then
189+
python3 scripts/check_perf_regression.py build/current.json build/baseline.json 0.20
190+
else
191+
echo "No baseline found to compare against."
192+
fi
193+
194+
- name: Save New Baseline
195+
if: github.ref == 'refs/heads/main'
196+
uses: actions/cache/save@v4
197+
with:
198+
path: build/current.json
199+
key: perf-baseline-${{ runner.os }}-main-${{ github.sha }}
200+
201+
- name: Upload Current Results
202+
uses: actions/upload-artifact@v4
203+
with:
204+
name: performance-results
205+
path: build/current.json

benchmarks/execution_bench.cpp

Lines changed: 46 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,30 @@ static void SetupBenchTable(HeapTable& table, int num_rows) {
2929

3030
static void BM_ExecutionSeqScan(benchmark::State& state) {
3131
std::string test_dir = "./bench_exec_scan_" + std::to_string(state.range(0));
32+
std::filesystem::remove_all(test_dir);
3233
std::filesystem::create_directories(test_dir);
33-
StorageManager disk_manager(test_dir);
34-
BufferPoolManager bpm(2000, disk_manager);
35-
36-
Schema schema;
37-
schema.add_column("id", common::ValueType::TYPE_INT64);
38-
schema.add_column("data", common::ValueType::TYPE_TEXT);
3934

40-
for (auto _ : state) {
41-
state.PauseTiming();
42-
auto table = std::make_unique<HeapTable>("scan_table", bpm, schema);
35+
{
36+
StorageManager disk_manager(test_dir);
37+
BufferPoolManager bpm(2000, disk_manager);
38+
39+
Schema schema;
40+
schema.add_column("id", common::ValueType::TYPE_INT64);
41+
schema.add_column("data", common::ValueType::TYPE_TEXT);
42+
43+
auto table = std::make_shared<HeapTable>("scan_table", bpm, schema);
4344
table->create();
4445
SetupBenchTable(*table, state.range(0));
45-
state.ResumeTiming();
4646

47-
auto scan_op = std::make_unique<SeqScanOperator>(std::move(table));
48-
scan_op->init();
49-
Tuple tuple;
50-
while (scan_op->next(tuple)) {
51-
benchmark::DoNotOptimize(tuple);
47+
for (auto _ : state) {
48+
auto scan_op = std::make_unique<SeqScanOperator>(table);
49+
scan_op->init();
50+
scan_op->open();
51+
Tuple tuple;
52+
while (scan_op->next(tuple)) {
53+
benchmark::DoNotOptimize(tuple);
54+
}
5255
}
53-
54-
state.PauseTiming();
55-
std::filesystem::remove_all(test_dir);
56-
std::filesystem::create_directories(test_dir);
57-
state.ResumeTiming();
5856
}
5957

6058
state.SetItemsProcessed(state.iterations() * state.range(0));
@@ -64,45 +62,43 @@ BENCHMARK(BM_ExecutionSeqScan)->Arg(1000)->Arg(10000);
6462

6563
static void BM_ExecutionHashJoin(benchmark::State& state) {
6664
std::string test_dir = "./bench_exec_join_" + std::to_string(state.range(0));
65+
std::filesystem::remove_all(test_dir);
6766
std::filesystem::create_directories(test_dir);
68-
StorageManager disk_manager(test_dir);
69-
BufferPoolManager bpm(4000, disk_manager);
70-
71-
Schema schema;
72-
schema.add_column("id", common::ValueType::TYPE_INT64);
73-
schema.add_column("data", common::ValueType::TYPE_TEXT);
7467

75-
for (auto _ : state) {
76-
state.PauseTiming();
77-
auto left_table = std::make_unique<HeapTable>("left_table", bpm, schema);
68+
{
69+
StorageManager disk_manager(test_dir);
70+
BufferPoolManager bpm(4000, disk_manager);
71+
72+
Schema schema;
73+
schema.add_column("id", common::ValueType::TYPE_INT64);
74+
schema.add_column("data", common::ValueType::TYPE_TEXT);
75+
76+
auto left_table = std::make_shared<HeapTable>("left_table", bpm, schema);
7877
left_table->create();
7978
SetupBenchTable(*left_table, state.range(0));
8079

81-
auto right_table = std::make_unique<HeapTable>("right_table", bpm, schema);
80+
auto right_table = std::make_shared<HeapTable>("right_table", bpm, schema);
8281
right_table->create();
8382
SetupBenchTable(*right_table, state.range(0));
84-
state.ResumeTiming();
8583

86-
auto left_scan = std::make_unique<SeqScanOperator>(std::move(left_table));
87-
auto right_scan = std::make_unique<SeqScanOperator>(std::move(right_table));
88-
89-
// Join on "id"
90-
auto left_key = std::make_unique<ColumnExpr>("id");
91-
auto right_key = std::make_unique<ColumnExpr>("id");
92-
93-
auto join_op = std::make_unique<HashJoinOperator>(
94-
std::move(left_scan), std::move(right_scan), std::move(left_key), std::move(right_key));
95-
96-
join_op->init();
97-
Tuple tuple;
98-
while (join_op->next(tuple)) {
99-
benchmark::DoNotOptimize(tuple);
84+
for (auto _ : state) {
85+
auto left_scan = std::make_unique<SeqScanOperator>(left_table);
86+
auto right_scan = std::make_unique<SeqScanOperator>(right_table);
87+
88+
// Join on "id"
89+
auto left_key = std::make_unique<ColumnExpr>("id");
90+
auto right_key = std::make_unique<ColumnExpr>("id");
91+
92+
auto join_op = std::make_unique<HashJoinOperator>(
93+
std::move(left_scan), std::move(right_scan), std::move(left_key), std::move(right_key));
94+
95+
join_op->init();
96+
join_op->open();
97+
Tuple tuple;
98+
while (join_op->next(tuple)) {
99+
benchmark::DoNotOptimize(tuple);
100+
}
100101
}
101-
102-
state.PauseTiming();
103-
std::filesystem::remove_all(test_dir);
104-
std::filesystem::create_directories(test_dir);
105-
state.ResumeTiming();
106102
}
107103

108104
state.SetItemsProcessed(state.iterations() * state.range(0));

include/common/value.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class Value {
8585
[[nodiscard]] ValueType type() const { return type_; }
8686
[[nodiscard]] bool is_null() const { return type_ == ValueType::TYPE_NULL; }
8787
[[nodiscard]] bool is_numeric() const;
88+
[[nodiscard]] bool is_integer() const;
89+
[[nodiscard]] bool is_float() const;
8890

8991
[[nodiscard]] bool as_bool() const;
9092
[[nodiscard]] int8_t as_int8() const;
@@ -184,6 +186,15 @@ inline bool Value::is_numeric() const {
184186
type_ == ValueType::TYPE_DECIMAL;
185187
}
186188

189+
inline bool Value::is_integer() const {
190+
return type_ == ValueType::TYPE_INT8 || type_ == ValueType::TYPE_INT16 ||
191+
type_ == ValueType::TYPE_INT32 || type_ == ValueType::TYPE_INT64;
192+
}
193+
194+
inline bool Value::is_float() const {
195+
return type_ == ValueType::TYPE_FLOAT32 || type_ == ValueType::TYPE_FLOAT64;
196+
}
197+
187198
// Accessors
188199
inline bool Value::as_bool() const {
189200
if (type_ != ValueType::TYPE_BOOL) {

include/executor/operator.hpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,13 @@ class Operator {
105105
class SeqScanOperator : public Operator {
106106
private:
107107
std::string table_name_;
108-
std::unique_ptr<storage::HeapTable> table_;
108+
std::shared_ptr<storage::HeapTable> table_;
109109
std::unique_ptr<storage::HeapTable::Iterator> iterator_;
110+
110111
Schema schema_;
111112

112113
public:
113-
explicit SeqScanOperator(std::unique_ptr<storage::HeapTable> table, Transaction* txn = nullptr,
114+
explicit SeqScanOperator(std::shared_ptr<storage::HeapTable> table, Transaction* txn = nullptr,
114115
LockManager* lock_manager = nullptr);
115116

116117
bool init() override;
@@ -153,15 +154,15 @@ class IndexScanOperator : public Operator {
153154
private:
154155
std::string table_name_;
155156
std::string index_name_;
156-
std::unique_ptr<storage::HeapTable> table_;
157+
std::shared_ptr<storage::HeapTable> table_;
157158
std::unique_ptr<storage::BTreeIndex> index_;
158159
common::Value search_key_;
159160
std::vector<storage::HeapTable::TupleId> matching_ids_;
160161
size_t current_match_index_ = 0;
161162
Schema schema_;
162163

163164
public:
164-
IndexScanOperator(std::unique_ptr<storage::HeapTable> table,
165+
IndexScanOperator(std::shared_ptr<storage::HeapTable> table,
165166
std::unique_ptr<storage::BTreeIndex> index, common::Value search_key,
166167
Transaction* txn = nullptr, LockManager* lock_manager = nullptr);
167168

include/storage/heap_table.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class HeapTable {
122122
std::string filename_;
123123
BufferPoolManager& bpm_;
124124
executor::Schema schema_;
125+
uint32_t last_page_id_ = 0;
125126

126127
public:
127128
/**

include/storage/lru_replacer.hpp

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
#ifndef CLOUDSQL_STORAGE_LRU_REPLACER_HPP
77
#define CLOUDSQL_STORAGE_LRU_REPLACER_HPP
88

9-
#include <list>
109
#include <mutex>
11-
#include <unordered_map>
1210
#include <vector>
1311

1412
namespace cloudsql::storage {
@@ -17,8 +15,9 @@ namespace cloudsql::storage {
1715
* @class LRUReplacer
1816
* @brief Tracks page usage and determines which page to evict
1917
*
20-
* Implements a thread-safe LRU policy. Pages that are pinned are
21-
* removed from the replacer. When unpinned, they are added back.
18+
* Implements a CLOCK (Second Chance) replacement policy.
19+
* This implementation is zero-allocation during hot-path (pin/unpin)
20+
* by using fixed-size bitsets.
2221
*/
2322
class LRUReplacer {
2423
public:
@@ -64,8 +63,12 @@ class LRUReplacer {
6463
private:
6564
size_t capacity_;
6665
mutable std::mutex latch_;
67-
std::list<uint32_t> lru_list_;
68-
std::unordered_map<uint32_t, std::list<uint32_t>::iterator> lru_map_;
66+
67+
// CLOCK State
68+
std::vector<bool> in_replacer_; // true if frame is a candidate for eviction
69+
std::vector<bool> referenced_; // "Second chance" bit
70+
size_t clock_hand_ = 0;
71+
size_t current_size_ = 0;
6972
};
7073

7174
} // namespace cloudsql::storage

scripts/check_perf_regression.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python3
2+
import json
3+
import sys
4+
import os
5+
6+
def check_regression(current_file, baseline_file, threshold=0.2):
7+
# Load current results - failure here is a fatal error
8+
try:
9+
with open(current_file) as f:
10+
current = json.load(f)
11+
except Exception as e:
12+
print(f"Error loading current performance results from {current_file}: {e}")
13+
return False
14+
15+
# Load baseline results - missing file is handled gracefully
16+
try:
17+
with open(baseline_file) as f:
18+
baseline = json.load(f)
19+
except FileNotFoundError:
20+
print(f"No baseline found at {baseline_file}. Skipping comparison.")
21+
return True
22+
except Exception as e:
23+
print(f"Error loading baseline performance results from {baseline_file}: {e}")
24+
return False
25+
26+
regressions = []
27+
28+
# Create map of baseline metrics
29+
base_map = {b['name']: b.get('real_time') for b in baseline['benchmarks']}
30+
31+
print(f"{'Benchmark':<40} | {'Old (ns)':<12} | {'New (ns)':<12} | {'Change':<10}")
32+
print("-" * 85)
33+
34+
for b in current['benchmarks']:
35+
name = b['name']
36+
if name in base_map:
37+
old_time = base_map[name]
38+
new_time = b.get('real_time')
39+
40+
if old_time is None or new_time is None:
41+
print(f"{name:<40} | {'N/A':<12} | {'N/A':<12} | {'N/A':>9}")
42+
continue
43+
44+
# Guard against division by zero
45+
if old_time <= 0:
46+
print(f"{name:<40} | {old_time:<12.2f} | {new_time:<12.2f} | {'NEW/ZERO':>9}")
47+
continue
48+
49+
# Increase in time means decrease in performance
50+
change = (new_time - old_time) / old_time
51+
print(f"{name:<40} | {old_time:<12.2f} | {new_time:<12.2f} | {change:>+9.1%}")
52+
53+
if change > threshold:
54+
regressions.append(f"{name} regressed by {change:.1%}")
55+
56+
if regressions:
57+
print("\n!!! PERFORMANCE REGRESSION DETECTED !!!")
58+
for r in regressions:
59+
print(f" - {r}")
60+
return False
61+
62+
print("\nPerformance is within acceptable limits.")
63+
return True
64+
65+
if __name__ == "__main__":
66+
if len(sys.argv) < 3:
67+
print("Usage: check_perf_regression.py <current.json> <baseline.json> [threshold]")
68+
sys.exit(1)
69+
70+
thresh = float(sys.argv[3]) if len(sys.argv) > 3 else 0.2
71+
if not check_regression(sys.argv[1], sys.argv[2], thresh):
72+
sys.exit(1)

src/executor/operator.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
namespace cloudsql::executor {
3030

3131
/* --- SeqScanOperator --- */
32-
33-
SeqScanOperator::SeqScanOperator(std::unique_ptr<storage::HeapTable> table, Transaction* txn,
32+
SeqScanOperator::SeqScanOperator(std::shared_ptr<storage::HeapTable> table, Transaction* txn,
3433
LockManager* lock_manager)
3534
: Operator(OperatorType::SeqScan, txn, lock_manager),
3635
table_name_(table->table_name()),
@@ -131,7 +130,7 @@ Schema& BufferScanOperator::output_schema() {
131130

132131
/* --- IndexScanOperator --- */
133132

134-
IndexScanOperator::IndexScanOperator(std::unique_ptr<storage::HeapTable> table,
133+
IndexScanOperator::IndexScanOperator(std::shared_ptr<storage::HeapTable> table,
135134
std::unique_ptr<storage::BTreeIndex> index,
136135
common::Value search_key, Transaction* txn,
137136
LockManager* lock_manager)

src/executor/query_executor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ std::unique_ptr<Operator> QueryExecutor::build_plan(const parser::SelectStatemen
787787
col_name) {
788788
common::ValueType ktype = base_table_meta->columns[pos].type;
789789
current_root = std::make_unique<IndexScanOperator>(
790-
std::make_unique<storage::HeapTable>(base_table_name, bpm_,
790+
std::make_shared<storage::HeapTable>(base_table_name, bpm_,
791791
base_schema),
792792
std::make_unique<storage::BTreeIndex>(idx_info.name, bpm_,
793793
ktype),

0 commit comments

Comments
 (0)