From 0e322756aa3e8b3a70fda858800a0743e6a6f96b Mon Sep 17 00:00:00 2001 From: kunitoki Date: Mon, 30 Mar 2026 09:16:03 +0200 Subject: [PATCH] Speed up method overload resolution --- Source/LuaBridge/detail/CFunctions.h | 136 ++++++++--- Source/LuaBridge/detail/Namespace.h | 323 ++++++++++++++++++--------- 2 files changed, 317 insertions(+), 142 deletions(-) diff --git a/Source/LuaBridge/detail/CFunctions.h b/Source/LuaBridge/detail/CFunctions.h index dcd98e57..a80cb401 100644 --- a/Source/LuaBridge/detail/CFunctions.h +++ b/Source/LuaBridge/detail/CFunctions.h @@ -21,6 +21,7 @@ #include #include #include +#include namespace luabridge { @@ -947,6 +948,7 @@ inline int newindex_metamethod_simple(lua_State* L) } luaL_error(L, "no writable member '%s'", key); + return 0; } //================================================================================================= @@ -958,7 +960,6 @@ inline int newindex_metamethod_simple(lua_State* L) inline int read_only_error(lua_State* L) { raise_lua_error(L, "'%s' is read-only", lua_tostring(L, lua_upvalueindex(1))); - return 0; } @@ -1551,67 +1552,140 @@ int invoke_proxy_destructor(lua_State* L) return 1; } +//================================================================================================= +/** + * @brief C++ storage for a single overload entry: arity and optional type checker. + * + * Stored inside an OverloadSet which is kept as a Lua full userdata (auto-GC'd). + */ +struct OverloadEntry +{ + using TypeChecker = bool (*)(lua_State*, int start); + + int arity; // -1 for variadic (lua_CFunction): always attempt + TypeChecker checker; // nullptr for variadic: skip type pre-checking +}; + +/** + * @brief C++ storage for all overloads of a function. + * + * Stored as a Lua full userdata so it is GC'd automatically when the closure is collected. + * The actual function closures are stored separately in a flat Lua table (upvalue 2). + */ +struct OverloadSet +{ + std::vector entries; +}; + +/** + * @brief Check a single argument type, skipping lua_State* (auto-injected, not on the Lua stack). + */ +template +bool overload_check_one_arg(lua_State* L, int& idx) +{ + if constexpr (std::is_pointer_v && + std::is_same_v>, lua_State>) + { + return true; // lua_State* is auto-injected by LuaBridge, not a Lua-visible argument + } + else + { + return Stack::isInstance(L, idx++); + } +} + +template +bool overload_check_args_impl(lua_State* L, int start, std::index_sequence) +{ + int idx = start; + return (overload_check_one_arg>(L, idx) && ...); +} + +template +bool overload_check_args(lua_State* L, int start) +{ + return overload_check_args_impl(L, start, + std::make_index_sequence>{}); +} + +/** + * @brief Type checker instantiable as an OverloadEntry::TypeChecker function pointer. + * + * Checks that the Lua stack arguments starting at @p start match the types in @tparam ArgsPack, + * using Stack::isInstance without raising errors. lua_State* arguments are skipped. + */ +template +bool overload_type_checker(lua_State* L, int start) +{ + return overload_check_args(L, start); +} + //================================================================================================= /** * @brief lua_CFunction to resolve an invocation between several overloads. * - * The list of overloads is in the first upvalue. The arguments of the function call are at the top of the Lua stack. + * upvalue[1] = OverloadSet full userdata — C++ vector of {arity, type_checker} per overload. + * upvalue[2] = flat Lua table {[1]=func1, [2]=func2, ...} — the actual function closures. + * + * Dispatch: + * 1. Arity check in C++ (no Lua call). + * 2. Type check via Stack::isInstance in C++ (no pcall) — skips clearly mismatched overloads. + * 3. Only calls lua_pcall for type-matched candidates, eliminating failed pcalls for type mismatches. */ template inline int try_overload_functions(lua_State* L) { const int nargs = lua_gettop(L); const int effective_args = nargs - (Member ? 1 : 0); + const int start_arg = Member ? 2 : 1; + + LUABRIDGE_ASSERT(isfulluserdata(L, lua_upvalueindex(1))); + auto* overload_set = align(lua_touserdata(L, lua_upvalueindex(1))); - // get the list of overloads - lua_pushvalue(L, lua_upvalueindex(1)); + // push flat functions table (upvalue 2) + lua_pushvalue(L, lua_upvalueindex(2)); LUABRIDGE_ASSERT(lua_istable(L, -1)); - const int idx_overloads = nargs + 1; - const int num_overloads = get_length(L, idx_overloads); + const int idx_funcs = nargs + 1; // create table to hold error messages - lua_createtable(L, num_overloads, 0); + lua_createtable(L, static_cast(overload_set->entries.size()), 0); const int idx_errors = nargs + 2; int nerrors = 0; - // iterate through table, snippet taken from Lua docs - lua_pushnil(L); // first key - while (lua_next(L, idx_overloads) != 0) + for (int i = 0; i < static_cast(overload_set->entries.size()); ++i) { - LUABRIDGE_ASSERT(lua_istable(L, -1)); - - // check matching arity - lua_rawgeti(L, -1, 1); - LUABRIDGE_ASSERT(lua_isnumber(L, -1)); + const auto& entry = overload_set->entries[i]; - const int overload_arity = static_cast(lua_tointeger(L, -1)); - if (overload_arity >= 0 && overload_arity != effective_args) + // fast arity check (C++, no Lua calls) + if (entry.arity >= 0 && entry.arity != effective_args) { - // store error message and try next overload - lua_pushfstring(L, "Skipped overload #%d with unmatched arity of %d instead of %d", nerrors, overload_arity, effective_args); + lua_pushfstring(L, "Skipped overload #%d with unmatched arity of %d instead of %d", i, entry.arity, effective_args); lua_rawseti(L, idx_errors, ++nerrors); - - lua_pop(L, 2); // pop arity, value (table) continue; } - lua_pop(L, 1); // pop arity + // fast type check (C++, no pcall) — avoids expensive pcall for clearly mismatched types + if (entry.checker != nullptr && !entry.checker(L, start_arg)) + { + lua_pushfstring(L, "Skipped overload #%d with unmatched argument types", i); + lua_rawseti(L, idx_errors, ++nerrors); + continue; + } - // push function - lua_pushnumber(L, 2); - lua_gettable(L, -2); + // O(1) function lookup from flat table + lua_rawgeti(L, idx_funcs, i + 1); LUABRIDGE_ASSERT(lua_isfunction(L, -1)); // push arguments - for (int i = 1; i <= nargs; ++i) - lua_pushvalue(L, i); + for (int j = 1; j <= nargs; ++j) + lua_pushvalue(L, j); // call f, this pops the function and its args, pushes result(s) const int err = lua_pcall(L, nargs, LUA_MULTRET, 0); if (err == LUABRIDGE_LUA_OK) { - // calculate number of return values and return - return lua_gettop(L) - nargs - 4; // 4: overloads, errors, key, table + // 2 extra items on stack below results: idx_funcs, idx_errors + return lua_gettop(L) - nargs - 2; } else if (err == LUA_ERRRUN) { @@ -1620,10 +1694,8 @@ inline int try_overload_functions(lua_State* L) } else { - return lua_error_x(L); // critical error: rethrow + return lua_error_x(L); // critical error: rethrow } - - lua_pop(L, 1); // pop value (table) } lua_Debug debug; diff --git a/Source/LuaBridge/detail/Namespace.h b/Source/LuaBridge/detail/Namespace.h index 7c742fdf..266daf4b 100644 --- a/Source/LuaBridge/detail/Namespace.h +++ b/Source/LuaBridge/detail/Namespace.h @@ -580,30 +580,41 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads - - int idx = 1; + // upvalue 1: OverloadSet (C++ struct with arity + type checker per overload) + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); ([&] { - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); + detail::OverloadEntry entry; if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); + { + entry.arity = -1; + entry.checker = nullptr; + } else - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); - detail::push_function(L, std::move(functions), name); - lua_settable(L, -3); + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + overload_set->entries.push_back(entry); + + } (), ...); + + // upvalue 2: flat table of function closures indexed 1..N + lua_createtable(L, static_cast(sizeof...(Functions)), 0); + + int idx = 1; - lua_rawseti(L, -2, idx); - ++idx; + ([&] + { + detail::push_function(L, std::move(functions), name); + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, name, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, name, 2); } rawsetfield(L, -2, name); @@ -772,6 +783,40 @@ class Namespace : public detail::Registrar // create new closure of const try_overload_functions with new table if constexpr (detail::const_functions_count > 0) { + // upvalue 1: OverloadSet + auto* overload_set_const_unaligned = lua_newuserdata_aligned(L); + auto* overload_set_const = align(overload_set_const_unaligned); + + ([&] + { + if (!detail::is_const_function) + return; + + detail::OverloadEntry entry; + if constexpr (detail::is_any_cfunction_pointer_v) + { + entry.arity = -1; + entry.checker = nullptr; + } + else if constexpr (detail::is_proxy_member_function_v) + { + using ArgsPack = detail::remove_first_type_t>; + entry.arity = static_cast(detail::member_function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + else + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::member_function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + overload_set_const->entries.push_back(entry); + + } (), ...); + + LUABRIDGE_ASSERT(!overload_set_const->entries.empty()); + + // upvalue 2: flat table of function closures lua_createtable(L, static_cast(detail::const_functions_count), 0); int idx = 1; @@ -781,25 +826,12 @@ class Namespace : public detail::Registrar if (!detail::is_const_function) return; - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); - else - lua_pushinteger(L, static_cast(detail::member_function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); detail::push_member_function(L, std::move(functions), name); - lua_settable(L, -3); - - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - LUABRIDGE_ASSERT(idx > 1); - - lua_pushcclosure_x(L, &detail::try_overload_functions, name, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, name, 2); lua_pushvalue(L, -1); // Stack: co, cl, st, function, function rawsetfield(L, -4, name); // Stack: co, cl, st, function rawsetfield(L, -4, name); // Stack: co, cl, st @@ -808,6 +840,40 @@ class Namespace : public detail::Registrar // create new closure of non const try_overload_functions with new table if constexpr (detail::non_const_functions_count > 0) { + // upvalue 1: OverloadSet + auto* overload_set_nonconst_unaligned = lua_newuserdata_aligned(L); + auto* overload_set_nonconst = align(overload_set_nonconst_unaligned); + + ([&] + { + if (detail::is_const_function) + return; + + detail::OverloadEntry entry; + if constexpr (detail::is_any_cfunction_pointer_v) + { + entry.arity = -1; + entry.checker = nullptr; + } + else if constexpr (detail::is_proxy_member_function_v) + { + using ArgsPack = detail::remove_first_type_t>; + entry.arity = static_cast(detail::member_function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + else + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::member_function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + overload_set_nonconst->entries.push_back(entry); + + } (), ...); + + LUABRIDGE_ASSERT(!overload_set_nonconst->entries.empty()); + + // upvalue 2: flat table of function closures lua_createtable(L, static_cast(detail::non_const_functions_count), 0); int idx = 1; @@ -817,25 +883,12 @@ class Namespace : public detail::Registrar if (detail::is_const_function) return; - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); - else - lua_pushinteger(L, static_cast(detail::member_function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); detail::push_member_function(L, std::move(functions), name); - lua_settable(L, -3); - - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - LUABRIDGE_ASSERT(idx > 1); - - lua_pushcclosure_x(L, &detail::try_overload_functions, name, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, name, 2); rawsetfield(L, -3, name); // Stack: co, cl, st } } @@ -868,26 +921,33 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads + // upvalue 1: OverloadSet + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + ([&] + { + using ArgsPack = detail::function_arguments_t; + detail::OverloadEntry entry; + entry.arity = static_cast(detail::function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + overload_set->entries.push_back(entry); + + } (), ...); + + // upvalue 2: flat table of function closures + lua_createtable(L, static_cast(sizeof...(Functions)), 0); int idx = 1; ([&] { - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); lua_pushcclosure_x(L, &detail::constructor_placement_proxy>, className, 0); - lua_settable(L, -3); - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, className, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, className, 2); } rawsetfield(L, -2, "__call"); @@ -926,8 +986,31 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads + // upvalue 1: OverloadSet + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + ([&] + { + detail::OverloadEntry entry; + if constexpr (detail::is_any_cfunction_pointer_v) + { + entry.arity = -1; + entry.checker = nullptr; + } + else + { + // skip void* first arg (placement new destination, not a Lua argument) + using ArgsPack = detail::remove_first_type_t>; + entry.arity = static_cast(detail::function_arity_excluding_v) - 1; + entry.checker = &detail::overload_type_checker; + } + overload_set->entries.push_back(entry); + + } (), ...); + + // upvalue 2: flat table of function closures + lua_createtable(L, static_cast(sizeof...(Functions)), 0); int idx = 1; @@ -935,23 +1018,13 @@ class Namespace : public detail::Registrar { using F = detail::constructor_forwarder; - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); - else - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v) - 1); // 1: for void* ptr - lua_settable(L, -3); - lua_pushinteger(L, 2); lua_newuserdata_aligned(L, F(std::move(functions))); lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); - lua_settable(L, -3); - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, className, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, className, 2); } rawsetfield(L, -2, "__call"); // Stack: co, cl, st @@ -979,26 +1052,33 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads + // upvalue 1: OverloadSet + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + ([&] + { + using ArgsPack = detail::function_arguments_t; + detail::OverloadEntry entry; + entry.arity = static_cast(detail::function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + overload_set->entries.push_back(entry); + + } (), ...); + + // upvalue 2: flat table of function closures + lua_createtable(L, static_cast(sizeof...(Functions)), 0); int idx = 1; ([&] { - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); lua_pushcclosure_x(L, &detail::constructor_container_proxy>, className, 0); - lua_settable(L, -3); - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, className, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, className, 2); } rawsetfield(L, -2, "__call"); @@ -1033,8 +1113,30 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads + // upvalue 1: OverloadSet + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); + + ([&] + { + detail::OverloadEntry entry; + if constexpr (detail::is_any_cfunction_pointer_v) + { + entry.arity = -1; + entry.checker = nullptr; + } + else + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + overload_set->entries.push_back(entry); + + } (), ...); + + // upvalue 2: flat table of function closures + lua_createtable(L, static_cast(sizeof...(Functions)), 0); int idx = 1; @@ -1042,23 +1144,13 @@ class Namespace : public detail::Registrar { using F = detail::container_forwarder; - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); - if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); - else - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); lua_newuserdata_aligned(L, F(std::move(functions))); lua_pushcclosure_x(L, &detail::invoke_proxy_constructor, className, 1); - lua_settable(L, -3); - lua_rawseti(L, -2, idx); - ++idx; + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, className, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, className, 2); } rawsetfield(L, -2, "__call"); // Stack: co, cl, st @@ -1593,30 +1685,41 @@ class Namespace : public detail::Registrar } else { - // create new closure of try_overloads with new table - lua_createtable(L, static_cast(sizeof...(Functions)), 0); // reserve space for N overloads - - int idx = 1; + // upvalue 1: OverloadSet (C++ struct with arity + type checker per overload) + auto* overload_set_unaligned = lua_newuserdata_aligned(L); + auto* overload_set = align(overload_set_unaligned); ([&] { - lua_createtable(L, 2, 0); // reserve space for: function, arity - lua_pushinteger(L, 1); + detail::OverloadEntry entry; if constexpr (detail::is_any_cfunction_pointer_v) - lua_pushinteger(L, -1); + { + entry.arity = -1; + entry.checker = nullptr; + } else - lua_pushinteger(L, static_cast(detail::function_arity_excluding_v)); - lua_settable(L, -3); - lua_pushinteger(L, 2); - detail::push_function(L, std::move(functions), name); - lua_settable(L, -3); + { + using ArgsPack = detail::function_arguments_t; + entry.arity = static_cast(detail::function_arity_excluding_v); + entry.checker = &detail::overload_type_checker; + } + overload_set->entries.push_back(entry); + + } (), ...); - lua_rawseti(L, -2, idx); - ++idx; + // upvalue 2: flat table of function closures indexed 1..N + lua_createtable(L, static_cast(sizeof...(Functions)), 0); + + int idx = 1; + + ([&] + { + detail::push_function(L, std::move(functions), name); + lua_rawseti(L, -2, idx++); } (), ...); - lua_pushcclosure_x(L, &detail::try_overload_functions, name, 1); + lua_pushcclosure_x(L, &detail::try_overload_functions, name, 2); } rawsetfield(L, -2, name);