Skip to content

Conversation

@ashleynh
Copy link
Collaborator

While determining whether repeat sequences of instructions are candidates for outlining, remove sequences that overlap, giving weight to sequences that are longer and appear more frequently.

@ashleynh ashleynh force-pushed the intervals branch 3 times, most recently from 1a589b6 to 0d961b8 Compare January 17, 2025 21:03
@ashleynh ashleynh changed the title [*WIP* - Outlining] Remove overlapping sequences [Outlining] Remove overlapping sequences Jan 17, 2025
@ashleynh ashleynh requested a review from tlively January 18, 2025 00:05
@ashleynh ashleynh marked this pull request as ready for review January 18, 2025 00:05
Comment on lines 36 to 38
bool operator<(const Interval& other) const {
return start < other.start && weight < other.weight;
}
Copy link
Member

Choose a reason for hiding this comment

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

This should take end into account as well. Otherwise the std::set<Interval> returned by IntervalProcessor::getOverlaps() will not be able to hold two intervals that differ only in their ends.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


std::set<Interval>
IntervalProcessor::getOverlaps(std::vector<Interval>& intervals) {
std::sort(intervals.begin(), intervals.end(), [](Interval a, Interval b) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
std::sort(intervals.begin(), intervals.end(), [](Interval a, Interval b) {
std::sort(intervals.begin(), intervals.end(), [](const Interval& a, const Interval& b) {

Just to avoid copying intervals around unnecessarily.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

});

std::set<Interval> overlaps;
auto& firstInterval = intervals[0];
Copy link
Member

Choose a reason for hiding this comment

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

There should be an early return if the input vector is empty to avoid UB here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

for (auto startIdx : substring.StartIndices) {
auto interval =
Interval(startIdx,
startIdx + substring.Length - 1,
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised we're using intervals inclusive of their ends. Would this work without the - 1 as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, done

auto interval =
Interval(startIdx,
startIdx + substring.Length - 1,
substring.Length * substring.StartIndices.size());
Copy link
Member

Choose a reason for hiding this comment

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

Probably worth a comment about why we are using this weight.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 170 to 174
std::set<Interval> overlaps = IntervalProcessor::getOverlaps(intervals);
std::set<unsigned> doNotInclude;
for (auto& interval : overlaps) {
doNotInclude.insert(intervalMap[interval]);
}
Copy link
Member

Choose a reason for hiding this comment

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

We could simplify the code here and get away without any map or set lookups if IntervalProcessor returned a sequence of kept indices in its input vector rather than a set of removed intervals. With a sequence of kept indices, we could directly construct the list of kept substrings.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

great idea, thanks!

)
)

;; Test that no attempt is made to outline overlapping repeat substrings
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to add comments about what the overlapping substrings are.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 1013 to 1016
(drop (i32.add
(i32.const 0)
(i32.const 1)
))
Copy link
Member

Choose a reason for hiding this comment

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

We could make the test more concise and easy to read by just using constants and drops, unless I'm missing some reason why this wouldn't work.

(drop (i32.const 0))
(drop (i32.const 1))
(drop (i32.const 2))
(drop (i32.const 3))
(drop (i32.const 0))
(drop (i32.const 1))
(drop (i32.const 2))
(drop (i32.const 3))
(drop (i32.const 1))
(drop (i32.const 2))
(drop (i32.const 1))
(drop (i32.const 2))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good call, thanks

};

struct IntervalProcessor {
static std::set<Interval> getOverlaps(std::vector<Interval>&);
Copy link
Collaborator Author

@ashleynh ashleynh Jan 23, 2025

Choose a reason for hiding this comment

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

add gTests for edge cases

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

It would still be good to include gtest unit tests for the various kinds of overlaps.

for (Index i = 0; i < substrings.size(); i++) {
auto substring = substrings[i];
for (auto startIdx : substring.StartIndices) {
// TODO: This weight was picked with an assumption
Copy link
Member

Choose a reason for hiding this comment

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

What assumption?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

DBG(printHashString(stringify.hashString, stringify.exprs));
// Remove substrings that are substrings of longer repeat substrings.
substrings = StringifyProcessor::dedupe(substrings);
// Remove substrings with overlapping indices
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// Remove substrings with overlapping indices
// Remove substrings with overlapping indices.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines +153 to +154
std::vector<Interval> intervals;
std::vector<int> substringIdxs;
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have a comment saying how these two vectors relate to each other.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 179 to 182
if (substringsIncluded.find(substringIdx) != substringsIncluded.end()) {
continue;
}
substringsIncluded.insert(substringIdx);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (substringsIncluded.find(substringIdx) != substringsIncluded.end()) {
continue;
}
substringsIncluded.insert(substringIdx);
if (!substringsIncluded.insert(substringIdx)->second) {
continue;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

};

struct IntervalProcessor {
// TODO: Given a vector of Interval, returns a vector of the indices, mapping
Copy link
Member

Choose a reason for hiding this comment

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

What's the TODO here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it was for me to review the comment before submitting

Comment on lines 184 to 186
if (seenCount[substringIdx] == substring.StartIndices.size() &&
substringsIncluded.insert(substringIdx).second) {
result.push_back(substring);
Copy link
Member

Choose a reason for hiding this comment

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

So we're only considering a substring for outlining at all if all of its ocurrences survive overlap filtering? Could we keep the substring in consideration and just remove the particular occurrence of it that had the overlap instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

std::sort(
intIntervals.begin(), intIntervals.end(), [](const auto& a, const auto& b) {
return a.first.start < b.first.end;
return a.first.start < b.first.start;
Copy link
Member

Choose a reason for hiding this comment

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

No need for the lambda here if you fix operator< to be a total order (meaning that for any pair of intervals a and b, exactly one of a < b,b < a, or a == b is true)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


std::vector<int> result;
auto& firstInterval = intIntervals[0];
auto& formerInterval = intIntervals[0];
Copy link
Member

Choose a reason for hiding this comment

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

Making this a reference means that when you do formerInterval = latterInterval below, it writes to the first element in intIntervals, which is a little odd. Intervals should be small enough that copying them is cheap, so let's just make this a non-reference. Alternatively, to avoid copying intervals, you could make this an index.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

firstInterval = nextInterval;
} else {
result.push_back(firstInterval.second);
if (latterInterval.first.weight > formerInterval.first.weight) {
Copy link
Member

Choose a reason for hiding this comment

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

If the weights are equal, perhaps you can choose to keep the interval with the nearest end to reduce its potential to overlap with subsequent intervals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

// back to the original input vector, of non-overlapping indices, ie, the
// intervals that overlap have already been removed.
// Given a vector of Interval, returns a vector of the indices that, mapping
// back to the original input vector, do not overlap with each other, ie: the
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// back to the original input vector, do not overlap with each other, ie: the
// back to the original input vector, do not overlap with each other, i.e. the

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Copy link
Member

Choose a reason for hiding this comment

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

Doesn't look like it!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

batched suggestion

std::vector<Interval> intervals;
intervals.emplace_back(Interval{0, 4, 2});
intervals.emplace_back(Interval{4, 8, 2});
ASSERT_EQ(IntervalProcessor::filterOverlaps(intervals).size(), 2u);
Copy link
Member

Choose a reason for hiding this comment

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

It would be better to test the precise results rather than just the size of the results. You can still do it with a single ASSERT_EQ:

std::vector<int> expected{0, 1};
ASSERT_EQ(IntervalProcessor::filterOverlaps(intervals), expected);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


struct IntervalProcessor {
// Given a vector of Interval, returns a vector of the indices that, mapping
// back to the original input vector, do not overlap with each other, ie: the
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

adjust punctuation around ie to , i.e.,

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


// Construct intervals
for (Index i = 0; i < substrings.size(); i++) {
auto substring = substrings[i];
Copy link
Member

Choose a reason for hiding this comment

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

To avoid copying the list of start indices and whatever else is in there:

Suggested change
auto substring = substrings[i];
auto& substring = substrings[i];

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

batched suggestion

Comment on lines 185 to 186
result.push_back(SuffixTree::RepeatedSubstring(
{substrings[i].Length, std::move(startIndices[i])}));
Copy link
Member

Choose a reason for hiding this comment

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

Can this be an emplace_back to really be sure we're not copying the vector of start indices?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 165 to 167
auto interval =
Interval(startIdx, startIdx + substring.Length, substring.Length);
intervals.push_back(interval);
Copy link
Member

Choose a reason for hiding this comment

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

This could be slightly shorter using emplace_back.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines 22 to 23
#include <iostream>
#include <optional>
Copy link
Member

Choose a reason for hiding this comment

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

Looks like these are stale.

Suggested change
#include <iostream>
#include <optional>

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

batched suggestion

Comment on lines 50 to 51
// former overlaps with candidate
// replace former if the weights are the same but the candidate ends earlier
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// former overlaps with candidate
// replace former if the weights are the same but the candidate ends earlier
// Former overlaps with candidate. Replace former if the weights are the same but the candidate ends earlier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

batched suggestion

// back to the original input vector, of non-overlapping indices, ie, the
// intervals that overlap have already been removed.
// Given a vector of Interval, returns a vector of the indices that, mapping
// back to the original input vector, do not overlap with each other, ie: the
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't look like it!

ASSERT_EQ(IntervalProcessor::filterOverlaps(intervals), expected);
}

TEST(IntervalsTest, TestOverlapFoundA) {
Copy link
Member

Choose a reason for hiding this comment

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

It would be best to give these more descriptive names like TestOverlapInnerNested, TestOverlapOuterNested, TestOverlapAscendingSequence, TestOverlapDescendingSequence, etc. Ideally the role each interval plays in a test should be clear from the name of the test.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Comment on lines +1038 to +1044
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
Copy link
Member

Choose a reason for hiding this comment

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

Since this substring isn't outlined at all, it's not clear to me that the test is behaving as expected. If we add more repetitions of this shorter substring, will it be outlined?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done

Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

LGTM with these last comments addressed.

Comment on lines +184 to +185
result.emplace_back(SuffixTree::RepeatedSubstring(
{substrings[i].Length, std::move(startIndices[i])}));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
result.emplace_back(SuffixTree::RepeatedSubstring(
{substrings[i].Length, std::move(startIndices[i])}));
result.emplace_back(substrings[i].Length, std::move(startIndices[i]));

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When I remove SuffixTree::RepeatedSubstring, I get the following error:

FAILED: src/passes/CMakeFiles/passes.dir/hash-stringify-walker.cpp.o /usr/bin/c++ -I/src -I/third_party/FP16/include -I/third_party/llvm-project/include -I -DBUILD_LLVM_DWARF -Wall -Werror -Wextra -Wno-unused-parameter -Wno-dangling-pointer -fno-omit-frame-pointer -fno-rtti -Wno-implicit-int-float-conversion -Wno-unknown-warning-option -Wswitch -Wimplicit-fallthrough -Wnon-virtual-dtor -fPIC -fdiagnostics-color=always -g -g3 -std=c++17 -MD -MT src/passes/CMakeFiles/passes.dir/hash-stringify-walker.cpp.o -MF src/passes/CMakeFiles/passes.dir/hash-stringify-walker.cpp.o.d -o src/passes/CMakeFiles/passes.dir/hash-stringify-walker.cpp.o -c /src/passes/hash-stringify-walker.cpp /src/passes/hash-stringify-walker.cpp: In static member function ‘static std::vector<wasm::SuffixTree::RepeatedSubstring> wasm::StringifyProcessor::filterOverlaps(const std::vector<wasm::SuffixTree::RepeatedSubstring>&)’: /src/passes/hash-stringify-walker.cpp:184:26: error: no matching function for call to ‘std::vector<wasm::SuffixTree::RepeatedSubstring>::emplace_back(<brace-enclosed initializer list>)’ 184 | result.emplace_back( | ~~~~~~~~~~~~~~~~~~~^ 185 | {substrings[i].Length, std::move(startIndices[i])}); | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In file included from /usr/include/c++/14/vector:72, from /src/wasm.h:34, from /src/ir/boolean.h:20, from /src/ir/bits.h:20, from /src/ir/properties.h:20, from /src/ir/iteration.h:20, from /src/passes/stringify-walker.h:20, from /src/passes/hash-stringify-walker.cpp:17: /usr/include/c++/14/bits/vector.tcc:111:7: note: candidate: ‘std::vector<_Tp, _Alloc>::reference std::vector<_Tp, _Alloc>::emplace_back(_Args&& ...) [with _Args = {}; _Tp = wasm::SuffixTree::RepeatedSubstring; _Alloc = std::allocator<wasm::SuffixTree::RepeatedSubstring>; reference = wasm::SuffixTree::RepeatedSubstring&]’ 111 | vector<_Tp, _Alloc>:: | ^~~~~~~~~~~~~~~~~~~ /usr/include/c++/14/bits/vector.tcc:111:7: note: candidate expects 0 arguments, 1 provided At global scope: cc1plus: note: unrecognized command-line option ‘-Wno-unknown-warning-option’ may have been intended to silence earlier diagnostics cc1plus: note: unrecognized command-line option ‘-Wno-implicit-int-float-conversion’ may have been intended to silence earlier diagnostics [3/18] Building CXX object src/passes/CMakeFiles/passes.dir/Outlining.cpp.o ninja: build stopped: subcommand failed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no constructor provided on SuffixTree, so we'll keep as-is

intervals.emplace_back(Interval{12, 18, 3});
intervals.emplace_back(Interval{14, 21, 5});
intervals.emplace_back(Interval{23, 28, 5});
std::vector<int> expected{4, 3, 7};
Copy link
Member

Choose a reason for hiding this comment

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

Am I reading correctly that this is keeping {6, 20, 15} at index 4 followed by {4, 11, 1} at index 3? That doesn't seem right.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Indeed this expected vector was wrong and the test was failing. it is keeping {0, 4, 7}.


TEST(IntervalsTest, TestOne) {
std::vector<Interval> intervals;
intervals.emplace_back(Interval{0, 2, 5});
Copy link
Member

Choose a reason for hiding this comment

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

Since we're using emplace_back, we can just pass the arguments to the constructor without having to construct and then move a temporary object.

Suggested change
intervals.emplace_back(Interval{0, 2, 5});
intervals.emplace_back(0, 2, 5);

Comment on lines 80 to 81
intervals.emplace_back(Interval{2, 4, 2});
intervals.emplace_back(Interval{3, 6, 5});
Copy link
Member

Choose a reason for hiding this comment

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

By the name of the test, I would have expected to see an interval nested fully within another interval.

@ashleynh ashleynh requested a review from tlively March 25, 2025 22:49
Copy link
Member

@tlively tlively left a comment

Choose a reason for hiding this comment

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

🎉 🎉 🎉

@ashleynh ashleynh merged commit 1f01a77 into main Mar 26, 2025
14 checks passed
@ashleynh ashleynh deleted the intervals branch March 26, 2025 00:40
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.

2 participants