diff --git a/CHANGELOG.md b/CHANGELOG.md index 606c5845..39b84917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ - magic numbers for tables start in bytecode files have been changed from 0x01, 0x02, 0x03 to 0xA1, 0xA2, 0xA3 (symbols, values, code) to make them stand out in hex editors - magic numbers for value types in bytecode files have been changed from 0x01, 0x02, 0x03 to 0xF1, 0xF2, 0xF3 (number, string, function) - numbers in the values table in bytecode files are no longer stringified but their IEEE754 representation is now encoded on 12 bytes (4 for the exponent, 8 for the mantissa) +- changed how scopes are stored inside the VM to enhance performances. All scope data are now contiguous! ### Removed - removed unused `NodeType::Closure` diff --git a/include/Ark/Constants.hpp.in b/include/Ark/Constants.hpp.in index a823fc31..2a958707 100644 --- a/include/Ark/Constants.hpp.in +++ b/include/Ark/Constants.hpp.in @@ -66,6 +66,7 @@ namespace Ark constexpr std::size_t MaxMacroProcessingDepth = 256; ///< Controls the number of recursive calls to MacroProcessor::processNode constexpr std::size_t MaxMacroUnificationDepth = 256; ///< Controls the number of recursive calls to MacroProcessor::unify constexpr std::size_t VMStackSize = 8192; + constexpr std::size_t ScopeStackSize = 8192; } #endif diff --git a/include/Ark/VM/ExecutionContext.hpp b/include/Ark/VM/ExecutionContext.hpp index 0c6a9706..394c8cc1 100644 --- a/include/Ark/VM/ExecutionContext.hpp +++ b/include/Ark/VM/ExecutionContext.hpp @@ -5,7 +5,7 @@ * @version 0.1 * @date 2021-11-15 * - * @copyright Copyright (c) 2021-2024 + * @copyright Copyright (c) 2021-2025 * */ @@ -19,7 +19,8 @@ #include #include -#include +#include +#include #ifdef max # undef max @@ -38,9 +39,12 @@ namespace Ark::internal uint16_t last_symbol; const bool primary; ///< Tells if the current ExecutionContext is the primary one or not - std::optional saved_scope {}; ///< Scope created by CAPTURE instructions, used by the MAKE_CLOSURE instruction - std::vector locals {}; - std::vector> stacked_closure_scopes {}; ///< Stack the closure scopes to keep the closure alive as long as we are calling them + std::optional saved_scope {}; ///< Scope created by CAPTURE instructions, used by the MAKE_CLOSURE instruction + std::vector> stacked_closure_scopes {}; ///< Stack the closure scopes to keep the closure alive as long as we are calling them + + std::vector locals {}; + std::array scopes_storage {}; ///< All the ScopeView use this array to store id->value + std::array stack {}; ExecutionContext() noexcept : diff --git a/include/Ark/VM/Scope.hpp b/include/Ark/VM/ScopeView.hpp similarity index 63% rename from include/Ark/VM/Scope.hpp rename to include/Ark/VM/ScopeView.hpp index 29aa251e..547402b1 100644 --- a/include/Ark/VM/Scope.hpp +++ b/include/Ark/VM/ScopeView.hpp @@ -5,14 +5,14 @@ * @version 0.2 * @date 2020-10-27 * - * @copyright Copyright (c) 2020-2024 + * @copyright Copyright (c) 2020-2025 * */ #ifndef ARK_VM_SCOPE_HPP #define ARK_VM_SCOPE_HPP -#include +#include #include #include @@ -24,21 +24,23 @@ namespace Ark::internal * @brief A class to handle the VM scope more efficiently * */ - class ARK_API Scope + class ARK_API ScopeView { public: + using pair_t = std::pair; + /** - * @brief Construct a new Scope object - * + * @brief Deleted constructor to avoid creating ScopeViews pointing to nothing. Helps catch bugs at compile time */ - Scope() noexcept; + ScopeView() = delete; /** - * @brief Merge values from this scope as refs in the other scope - * @details This scope must be kept alive for the ref to be used - * @param other + * @brief Create a new ScopeView + * + * @param storage pointer to the shared scope storage + * @param start first free starting position */ - void mergeRefInto(Scope& other); + ScopeView(pair_t* storage, std::size_t start) noexcept; /** * @brief Put a value in the scope @@ -89,20 +91,44 @@ namespace Ark::internal */ [[nodiscard]] uint16_t idFromValue(const Value& val) const noexcept; + /** + * @brief Return the start index of the current + * + * @return const std::size_t + */ + [[nodiscard]] inline const pair_t& atPos(const std::size_t i) const noexcept + { + return m_storage[m_start + i]; + } + /** * @brief Return the size of the scope * * @return const std::size_t */ - [[nodiscard]] std::size_t size() const noexcept; + [[nodiscard]] inline std::size_t size() const noexcept + { + return m_size; + } + + /** + * @brief Compute the position of the first free slot in the shared storage, after this scope + * + * @return std::size_t + */ + [[nodiscard]] inline std::size_t storageEnd() const noexcept + { + return m_start + m_size; + } - friend ARK_API bool operator==(const Scope& A, const Scope& B) noexcept; + friend ARK_API bool operator==(const ScopeView& A, const ScopeView& B) noexcept; friend class Ark::VM; - friend class Ark::internal::Closure; private: - std::vector> m_data; + pair_t* m_storage; + std::size_t m_start; + std::size_t m_size; uint16_t m_min_id; ///< Minimum stored ID, used for a basic bloom filter uint16_t m_max_id; ///< Maximum stored ID, used for a basic bloom filter }; diff --git a/include/Ark/VM/VM.hpp b/include/Ark/VM/VM.hpp index 75373227..2a87b95e 100644 --- a/include/Ark/VM/VM.hpp +++ b/include/Ark/VM/VM.hpp @@ -5,7 +5,7 @@ * @version 2.0 * @date 2020-10-27 * - * @copyright Copyright (c) 2020-2024 + * @copyright Copyright (c) 2020-2025 * */ @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/include/Ark/VM/VM.inl b/include/Ark/VM/VM.inl index 31bed6e6..77b16b01 100644 --- a/include/Ark/VM/VM.inl +++ b/include/Ark/VM/VM.inl @@ -283,7 +283,6 @@ inline void VM::returnFromFuncCall(internal::ExecutionContext& context) { --context.fc; context.stacked_closure_scopes.pop_back(); - // NOTE: high cpu cost because destroying variants cost context.locals.pop_back(); } @@ -332,8 +331,8 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc) { const PageAddr_t new_page_pointer = function.pageAddr(); - // create dedicated frame - context.locals.emplace_back(); + // create dedicated scope + context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); swapStackForFunCall(argc, context); // store "reference" to the function to speed the recursive functions @@ -351,8 +350,8 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc) Closure& c = function.refClosure(); const PageAddr_t new_page_pointer = c.pageAddr(); - // create dedicated frame - context.locals.emplace_back(); + // create dedicated scope + context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); // load saved scope c.refScope().mergeRefInto(context.locals.back()); context.stacked_closure_scopes.back() = c.scopePtr(); diff --git a/include/Ark/VM/Value.hpp b/include/Ark/VM/Value.hpp index 91515c20..eabb700b 100644 --- a/include/Ark/VM/Value.hpp +++ b/include/Ark/VM/Value.hpp @@ -204,7 +204,9 @@ namespace Ark return A.string().empty(); case ValueType::User: + [[fallthrough]]; case ValueType::Nil: + [[fallthrough]]; case ValueType::False: return true; diff --git a/include/Ark/VM/Value/Closure.hpp b/include/Ark/VM/Value/Closure.hpp index e8ac77a7..eb9bbda6 100644 --- a/include/Ark/VM/Value/Closure.hpp +++ b/include/Ark/VM/Value/Closure.hpp @@ -25,10 +25,10 @@ namespace Ark namespace Ark::internal { - class Scope; - using PageAddr_t = uint16_t; + class ClosureScope; + /** * @brief Closure management * @@ -42,18 +42,18 @@ namespace Ark::internal * @param scope the scope of the function turned into a closure * @param pa the current page address of the function turned into a closure */ - Closure(const Scope& scope, PageAddr_t pa) noexcept; + Closure(const ClosureScope& scope, PageAddr_t pa) noexcept; /** * @brief Construct a new Closure object * @param scope_ptr a shared pointer to the scope of the function turned into a closure * @param pa the current page address of the function turned into a closure */ - Closure(const std::shared_ptr& scope_ptr, PageAddr_t pa) noexcept; + Closure(const std::shared_ptr& scope_ptr, PageAddr_t pa) noexcept; - [[nodiscard]] const Scope& scope() const noexcept { return *m_scope; } - [[nodiscard]] Scope& refScope() const noexcept { return *m_scope; } - [[nodiscard]] const std::shared_ptr& scopePtr() const { return m_scope; } + [[nodiscard]] const ClosureScope& scope() const noexcept { return *m_scope; } + [[nodiscard]] ClosureScope& refScope() const noexcept { return *m_scope; } + [[nodiscard]] const std::shared_ptr& scopePtr() const { return m_scope; } /** * @@ -68,7 +68,7 @@ namespace Ark::internal * @param vm * @return true if the closure has a field which is the end of 'end' */ - [[nodiscard]] bool hasFieldEndingWith(const std::string& end, VM& vm) const; + [[nodiscard]] bool hasFieldEndingWith(const std::string& end, const VM& vm) const; /** * @brief Print the closure to a string @@ -81,7 +81,7 @@ namespace Ark::internal friend ARK_API_INLINE bool operator<(const Closure& A, const Closure& B) noexcept; private: - std::shared_ptr m_scope; + std::shared_ptr m_scope; // keep track of the code page number, in case we need it later PageAddr_t m_page_addr; }; diff --git a/include/Ark/VM/Value/ClosureScope.hpp b/include/Ark/VM/Value/ClosureScope.hpp new file mode 100644 index 00000000..3640d70f --- /dev/null +++ b/include/Ark/VM/Value/ClosureScope.hpp @@ -0,0 +1,71 @@ +/** + * @file ClosureScope.hpp + * @author Alexandre Plateau (lexplt.dev@gmail.com) + * @brief Subtype of the value type, handling closures + * @version 0.1 + * @date 2025-03-17 + * + * @copyright Copyright (c) 2025 + * + */ + +#ifndef ARK_VM_VALUE_CLOSURESCOPE_HPP +#define ARK_VM_VALUE_CLOSURESCOPE_HPP + +#include +#include +#include + +#include +#include + +namespace Ark::internal +{ + class ScopeView; + + /** + * @brief A class to store fields captured by a closure + */ + class ARK_API ClosureScope + { + public: + /** + * @brief Create a new ClosureScope + */ + ClosureScope() noexcept = default; + + /** + * @brief Put a value in the scope + * + * @param id The symbol id of the variable + * @param val The value linked to the symbol + */ + void push_back(uint16_t id, Value&& val); + + /** + * @brief Put a value in the scope + * + * @param id The symbol id of the variable + * @param val The value linked to the symbol + */ + void push_back(uint16_t id, const Value& val); + + Value* operator[](uint16_t id_to_look_for); + + /** + * @brief Merge values from this scope as refs in the other scope + * @details This scope must be kept alive for the ref to be used + * @param other + */ + void mergeRefInto(ScopeView& other); + + friend class Closure; + + friend ARK_API bool operator==(const ClosureScope& A, const ClosureScope& B) noexcept; + + private: + std::vector> m_data; + }; +} + +#endif // ARK_VM_VALUE_CLOSURESCOPE_HPP diff --git a/src/arkreactor/VM/Scope.cpp b/src/arkreactor/VM/Scope.cpp deleted file mode 100644 index d6928a12..00000000 --- a/src/arkreactor/VM/Scope.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include - -#include - -namespace Ark::internal -{ - Scope::Scope() noexcept : - m_min_id(std::numeric_limits::max()), m_max_id(0) - { - m_data.reserve(3); - } - - void Scope::mergeRefInto(Scope& other) - { - for (auto& [id, val] : m_data) - { - if (val.valueType() == ValueType::Reference) - other.push_back(id, val); - else - other.push_back(id, Value(&val)); - } - } - - void Scope::push_back(uint16_t id, Value&& val) noexcept - { - if (id < m_min_id) - m_min_id = id; - if (id > m_max_id) - m_max_id = id; - - m_data.emplace_back(id, std::move(val)); - } - - void Scope::push_back(uint16_t id, const Value& val) noexcept - { - if (id < m_min_id) - m_min_id = id; - if (id > m_max_id) - m_max_id = id; - - m_data.emplace_back(id, val); - } - - bool Scope::maybeHas(const uint16_t id) const noexcept - { - return m_min_id <= id && id <= m_max_id; - } - - Value* Scope::operator[](const uint16_t id_to_look_for) noexcept - { - if (!maybeHas(id_to_look_for)) - return nullptr; - - for (auto& [id, value] : m_data) - { - if (id == id_to_look_for) - return &value; - } - return nullptr; - } - - const Value* Scope::operator[](const uint16_t id_to_look_for) const noexcept - { - if (!maybeHas(id_to_look_for)) - return nullptr; - - for (const auto& [id, value] : m_data) - { - if (id == id_to_look_for) - return &value; - } - return nullptr; - } - - uint16_t Scope::idFromValue(const Value& val) const noexcept - { - for (const auto& [id, data] : m_data) - { - if (data == val) - return id; - } - return std::numeric_limits::max(); - } - - std::size_t Scope::size() const noexcept - { - return m_data.size(); - } - - bool operator==(const Scope& A, const Scope& B) noexcept - { - const std::size_t size = A.size(); - if (size != B.size()) - return false; - if (A.m_min_id != B.m_min_id || A.m_max_id != B.m_max_id) - return false; - - // assuming we have the same closure page address, the element should be in the same order - for (std::size_t i = 0; i < size; ++i) - { - if (A.m_data[i] != B.m_data[i]) - return false; - } - - return true; - } -} diff --git a/src/arkreactor/VM/ScopeView.cpp b/src/arkreactor/VM/ScopeView.cpp new file mode 100644 index 00000000..97cfbe24 --- /dev/null +++ b/src/arkreactor/VM/ScopeView.cpp @@ -0,0 +1,83 @@ +#include + +#include + +namespace Ark::internal +{ + ScopeView::ScopeView(pair_t* storage, const std::size_t start) noexcept : + m_storage(storage), m_start(start), m_size(0), m_min_id(std::numeric_limits::max()), m_max_id(0) + {} + + void ScopeView::push_back(uint16_t id, Value&& val) noexcept + { + if (id < m_min_id) + m_min_id = id; + if (id > m_max_id) + m_max_id = id; + + m_storage[m_start + m_size] = std::make_pair(id, std::move(val)); + ++m_size; + } + + void ScopeView::push_back(uint16_t id, const Value& val) noexcept + { + if (id < m_min_id) + m_min_id = id; + if (id > m_max_id) + m_max_id = id; + + m_storage[m_start + m_size] = std::make_pair(id, val); + ++m_size; + } + + bool ScopeView::maybeHas(const uint16_t id) const noexcept + { + return m_min_id <= id && id <= m_max_id; + } + + Value* ScopeView::operator[](const uint16_t id_to_look_for) noexcept + { + if (!maybeHas(id_to_look_for)) + return nullptr; + + for (std::size_t i = m_start; i < m_start + m_size; ++i) + { + auto& [id, value] = m_storage[i]; + if (id == id_to_look_for) + return &value; + } + return nullptr; + } + + const Value* ScopeView::operator[](const uint16_t id_to_look_for) const noexcept + { + if (!maybeHas(id_to_look_for)) + return nullptr; + + for (std::size_t i = m_start; i < m_start + m_size; ++i) + { + auto& [id, value] = m_storage[i]; + if (id == id_to_look_for) + return &value; + } + return nullptr; + } + + uint16_t ScopeView::idFromValue(const Value& val) const noexcept + { + for (std::size_t i = m_start; i < m_start + m_size; ++i) + { + const auto& [id, value] = m_storage[i]; + if (value == val) + return id; + } + return std::numeric_limits::max(); + } + + bool operator==(const ScopeView& A, const ScopeView& B) noexcept + { + // if we have two scopes with the same number of elements and starting at the same position, + // they must be identical, as we have a single storage for all scopes + return A.m_size == B.m_size && A.m_start == B.m_start; + } +} diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index fc5ac72c..92c1b61e 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -79,7 +79,7 @@ namespace Ark VM::VM(State& state) noexcept : m_state(state), m_exit_code(0), m_running(false) { - m_execution_contexts.emplace_back(std::make_unique())->locals.reserve(4); + m_execution_contexts.emplace_back(std::make_unique()); } void VM::init() noexcept @@ -103,7 +103,7 @@ namespace Ark m_exit_code = 0; context.locals.clear(); - context.locals.emplace_back(); + context.locals.emplace_back(context.scopes_storage.data(), 0); // loading bound stuff // put them in the global frame if we can, aka the first one @@ -233,8 +233,14 @@ namespace Ark ctx->stacked_closure_scopes.emplace_back(nullptr); ctx->locals.reserve(m_execution_contexts.front()->locals.size()); + ctx->scopes_storage = m_execution_contexts.front()->scopes_storage; for (const auto& local : m_execution_contexts.front()->locals) - ctx->locals.push_back(local); + { + auto& scope = ctx->locals.emplace_back(ctx->scopes_storage.data(), local.m_start); + scope.m_size = local.m_size; + scope.m_min_id = local.m_min_id; + scope.m_max_id = local.m_max_id; + } return ctx; } @@ -554,7 +560,7 @@ namespace Ark TARGET(CAPTURE) { if (!context.saved_scope) - context.saved_scope = Scope(); + context.saved_scope = ClosureScope(); const Value* ptr = findNearestVariable(arg, context); if (!ptr) @@ -916,7 +922,7 @@ namespace Ark TARGET(CREATE_SCOPE) { - context.locals.emplace_back(); + context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); DISPATCH(); } @@ -1470,7 +1476,7 @@ namespace Ark if (const uint16_t original_frame_count = context.fc; original_frame_count > 1) { // display call stack trace - const Scope old_scope = context.locals.back(); + const ScopeView old_scope = context.locals.back(); while (context.fc != 0) { @@ -1515,8 +1521,8 @@ namespace Ark { fmt::println( "{} = {}", - fmt::styled(m_state.m_symbols[old_scope.m_data[i].first], fmt::fg(fmt::color::cyan)), - old_scope.m_data[i].second.toString(*this)); + fmt::styled(m_state.m_symbols[old_scope.atPos(i).first], fmt::fg(fmt::color::cyan)), + old_scope.atPos(i).second.toString(*this)); } while (context.fc != 1) diff --git a/src/arkreactor/VM/Value/Closure.cpp b/src/arkreactor/VM/Value/Closure.cpp index 0631eac8..2f8bede0 100644 --- a/src/arkreactor/VM/Value/Closure.cpp +++ b/src/arkreactor/VM/Value/Closure.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include #include @@ -8,17 +8,17 @@ namespace Ark::internal { - Closure::Closure(const Scope& scope, const PageAddr_t pa) noexcept : - m_scope(std::make_shared(scope)), + Closure::Closure(const ClosureScope& scope, const PageAddr_t pa) noexcept : + m_scope(std::make_shared(scope)), m_page_addr(pa) {} - Closure::Closure(const std::shared_ptr& scope_ptr, const PageAddr_t pa) noexcept : + Closure::Closure(const std::shared_ptr& scope_ptr, const PageAddr_t pa) noexcept : m_scope(scope_ptr), m_page_addr(pa) {} - bool Closure::hasFieldEndingWith(const std::string& end, VM& vm) const + bool Closure::hasFieldEndingWith(const std::string& end, const VM& vm) const { for (const auto id : std::ranges::views::keys(m_scope->m_data)) { diff --git a/src/arkreactor/VM/Value/ClosureScope.cpp b/src/arkreactor/VM/Value/ClosureScope.cpp new file mode 100644 index 00000000..0cf1fbe6 --- /dev/null +++ b/src/arkreactor/VM/Value/ClosureScope.cpp @@ -0,0 +1,42 @@ +#include + +#include + +namespace Ark::internal +{ + void ClosureScope::push_back(const uint16_t id, Value&& val) + { + m_data.emplace_back(id, std::move(val)); + } + + void ClosureScope::push_back(const uint16_t id, const Value& val) + { + m_data.emplace_back(id, val); + } + + Value* ClosureScope::operator[](const uint16_t id_to_look_for) + { + for (auto& [id, value] : m_data) + { + if (id == id_to_look_for) + return &value; + } + return nullptr; + } + + void ClosureScope::mergeRefInto(ScopeView& other) + { + for (auto& [id, value] : m_data) + { + if (value.valueType() == ValueType::Reference) + other.push_back(id, value); + else + other.push_back(id, Value(&value)); + } + } + + bool operator==(const ClosureScope& A, const ClosureScope& B) noexcept + { + return A.m_data == B.m_data; + } +} diff --git a/src/arkscript/main.cpp b/src/arkscript/main.cpp index 054e44a3..72cf4473 100644 --- a/src/arkscript/main.cpp +++ b/src/arkscript/main.cpp @@ -226,7 +226,7 @@ int main(int argc, char** argv) // vm sizeof(Ark::VM), sizeof(Ark::State), - sizeof(Ark::internal::Scope), + sizeof(Ark::internal::ScopeView), sizeof(Ark::internal::ExecutionContext), // misc sizeof(std::vector), diff --git a/tests/benchmarks/results/9-d1be6b9f.csv b/tests/benchmarks/results/9-d1be6b9f.csv new file mode 100644 index 00000000..8eafb016 --- /dev/null +++ b/tests/benchmarks/results/9-d1be6b9f.csv @@ -0,0 +1,8 @@ +name,iterations,real_time,cpu_time,time_unit,bytes_per_second,items_per_second,label,error_occurred,error_message +"quicksort",6011,0.117192,0.117102,ms,,,,, +"ackermann/iterations:50",50,49.2279,49.1553,ms,,,,, +"fibonacci/iterations:100",100,4.55086,4.54807,ms,,,,, +"man_or_boy",23276,0.0302097,0.0301714,ms,,,,, +"builtins",1267,0.570876,0.570242,ms,,,,, +"binary_trees",1,918.186,916.986,ms,,,,, +"for_sum",6,120.091,120.022,ms,,,,,