Skip to content

Commit 6763e71

Browse files
authored
[math] Add PartialPermutation::ResetToSize to mitigate heap use (#24482)
1 parent 88abc47 commit 6763e71

4 files changed

Lines changed: 111 additions & 16 deletions

File tree

math/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ drake_cc_googletest(
505505
deps = [
506506
":partial_permutation",
507507
"//common/test_utilities:expect_throws_message",
508+
"//common/test_utilities:limit_malloc",
508509
],
509510
)
510511

math/partial_permutation.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,20 @@ PartialPermutation::PartialPermutation(std::vector<int> permutation)
6060
}
6161

6262
PartialPermutation::PartialPermutation(int domain_size) {
63+
DRAKE_THROW_UNLESS(domain_size >= 0);
6364
permutation_.resize(domain_size, -1);
6465
}
6566

67+
void PartialPermutation::ResetToSize(int domain_size) {
68+
DRAKE_THROW_UNLESS(domain_size >= 0);
69+
permutation_.clear();
70+
permutation_.resize(domain_size, -1);
71+
// Potentially over-allocate the inverse permutation now, so that subsequent
72+
// push() calls don't allocate.
73+
inverse_permutation_.clear();
74+
inverse_permutation_.reserve(domain_size);
75+
}
76+
6677
int PartialPermutation::push(int i) {
6778
DRAKE_THROW_UNLESS(0 <= i && i < domain_size());
6879
if (!participates(i)) {

math/partial_permutation.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,25 @@ class PartialPermutation {
6262

6363
// Constructs a partial permutation of `domain_size` and
6464
// permuted_domain_size() equal to zero. In other words, participates(i) =
65-
// false for in [0, domain_size). The permutation can be updated with further
66-
// calls to push().
65+
// false for i in [0, domain_size). The permutation can be updated with
66+
// further calls to push().
6767
// @throws exception if domain_size is negative.
6868
explicit PartialPermutation(int domain_size);
6969

70+
// Resets a partial permutation to a new `domain_size` and clears the
71+
// permutation, such that permuted_domain_size() is equal to zero. In other
72+
// words, participates(i) = false for i in [0, domain_size). The permutation
73+
// can be updated with further calls to push().
74+
//
75+
// Allows reusing this object's storage with a minimum of heap allocation
76+
// activity, and anticipates the heap effects of subsequent push()
77+
// calls.
78+
//
79+
// @post permutation().capacity() >= domain_size()
80+
// @post inverse_permutation().capacity() >= domain_size()
81+
// @throws exception if domain_size is negative.
82+
void ResetToSize(int domain_size);
83+
7084
// If participates(i) = false, defines P(i) = permuted_domain_size() and
7185
// further increases the permuted domain size. If participates(i) = true, the
7286
// permutation does not change and this method simply returns P(i).

math/test/partial_permutation_test.cc

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "drake/common/eigen_types.h"
99
#include "drake/common/test_utilities/expect_throws_message.h"
10+
#include "drake/common/test_utilities/limit_malloc.h"
1011

1112
using Eigen::VectorXd;
1213

@@ -29,28 +30,96 @@ GTEST_TEST(PartialPermutation, EmptyPermutationFromStdVector) {
2930
}
3031

3132
// Creates a permutation of size 6 where none of the indexes participates and
32-
// adds permuted indexes one at a time with push().
33-
GTEST_TEST(PartialPermutation, PushElements) {
33+
// adds permuted indexes one at a time with push(). Also tests push() with
34+
// ResetToSize().
35+
GTEST_TEST(PartialPermutation, PushAndReset) {
3436
PartialPermutation p(6);
3537
EXPECT_EQ(p.domain_size(), 6);
3638
EXPECT_EQ(p.permuted_domain_size(), 0);
3739
for (int i = 0; i < 6; ++i) {
3840
EXPECT_FALSE(p.participates(i));
3941
}
4042

41-
EXPECT_EQ(p.push(1), 0);
42-
EXPECT_EQ(p.permuted_domain_size(), 1); // permuted domain increases.
43-
EXPECT_EQ(p.push(3), 1);
44-
EXPECT_EQ(p.permuted_domain_size(), 2); // permuted domain increases.
45-
EXPECT_EQ(p.push(2), 2);
46-
EXPECT_EQ(p.permuted_domain_size(), 3); // permuted domain increases.
47-
EXPECT_EQ(p.push(3), 1); // already added.
48-
EXPECT_EQ(p.permuted_domain_size(), 3); // No change.
49-
EXPECT_EQ(p.push(5), 3);
50-
EXPECT_EQ(p.permuted_domain_size(), 4); // permuted domain increases.
51-
5243
const std::vector<int> expected_permutation = {-1, 0, 2, 1, -1, 3};
53-
EXPECT_EQ(p.permutation(), expected_permutation);
44+
45+
// Uses push() to build the expected permutation setup, optionally wrapping
46+
// the push() calls in LimitMalloc guard blocks, and checks for expected
47+
// results.
48+
auto evaluate = [&](std::string_view context_message,
49+
bool push_is_guarded = false) {
50+
SCOPED_TRACE(context_message);
51+
52+
auto push = [&](int value) -> int {
53+
if (push_is_guarded) {
54+
drake::test::LimitMalloc guard;
55+
return p.push(value);
56+
} else {
57+
return p.push(value);
58+
}
59+
};
60+
61+
EXPECT_EQ(push(1), 0);
62+
EXPECT_EQ(p.permuted_domain_size(), 1); // permuted domain increases.
63+
EXPECT_EQ(push(3), 1);
64+
EXPECT_EQ(p.permuted_domain_size(), 2); // permuted domain increases.
65+
EXPECT_EQ(push(2), 2);
66+
EXPECT_EQ(p.permuted_domain_size(), 3); // permuted domain increases.
67+
EXPECT_EQ(push(3), 1); // already added.
68+
EXPECT_EQ(p.permuted_domain_size(), 3); // No change.
69+
EXPECT_EQ(push(5), 3);
70+
EXPECT_EQ(p.permuted_domain_size(), 4); // permuted domain increases.
71+
72+
EXPECT_EQ(p.permutation(), expected_permutation);
73+
};
74+
75+
evaluate("from constructor");
76+
77+
// For memory behavior tests below, first expand the permutation to full
78+
// size.
79+
p.ExtendToFullPermutation();
80+
ASSERT_EQ(p.permutation().size(), 6);
81+
ASSERT_EQ(p.inverse_permutation().size(), 6);
82+
83+
// Reset to same size.
84+
{
85+
drake::test::LimitMalloc guard;
86+
p.ResetToSize(6);
87+
}
88+
EXPECT_EQ(p.domain_size(), 6);
89+
EXPECT_EQ(p.permuted_domain_size(), 0);
90+
for (int i = 0; i < 6; ++i) {
91+
EXPECT_FALSE(p.participates(i));
92+
}
93+
evaluate("from reset, same size", /*push_is_guarded*/ true);
94+
95+
// Reset, smaller size.
96+
{
97+
drake::test::LimitMalloc guard;
98+
p.ResetToSize(3);
99+
}
100+
EXPECT_EQ(p.domain_size(), 3);
101+
EXPECT_EQ(p.permuted_domain_size(), 0);
102+
for (int i = 0; i < 3; ++i) {
103+
EXPECT_FALSE(p.participates(i));
104+
}
105+
106+
// Reset back to original size.
107+
ASSERT_GE(p.permutation().capacity(), 6);
108+
ASSERT_GE(p.inverse_permutation().capacity(), 6);
109+
{
110+
drake::test::LimitMalloc guard;
111+
p.ResetToSize(6);
112+
}
113+
EXPECT_EQ(p.domain_size(), 6);
114+
EXPECT_EQ(p.permuted_domain_size(), 0);
115+
for (int i = 0; i < 6; ++i) {
116+
EXPECT_FALSE(p.participates(i));
117+
}
118+
evaluate("from reset, return to original size from smaller",
119+
/*push_is_guarded*/ true);
120+
121+
// Reset, negative size.
122+
EXPECT_THROW(p.ResetToSize(-10), std::exception);
54123
}
55124

56125
GTEST_TEST(PartialPermutation, Construction) {

0 commit comments

Comments
 (0)