Skip to content

[NFC] Use a sorted vector instead of a map in ConstraintAnalysis - #9154

Open
kripken wants to merge 13 commits into
WebAssembly:mainfrom
kripken:c.sorted
Open

kripken wants to merge 13 commits into
WebAssembly:mainfrom
kripken:c.sorted

Conversation

@kripken

@kripken kripken commented Sep 25, 2026

Copy link
Copy Markdown
Member

We usually have only a few useful constraints, so the map is mostly
empty. An unordered_map is then pretty inefficient, and it is better
to use a vector. Sorting it makes lookup still pretty fast.

This especially helps in ORing two sets of constraints, as we can
just do an intersection (the OR result is only interesting if we had
something in both inputs - otherwise we can prove nothing), which
is efficient on sorted vectors.

To implement this, generalize the existing SortedVector.

This makes the pass 50% faster on average, though the spread
is wide (20%-almost 2X faster). I tested around 10 real-world
wasm files and did not see a single one with less than an 18%
speedup, and never a regression.

@kripken
kripken requested a review from a team as a code owner September 25, 2026 19:03
@kripken
kripken requested review from aheejin and removed request for a team September 25, 2026 19:03
Comment thread src/ir/constraint.cpp
if (constraints.provesNothing()) {
setProvesNothing(index);
} else {
if (!constraints.provesNothing()) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by fix, see the comment on line 1054 - we already prove nothing at this point.

Comment thread src/ir/constraint.cpp
// We just proved we are in unreachable code.
unreachable = true;
map.clear();
refs.clear();

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another drive-by trivial fix (irrelevent for correctness, see the comment on new line 1215).

Comment thread src/ir/constraint.cpp

auto& refIndexes = iter->second;
auto refIndexes = std::move(iter->value);
refs.erase(iter);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(as above, we kept around stale refs unnecessarily; added a comment in the header to mention that this function is called when we wipe out all the info)

@kripken
kripken requested a review from tlively September 28, 2026 17:57
Comment thread src/ir/constraint.cpp
Comment on lines +1211 to +1213
if (map.size() != oldSize) {
changed = true;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could instead have intersectAndFilter return true if there are any changes it knows about. That might be slightly nicer because it wouldn't be using the size as a side channel for that information.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I kind of disagree. The size isn't a side channel, it is the thing we care about - whether the size changed. Adding code inside intersectAndFilter to track if we remove anything would be more code, and not needed in other callers (though I suppose as this is in a header, that overhead should get optimized out).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We care about whether there were any changes at all, not about whether the size changed in particular. The only reason checking the size works here is because in-place intersection has the property that the size goes down if the intersection makes any changes.

... but I don't feel strongly about it :)

Comment thread src/ir/constraint.cpp
// about, propagate it. TODO: even without equality, we can add more
// constraints here (e.g. x < y and y < 10 can lead to proving x < 10)
if (auto* other = std::get_if<Index>(&condition.constraint.term)) {
auto otherConstraints = get(*other);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did get go away in these lookup sequences?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get() is a nice "userspace" API that handles missing elements etc., generating an empty AndedConstraintSet if so. It seems nicer and more efficient to access the data structure directly, in the internal methods.

Comment thread src/ir/constraint.h
Index index;
T value;

bool operator<(const Indexed& other) const { return index < other.index; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really should figure out if we can start using <=> yet...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we checked and we couldn't? Maybe my memory is faulty...

void insert(Index x) {
T& insert(T x) {
if (empty() || back() < x) {
push_back(std::move(x));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be emplace_back so the move constructor gets called, or does it not matter?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am no expert but I don't think it matters. Gemini concurs fwiw.

Comment on lines +127 to +129
if (skip > 0) {
(*this)[i - skip] = std::move((*this)[i]);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does guarding the move behind skip > 0 make a difference for performance? Generally things should be robust against being moved into themselves. Same question below in intersectAndFilter.

namespace wasm {

struct SortedVector : public std::vector<Index> {
template<typename T> struct SortedVector : public std::vector<T> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semi-related to this PR: I think we shouldn't use public inheritance here but either composition or private inheritance. If a SortedVector is ever passed as a std::vector& then the vector methods will break the invariants of the class (e.g. insert won't preserve the order).

Also deleting a SortedVector via a pointer to std::vector is UB since vector's destructor isn't virtual.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants