Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

tlively
Copy link
Member

@tlively tlively commented Sep 12, 2025

We previously special-cased copies from a field to itself in CFP.
Generalize the analysis to handle copies from any field in any type to
any field in any type. Along with this change, make the entire analysis
more precise by explicit analyzing the written values, analyzing the
readable values that result from the written values, then propagating
readable values back to the writable values via copies and iterating
until a fixed point. Use custom propagation logic after applying the
copies the first time to minimize the amount of work done when
propagating.

We previously special-cased copies from a field to itself in CFP.
Generalize the analysis to handle copies from any field in any type to
any field in any type. Along with this change, make the entire analysis
more precise by explicit analyzing the written values, analyzing the
readable values that result from the written values, then propagating
readable values back to the writable values via copies and iterating
until a fixed point. Use custom propagation logic after applying the
copies the first time to minimize the amount of work done when
propagating.
@tlively tlively requested a review from kripken September 12, 2025 22:02
Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

(some initial comments)

@@ -85,6 +86,42 @@ struct PossibleConstantValues {
// identify a constant value here.
void noteUnknown() { value = Many(); }

void packForField(const Field& field, bool isSigned = false) {
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment for this method. On the face of it it seems quite different than the others above it. Is it called when written to a packed field, or read from one?

}
return false;
}

Copy link
Member

Choose a reason for hiding this comment

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

Was this large chunk of code entirely unneeded? I'm not quite seeing the connection to this PR.

Copy link
Member Author

@tlively tlively Sep 12, 2025

Choose a reason for hiding this comment

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

Yeah, this was just dead code I removed as a drive-by. I can move it to a separate PR if you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

It's ok as is, I was just confused.

@@ -66,6 +70,40 @@ namespace wasm {

namespace {

struct CopyInfo {
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment on this struct. Copying is not obviously a key part of the optimization so some context could help.

@@ -436,17 +493,11 @@ struct PCVScanner
Type type,
Index index,
Copy link
Member

Choose a reason for hiding this comment

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

Maybe type and index could be prefixed dst to clarify they are the destination.

Comment on lines 538 to 540
for (auto& func : module->functions) {
functionCopyInfos[func.get()];
}
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
for (auto& func : module->functions) {
functionCopyInfos[func.get()];
}

FunctionStructValuesMap does these three lines in the constructor.

}
if (dst.exact == Inexact) {
// Propagate down to subtypes.
written[{dst.type, Exact}][dst.index].combine(val);
Copy link
Member

Choose a reason for hiding this comment

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

Why does this combine() not cause later propagation? In general I'd expect any such operation to open up new possible work. And specifically here, if we just found a new written value, then any copies from it should be propagated?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copies are from readable values to written values, so we only check for further copies to propagate when we update the readable values. Of course we do have to be careful to update the readable values accordingly whenever we update the writable values, too, since the readable values are a superset of the writable values.

Copy link
Member

Choose a reason for hiding this comment

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

I see, thanks, that makes sense - I missed that. Perhaps a comment?

(local $super (ref null $super))
(local $struct (ref null $struct))
(local $sub (ref null $sub))
(drop
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment here. I think it can say "we never wrote to $super, so the only things we can read are what was written to $struct (or $sub, if the reference in $copy was to a subtype)"

;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $init
;; Same as above, but now with a different value in $sub1.
Copy link
Member

Choose a reason for hiding this comment

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

The previous testcase didn't have $sub1? Or does this mean "added $sub1, with a different vaue in it" - ?

Copy link
Member Author

Choose a reason for hiding this comment

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

$sub from the previous case became $sub1 and got a new value, and we also added $sub2. Will clarify the comment.

(local $struct (ref null $struct))
(local $sub1 (ref null $sub1))
(local $sub2 (ref null $sub2))
(drop
Copy link
Member

Choose a reason for hiding this comment

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

A comment could mention that the copy might write to $sub1, so we can't optimize it; but $sub2 can only have the parent's value.

;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $init
;; Same as above.
Copy link
Member

Choose a reason for hiding this comment

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

Previous test didn't have sub1

Copy link
Member Author

Choose a reason for hiding this comment

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

I can globally rename sub1 to sub if that would help clarify things, but then we would lose the symmetry between the sub1 and sub2 names.

@kripken
Copy link
Member

kripken commented Sep 18, 2025

I'm halfway through the big testcase, but I wonder: is this maybe something we could test better programmatically? A small unittest/gtest could generate all combinations of "create the super,struct,sub1,sub2,other types, init with {options} and add a {copy}, add reads and exact reads", then run them through --fuzz-exec to verify.

That would cover correctness. Covering that we actually optimize is a little harder but maybe we can have a shorthand, something like [true|false, (pattern)] which says if we can optimize or not, and "pattern" is the choices used to generate the testcase, e.g. "exact write to sub1` - ?

@tlively
Copy link
Member Author

tlively commented Sep 18, 2025

There are so many interesting configurations to test that writing a script to generate them all might have almost been worth it. But even then it would probably make the most sense for such a script to generate lit test inputs so the tests could show the result of the optimization.

The number of interesting test cases is unfortunately increased by the fact that the first copies and subsequent copies are propagated by different code paths. Only because of these two code paths do we need tests with multiple copies. An alternative approach would be to have a test-only version of the pass that does the copy propagation in the most naive possible way and asserts that the results are the same as doing it in the more optimal way implemented here. Then we could rely on running that pass in the fuzzer to be sure of the correctness of the optimal approach.

HeapType type;
Exactness exact;
Index index;
bool isSigned;
Copy link
Member

Choose a reason for hiding this comment

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

Actually, can this struct be named WriteInfo? A copy would suggest a source and a destination, but this has just one side. And it is used in the queue in the sense of a write (we add work after adding a write iiuc)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that would be confusing because we only use this struct to record information for copies, not other writes.

Copy link
Member

Choose a reason for hiding this comment

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

Fair enough.

Copy link
Member

@kripken kripken left a comment

Choose a reason for hiding this comment

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

Ok, code lgtm, but I do still have half the big test left...

;; CHECK-NEXT: (local.get $super)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (i32.const 10)
Copy link
Member

Choose a reason for hiding this comment

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

wait, can't some of these be 0 or 10 because of the copy?

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