From 18e11da94bc661fad8b8f6fe1077883e502251b5 Mon Sep 17 00:00:00 2001 From: Martin Hofmann Date: Wed, 26 Aug 2026 20:23:44 +0200 Subject: [PATCH 1/2] Give per-user-node vectors inline storage Each topological node holds one value per user node, and only link-merged supernodes have more than one -- in a grid without links every topological node has exactly one. Three of these vectors are rebuilt on every scenario (node_injection, node_flow_from_branch and bus_injection), so each node asks the allocator for room to store a single complex number three times per scenario. Give them inline room for one element, so the ordinary node does not allocate. Link-merged nodes still work, they just allocate as before. The indexing interface is unchanged. As a side effect the copy from node_injection into bus_injection stops being a heap copy, because both sides are now the same inline type. Expose the container generically rather than at the point of use: a new common/small_vector.hpp holds a single using declaration for SmallVector, alongside the other type aliases in common/. Other call sites can then reach for it without pulling in boost directly, and the underlying container can be swapped in one place. boost::container::small_vector is header-only and Boost::headers is already an INTERFACE dependency of the power_grid_model target, so this adds nothing to the build. The inline capacity stays a per-use-site decision; here it is 1 because that is exactly what an ordinary topological node holds. Measured on tests/benchmark_cpp (radial grid, 2605 nodes, 1000-scenario symmetric Newton-Raphson batch), MSVC /O2, against the parent commit, 10 interleaved rounds per arm on an otherwise idle machine, medians: produce output 2.4487 s -> 2.2470 s 1.09x math calculation 2.0757 s -> 2.1361 s control, unchanged within noise whole batch run 4.9217 s -> 4.7514 s 1.04x The change won 9 of the 10 paired rounds, so the effect is real, but the 0.20 s median gain sits inside main's own 0.69 s run-to-run spread, so treat it as modest rather than decisive. The gain is smaller than it would have been before #1536, because most of the per-node allocation cost now sits in compute_link_solver, which this does not touch. See the linked issue. All ten C++ test suites pass, 176999 assertions, built with warnings as errors, including the handling-of-links cases that exercise supernodes holding more than one user node. Signed-off-by: Martin Hofmann --- .../calculation_parameters.hpp | 7 ++++++- .../power_grid_model/common/small_vector.hpp | 20 +++++++++++++++++++ .../main_core/topological_node_output.hpp | 8 ++++---- .../test_topological_node_output.cpp | 7 ++++--- 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 power_grid_model_c/power_grid_model/include/power_grid_model/common/small_vector.hpp diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index 87c70004e1..429372b30a 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -8,6 +8,7 @@ #include "common/enum.hpp" #include "common/grouped_index_vector.hpp" #include "common/maybe_owning_view.hpp" +#include "common/small_vector.hpp" #include "common/statistics.hpp" #include "common/three_phase_tensor.hpp" @@ -410,10 +411,14 @@ struct OptimizerOutput { template struct SupernodeOutput; +// One entry per user node of a topological node. Only link-merged supernodes hold more than one, so +// inline room for a single element keeps the ordinary node from allocating at all. +template using UserNodeValueVector = SmallVector, 1>; + template struct SupernodeOutput { using sym = decode_symmetry_v; - ComplexValueVector bus_injection; // user bus output + UserNodeValueVector bus_injection; // user bus output std::vector> link; // user link }; template struct SupernodeOutput { diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/small_vector.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/small_vector.hpp new file mode 100644 index 0000000000..cfa4ec956d --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/small_vector.hpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "common.hpp" + +#include + +#include + +namespace power_grid_model { + +// A vector that keeps room for the first N elements inside the object itself and only allocates +// beyond that. Use it where a container is almost always short but occasionally is not. N is a +// per-use-site decision: too large inflates every object for slots that go unused. +template using SmallVector = boost::container::small_vector; + +} // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topological_node_output.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topological_node_output.hpp index bc060310ae..b771004209 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topological_node_output.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/main_core/topological_node_output.hpp @@ -21,8 +21,8 @@ namespace power_grid_model::main_core { namespace detail { template struct SuperNodeSolverInput { std::span links; - ComplexValueVector node_injection; - ComplexValueVector node_flow_from_branch; + UserNodeValueVector node_injection; + UserNodeValueVector node_flow_from_branch; ComplexValueVector get_total_injection_per_node() const { assert(node_injection.size() == node_flow_from_branch.size()); @@ -225,8 +225,8 @@ solve_topological_nodes(LinkSolver link_solver, State const& state, std::views::transform([](auto const& topo_node) -> SuperNodeSolverInput { auto const node_number = topo_node.user_nodes.size(); return {.links = std::span{topo_node.user_links}, - .node_injection = ComplexValueVector(node_number), - .node_flow_from_branch = ComplexValueVector(node_number)}; + .node_injection = UserNodeValueVector(node_number), + .node_flow_from_branch = UserNodeValueVector(node_number)}; }) | std::ranges::to(); diff --git a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp index b81337043e..4e438db91c 100644 --- a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp +++ b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp @@ -532,9 +532,10 @@ TEST_CASE("Test topological node output") { CHECK(mock.recorded_loads[1] == ComplexVector{DoubleComplex{}}); REQUIRE(result.size() == 2); - CHECK(result[0].bus_injection == - ComplexVector{dummy_complex_value_sym(), dummy_complex_value_sym(), dummy_complex_value_sym()}); - CHECK(result[1].bus_injection == ComplexVector{DoubleComplex{}}); + CHECK(result[0].bus_injection == UserNodeValueVector{dummy_complex_value_sym(), + dummy_complex_value_sym(), + dummy_complex_value_sym()}); + CHECK(result[1].bus_injection == UserNodeValueVector{DoubleComplex{}}); REQUIRE(result[0].link.size() == 2); check_close(result[0].link[0].s_f, 2.0 * dummy_complex_value_sym()); check_close(result[0].link[0].s_t, -2.0 * dummy_complex_value_sym()); From 304aceb4c9c89623b455cf7f2f2bbad1d747ccb3 Mon Sep 17 00:00:00 2001 From: Martin Hofmann Date: Thu, 27 Aug 2026 09:06:44 +0200 Subject: [PATCH 2/2] Fix clang-format indentation in test_topological_node_output.cpp Signed-off-by: Martin Hofmann --- .../cpp_unit_tests/main_core/test_topological_node_output.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp index 4e438db91c..3136f42d61 100644 --- a/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp +++ b/tests/cpp_unit_tests/main_core/test_topological_node_output.cpp @@ -533,8 +533,8 @@ TEST_CASE("Test topological node output") { REQUIRE(result.size() == 2); CHECK(result[0].bus_injection == UserNodeValueVector{dummy_complex_value_sym(), - dummy_complex_value_sym(), - dummy_complex_value_sym()}); + dummy_complex_value_sym(), + dummy_complex_value_sym()}); CHECK(result[1].bus_injection == UserNodeValueVector{DoubleComplex{}}); REQUIRE(result[0].link.size() == 2); check_close(result[0].link[0].s_f, 2.0 * dummy_complex_value_sym());