-
Notifications
You must be signed in to change notification settings - Fork 0
feat: high-performance engine optimizations and Prepared Statement API #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ae162ec
3bda4e3
70fb252
7048e71
64b8018
4781951
41fb871
48fcf46
1d09a7a
71f912a
378b9c2
3f4f4c5
c49bcb9
9d165e0
14294cd
8ceaa93
54f6025
548839c
6cf093b
0dc042b
e9a845e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /** | ||
| * @file arena_allocator.hpp | ||
| * @brief High-performance bump allocator for execution-scoped data | ||
| */ | ||
|
|
||
| #ifndef CLOUDSQL_COMMON_ARENA_ALLOCATOR_HPP | ||
| #define CLOUDSQL_COMMON_ARENA_ALLOCATOR_HPP | ||
|
|
||
| #include <algorithm> | ||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <memory_resource> | ||
| #include <vector> | ||
|
|
||
| namespace cloudsql::common { | ||
|
|
||
| /** | ||
| * @class ArenaAllocator | ||
| * @brief Manages memory chunks and provides fast, contiguous allocations. | ||
| * | ||
| * Implements std::pmr::memory_resource for compatibility with standard | ||
| * containers like std::pmr::vector. | ||
| */ | ||
| class ArenaAllocator : public std::pmr::memory_resource { | ||
| public: | ||
| static constexpr size_t DEFAULT_CHUNK_SIZE = 65536; // 64KB | ||
|
|
||
| explicit ArenaAllocator(size_t chunk_size = DEFAULT_CHUNK_SIZE) | ||
| : chunk_size_(chunk_size), current_chunk_idx_(0), current_offset_(0) {} | ||
|
|
||
| ~ArenaAllocator() override { | ||
| for (auto* chunk : chunks_) { | ||
| delete[] chunk; | ||
| } | ||
| } | ||
|
|
||
| // Disable copy | ||
| ArenaAllocator(const ArenaAllocator&) = delete; | ||
| ArenaAllocator& operator=(const ArenaAllocator&) = delete; | ||
|
|
||
| /** | ||
| * @brief Reset the arena, reclaiming all memory for reuse. | ||
| * | ||
| * Keeps all allocated chunks but resets pointers so they can be overwritten. | ||
| * This is an O(1) or O(N_chunks) operation with zero heap overhead. | ||
| */ | ||
| void reset() { | ||
| current_chunk_idx_ = 0; | ||
| current_offset_ = 0; | ||
| } | ||
|
|
||
| protected: | ||
| /** | ||
| * @brief Internal allocation logic for PMR | ||
| */ | ||
| void* do_allocate(size_t bytes, size_t alignment) override { | ||
| if (bytes == 0) return nullptr; | ||
|
|
||
| // Align the offset | ||
| size_t mask = alignment - 1; | ||
|
|
||
| // Try current chunk | ||
| if (current_chunk_idx_ < chunks_.size()) { | ||
| size_t aligned_offset = (current_offset_ + mask) & ~mask; | ||
| if (aligned_offset + bytes <= chunk_size_) { | ||
| void* result = chunks_[current_chunk_idx_] + aligned_offset; | ||
| current_offset_ = aligned_offset + bytes; | ||
| return result; | ||
| } | ||
|
|
||
| // Move to next existing chunk if possible | ||
| current_chunk_idx_++; | ||
| current_offset_ = 0; | ||
| return do_allocate(bytes, alignment); | ||
| } | ||
|
|
||
| // Need a new chunk | ||
| if (bytes > chunk_size_) { | ||
| auto* large_chunk = new uint8_t[bytes]; | ||
| chunks_.push_back(large_chunk); | ||
| // We don't make this the "current" chunk for small allocations | ||
| // to avoid wasting space. We just return it. | ||
| return large_chunk; | ||
| } | ||
|
|
||
| allocate_new_chunk(); | ||
| return do_allocate(bytes, alignment); | ||
| } | ||
|
|
||
| /** | ||
| * @brief PMR deallocate is a no-op for bump allocators (we reset the whole arena) | ||
| */ | ||
| void do_deallocate(void* p, size_t bytes, size_t alignment) override { | ||
| // No-op | ||
| (void)p; | ||
| (void)bytes; | ||
| (void)alignment; | ||
| } | ||
|
|
||
| bool do_is_equal(const std::pmr::memory_resource& other) const noexcept override { | ||
| return this == &other; | ||
| } | ||
|
|
||
| private: | ||
| void allocate_new_chunk() { | ||
| chunks_.push_back(new uint8_t[chunk_size_]); | ||
| // Don't change current_chunk_idx_ here, let the recursive call handle it | ||
| } | ||
|
|
||
| size_t chunk_size_; | ||
| std::vector<uint8_t*> chunks_; | ||
| size_t current_chunk_idx_; | ||
| size_t current_offset_; | ||
| }; | ||
|
|
||
| } // namespace cloudsql::common | ||
|
|
||
| #endif // CLOUDSQL_COMMON_ARENA_ALLOCATOR_HPP | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,11 @@ | |
| #ifndef CLOUDSQL_EXECUTOR_QUERY_EXECUTOR_HPP | ||
| #define CLOUDSQL_EXECUTOR_QUERY_EXECUTOR_HPP | ||
|
|
||
| #include <mutex> | ||
| #include <unordered_map> | ||
|
|
||
| #include "catalog/catalog.hpp" | ||
| #include "common/arena_allocator.hpp" | ||
| #include "common/cluster_manager.hpp" | ||
| #include "distributed/raft_types.hpp" | ||
| #include "executor/operator.hpp" | ||
|
|
@@ -18,6 +22,20 @@ | |
|
|
||
| namespace cloudsql::executor { | ||
|
|
||
| /** | ||
| * @brief Represents a pre-parsed and pre-planned SQL statement | ||
| */ | ||
| struct PreparedStatement { | ||
| std::shared_ptr<parser::Statement> stmt; | ||
| std::string sql; | ||
|
|
||
| // Cached execution state for hot-path optimization | ||
| const TableInfo* table_meta = nullptr; | ||
| std::unique_ptr<Schema> schema; | ||
| std::unique_ptr<storage::HeapTable> table; | ||
| std::vector<std::unique_ptr<storage::BTreeIndex>> indexes; | ||
|
Comment on lines
+32
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't cache mutable storage handles inside a reusable prepared statement.
🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| /** | ||
| * @brief State machine for a specific data shard | ||
| */ | ||
|
|
@@ -62,11 +80,32 @@ class QueryExecutor { | |
| */ | ||
| void set_local_only(bool local) { is_local_only_ = local; } | ||
|
|
||
| /** | ||
| * @brief Prepare a SQL string into a reusable PreparedStatement | ||
| */ | ||
| std::shared_ptr<PreparedStatement> prepare(const std::string& sql); | ||
|
|
||
| /** | ||
| * @brief Execute a SQL statement and return results | ||
| */ | ||
| QueryResult execute(const parser::Statement& stmt); | ||
|
|
||
| /** | ||
| * @brief Execute a SQL string (includes parsing and cache lookup) | ||
| */ | ||
| QueryResult execute(const std::string& sql); | ||
|
|
||
| /** | ||
| * @brief Execute a PreparedStatement with bound parameters | ||
| */ | ||
| QueryResult execute(const PreparedStatement& prepared, | ||
| const std::vector<common::Value>& params); | ||
|
|
||
| /** | ||
| * @brief Get access to the query-scoped arena | ||
| */ | ||
| common::ArenaAllocator& arena() { return arena_; } | ||
|
|
||
| private: | ||
| Catalog& catalog_; | ||
| storage::BufferPoolManager& bpm_; | ||
|
|
@@ -78,6 +117,16 @@ class QueryExecutor { | |
| transaction::Transaction* current_txn_ = nullptr; | ||
| bool is_local_only_ = false; | ||
|
|
||
| // Bound parameters for the current execution | ||
| const std::vector<common::Value>* current_params_ = nullptr; | ||
|
|
||
| // Performance structures | ||
| common::ArenaAllocator arena_; | ||
|
|
||
| // Global statement cache (thread-safe) | ||
| static std::unordered_map<std::string, std::shared_ptr<parser::Statement>> statement_cache_; | ||
| static std::mutex cache_mutex_; | ||
|
|
||
| QueryResult execute_select(const parser::SelectStatement& stmt, transaction::Transaction* txn); | ||
| QueryResult execute_create_table(const parser::CreateTableStatement& stmt); | ||
| QueryResult execute_create_index(const parser::CreateIndexStatement& stmt); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep oversized allocations out of the reusable chunk list.
The
bytes > chunk_size_branch appends the dedicated buffer tochunks_but leavescurrent_chunk_idx_/current_offset_unchanged. The next small allocation can therefore be handed out from that same buffer at offset 0, aliasing the still-live large allocation.💡 One way to isolate large allocations
~ArenaAllocator() override { for (auto* chunk : chunks_) { delete[] chunk; } + for (auto* chunk : large_chunks_) { + delete[] chunk; + } } ... if (bytes > chunk_size_) { auto* large_chunk = new uint8_t[bytes]; - chunks_.push_back(large_chunk); + large_chunks_.push_back(large_chunk); // We don't make this the "current" chunk for small allocations // to avoid wasting space. We just return it. return large_chunk; } ... std::vector<uint8_t*> chunks_; + std::vector<uint8_t*> large_chunks_; size_t current_chunk_idx_; size_t current_offset_;Also applies to: 79-85, 111-114
🤖 Prompt for AI Agents