✕
Skip to content

[OptimizeInstructions] Remove redundant sign-exts - #9324

Open
tlively wants to merge 13 commits into
mainfrom
opt-inst-1-3-redundant-sign-ext
Open

tlively wants to merge 13 commits into
mainfrom
opt-inst-1-3-redundant-sign-ext

Conversation

@tlively

@tlively tlively commented Oct 29, 2026

Copy link
Copy Markdown
Member

Relax isSignExted to check signExtBits <= bits, teach getSignExtBits about signed narrow loads and i64.extend_i32_s, remove unary sign-extensions when maxBits < bits, strip wider inner sign-extensions inside narrower outer ones, and fold i64.extend32_s(i64.extend_i32_u(x)) to i64.extend_i32_s(x).

Relax isSignExted to check signExtBits <= bits, teach getSignExtBits about signed narrow loads and i64.extend_i32_s, remove unary sign-extensions when maxBits < bits, strip wider inner sign-extensions inside narrower outer ones, and fold i64.extend32_s(i64.extend_i32_u(x)) to i64.extend_i32_s(x).
@tlively
tlively requested a review from a team as a code owner October 29, 2026 04:56
@tlively
tlively requested review from aheejin and removed request for a team October 29, 2026 04:56

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enhances sign-extension optimizations in the OptimizeInstructions pass. It introduces a helper method getExpressionSignExtBits to track sign-extension widths, optimizes nested and redundant sign-extensions (both shift-based and unary), and simplifies expressions where the input's maximum bits are already within the target sign-extend limit. The feedback suggests a minor code cleanup to avoid a redundant cast by utilizing an already-bound pointer variable.

Comment thread src/passes/OptimizeInstructions.cpp Resolved

@aheejin aheejin left a comment

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.

It would be easier to understand to have each if case comment in the code has a simple example attached to it.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
Comment on lines +92 to +100
if (auto* unary = curr->dynCast<Unary>();
unary && unary->op == ExtendSInt32) {
// i64.extend_i32_s sign-extends from 32 bits, or from a narrower width if
// its i32 input is itself sign-extended (since any i32 sign extension is
// narrower than 32 bits and extending to i64 replicates the same sign
// bit).
bits = 32;
curr = unary->value;
}

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.

This unwraps an outer i64.extend_i32_s if one exists and we can recover 16 in the if below. But maybe it'd be nice to handle this case too and get 8? Or would this be overkill?

(i64.extend_i32_s
  (i32.extend16_s
    (i32.extend8_s (x))
  )
)

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.

OptimizeInstructions optimizes in post-order and re-optimizes whenever an expression is replaced, so in this case the middle i32.extend16_s should already have been removed by the time we look at the outer i64.extend_i32_s. So looking deeper in this function wouldn't have much benefit.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
Comment on lines 4512 to 4517
Index getSignExtBits(Expression* curr) {
if (Properties::getSignExtValue(curr)) {
return Properties::getSignExtBits(curr);
if (auto* unary = curr->dynCast<Unary>();
unary && unary->op == ExtendSInt32) {
auto innerBits = getSignExtBits(unary->value);
return innerBits > 0 ? innerBits : 32;
}

@aheejin aheejin Oct 31, 2026 •

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.

ExtendSInt32 check is also in LocalInfo::getExpressionSignExtBits, so I think we can skip that.

By the way can we merge this, LocalInfo::getExpressionSignExtBits, and Properties::getSignExtBits into one? We have three functions that do one task now. If having LocalInfo or not is a difference, we can provide it as a nullable option. We have a similar precedent already:

binaryen/src/ir/bits.h

Lines 164 to 166 in 677ad31

template<typename LocalInfoProvider = DummyLocalInfoProvider>
Index getMaxBits(Expression* curr,
LocalInfoProvider* localInfoProvider = nullptr) {

Can we merge all these into a single Properties::getSignExtBits?

(By the way, I'm new to this code, and getSignExtBits returning not the amount of sign-extended bits but the amount of 'kept' bits is really confusing... I'm not saying changing this is within the scope of this PR though)

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.

Improved the names and consolidated helpers in #9350. I have restacked this PR on top of that one.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
Comment on lines 631 to 632

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.

It's not identical-sized anymore because isSignExted was relaxed

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.

Updated the comment.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
if (auto* ext = Properties::getSignExtValue(curr->left)) {
// TODO: Reexamine whether replacing unary sign-extend instructions
// (as opposed to shift pairs) with zero-extend masks here is
// worthwhile, since an and with a constant is larger than a unary

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.

Suggested change
// worthwhile, since an and with a constant is larger than a unary
// worthwhile, since an 'and' with a constant is larger than a unary

Nit: This tripped my eyes several times because it looked like a conjunction... The same for other TODOs

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.

Fixed with backticks.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
Comment on lines +3506 to +3509
// TODO: Reexamine whether replacing unary sign-extend instructions (as
// opposed to shift pairs) with zero-extend masks in boolean contexts
// is worthwhile, since an and with a constant is larger than a unary
// extend.

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.

Suggested change
// TODO: Reexamine whether replacing unary sign-extend instructions (as
// opposed to shift pairs) with zero-extend masks in boolean contexts
// is worthwhile, since an and with a constant is larger than a unary
// extend.

This is within else if (auto* binary = boolean->dynCast<Binary>()) {

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.

Good catch. Removed.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
@@ -615,10 +635,22 @@ struct OptimizeInstructions
if (isSignExted(ext, bits) && extraLeftShifts == 0) {

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.

Suggested change
if (isSignExted(ext, bits) && extraLeftShifts == 0) {
if (isSignExted(ext, bits - extraLeftShifts)) {

This can unlock more cases, such as (extend8_s(x) << 16) >>_s 8.

Also if we do this, this case and the case below are representing the opposite cases: This case is when the inner is narrower and the case below is when the inner is wider. Would be nice to revise the comments to make it clear that these two cases are essentially the same thing with the inner and outer swapped.

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.

Good idea, done.

Comment thread src/passes/OptimizeInstructions.cpp Outdated
// having a larger sign-extend: if we have an 8-bit value in 32-bit, then
// there are 24 sign bits, and doing a sign-extend to 16 will only affect
// 16 of those 24, and the effect is to leave them as they are.
if (Bits::getMaxBits(ext, this) < bits || isSignExted(ext, bits)) {

@aheejin aheejin Nov 1, 2026 •

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.

Nit: To match the cases in visitBinary, to me it would be easier to split these to two cases

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.

Done.

Comment on lines +1105 to +1112
// Sign-extending the low 32 bits of a zero-extended i32 is a signed i32
// extension.
Unary* inner;
if (matches(curr,
unary(ExtendS32Int64, unary(&inner, ExtendUInt32, any())))) {
inner->op = ExtendSInt32;
return replaceCurrent(inner);
}

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.

Can we add this case to visitBinary too, so that we can do these?

(i64.extend_i32_u(x) << 32) >>_s 32 ⟶ i64.extend_i32_s(x)
(i64.extend_i32_u(x) << 32) >>_s 24 ⟶ i64.extend_i32_s(x) << 8  (when extraLeftShifts > 0)

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.

Good catch. Done.

Merge OptimizeInstructions::getSignExtBits into Properties::getSignExtBits using an optional LocalInfoProvider (following Bits::getMaxBits), and rename the sign/zero-extension bit width helpers from get*ExtBits to get*ExtKeptBits to clarify that they return the number of low bits kept before extension rather than the number of extended sign/zero bits.
# Conflicts:
#	src/passes/OptimizeInstructions.cpp
Merge OptimizeInstructions::getSignExtBits into Properties::getSignExtBits using an optional LocalInfoProvider (following Bits::getMaxBits), and rename the sign/zero-extension bit width helpers from get*ExtBits to get*ExtKeptBits to clarify that they return the number of low bits kept before extension rather than the number of extended sign/zero bits.
# Conflicts:
#	src/ir/properties.h
#	src/passes/OptimizeInstructions.cpp
…t-sign-ext

# Conflicts:
#	src/ir/bits.h
#	src/ir/properties.h
#	src/passes/OptimizeInstructions.cpp
#	src/passes/PickLoadSigns.cpp

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Google Antigravity Code Review

I have reviewed the changes in this Pull Request and left inline feedback.

Comment thread src/ir/bits.h
// If |curr| is known to be sign-extended, returns the number of low bits kept
// before sign-extending into the upper bits, or 0 otherwise.
Index getSignExtKeptBits(Expression* curr) const {
if (auto* unary = curr->dynCast<Unary>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH | Confidence: 5/5 | BUG]

getSignExtKeptBits does not verify that curr->type.isInteger() or that unary->value->type.isInteger(). In Binaryen IR, an inner expression can be replaced with an unreachable node (such as a trapping expression) before refinalization updates the parent's type, or curr itself may have Type::unreachable (e.g., (i64.extend_i32_s (unreachable))).

Existing helper functions in bits.h explicitly handle this invariant (see getSignExtValue lines 153 and 163-165: The operand may have become unreachable before refinalization updates curr->type). Without checking unary->value->type.isInteger(), getSignExtKeptBits recurses on the unreachable child, gets innerBits == 0, and falls back to returning 32 for an unreachable node. This causes hasSignExtKeptBits and callers in optimization passes to incorrectly treat unreachable expressions as valid 32-bit integer sign-extensions.

Consider guarding the check:

Suggested change
if (auto* unary = curr->dynCast<Unary>();
if (auto* unary = curr->dynCast<Unary>();
unary && unary->op == ExtendSInt32 &&
curr->type.isInteger() && unary->value->type.isInteger()) {
auto innerBits = getSignExtKeptBits(unary->value);
return innerBits > 0 ? innerBits : 32;
}

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.

Fixed with test.

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.

2 participants