Skip to content

[opt] Add masked-xor strength reduction for binary selects - #4881

Closed
mag-mga wants to merge 3 commits into
google:mainfrom
mag-mga:mag-mga/select-to-xor
Closed

mag-mga wants to merge 3 commits into
google:mainfrom
mag-mga:mag-mga/select-to-xor

Conversation

@mag-mga

@mag-mga mag-mga commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A binary select of the form sel(p, [x, xor(x, c)]) is rewritten into xor(x, sign_ext(p) & c) when c is fully known, with the selector complemented when the xor arm is the on-false case. The rewrite also applies when an earlier pass (e.g. concat_simp) already split the xor limb into per-slice pieces, as long as the two arms provably differ by a fully known constant (TryFoldXorDelta).

Runs at optimization levels where split-based rewrites are enabled (SplitsEnabled); it is the strength-reduced form of the per-step select in an unrolled CRC/shift loop.

A binary select of the form sel(p, [x, xor(x, c)]) is rewritten into
xor(x, sign_ext(p) & c) when c is fully known, with the selector
complemented when the xor arm is the on-false case. The rewrite also
applies when an earlier pass (e.g. concat_simp) already split the xor
limb into per-slice pieces, as long as the two arms provably differ by a
fully known constant (TryFoldXorDelta).

Runs at optimization levels where split-based rewrites are enabled
(SplitsEnabled); it is the strength-reduced form of the per-step select
in an unrolled CRC/shift loop.
@mag-mga

mag-mga commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Hi, @ericastor, please, take a look at this PR. My optimization seems to be close to your MaybeConvertSelectToMask inside select_simp.

@erinzmoore
erinzmoore requested a review from ericastor August 31, 2026 16:28
@mag-mga

mag-mga commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

@ericastor , ping

@allight

allight commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

@mag-mga please address comments before asking for further review.

Edit: sorry bad github ui. Comments sent

@allight allight left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Oh whoops sorry hadn't actually sent these

m::SignExt(m::Eq(m::Param("p"), m::Literal(0b00)))));
}

TEST_P(SelectSimplificationPassTest, SelectWithConstantXorCaseCrcShift) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use FunctionBuilder to create tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rewrote to FunctionBuilder

// Flattens `node` into its non-concat pieces, most-significant first: the same
// order in which `concat` lists its operands and `bits_ops::Concat`
// reassembles them.
void FlattenConcats(Node* node, std::vector<Node*>& pieces) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Because both concat_simp and select_simp are in the fixedpoint_simp compound pass this seems like it could cause the optimization to never finish if it triggers (because concat_simp will continuously undo the change).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IIUC, this doesn't actually flatten the concats - it reads through them so the operation can see what it's working on. It still only applies if this optimization actually takes an action, which I don't think is something concat_simp can undo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Make it as independent select_to_bitwise_ops pass in the second commit

Comment thread xls/passes/select_simplification_pass.h Outdated
// potentially more efficient components, and exposing further optimization
// opportunities.
//
// 16. **Converting Binary `Select` to Masked XOR**:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems very unlike the rest of the transforms in this pass in that it really depends on the values in the select cases.

This transform should either be in strength_red or more likely in a totally separate pass itself IMO.

(There's also some questions we've been having internally about changing the way that we organize optimization phases which would probably want this to be late in compilation, but that's only peripherally related to this.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It really is closely related to transform (14) above. I would agree that maybe both of these should be split out into another pass, though.

@ericastor

Copy link
Copy Markdown
Contributor

Apologies - I was at a conference all last week, and then today was a US holiday. I'll pick this up for a proper review in the morning.

@ericastor

Copy link
Copy Markdown
Contributor

The code seems solid, and this is an interesting proposal - though I think I agree with @allight that it's starting to look like it'd be worth splitting out the "select -> bitwise operations" transforms into their own pass. Are you able to share a concrete example of something where this makes a difference to optimized IR quality or to PPA?

@mag-mga
mag-mga force-pushed the mag-mga/select-to-xor branch from 7d5b733 to 02f1493 Compare September 10, 2026 08:40
@mag-mga

mag-mga commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

The code seems solid, and this is an interesting proposal - though I think I agree with @allight that it's starting to look like it'd be worth splitting out the "select -> bitwise operations" transforms into their own pass. Are you able to share a concrete example of something where this makes a difference to optimized IR quality or to PPA?

We had a hot CRC function inside many loops (something like that below)

top fn __crc16_attach_single_proc__crc16_step(crc: bits[16] id=1, bit: bits[1] id=2) -> bits[16] {
  bit_slice.14: bits[15] = bit_slice(crc, start=0, width=15, id=14, pos=[(0,9,16)])
  literal.13: bits[1] = literal(value=0, id=13, pos=[(0,9,16)])
  bit_slice.4: bits[1] = bit_slice(crc, start=15, width=1, id=4, pos=[(0,8,20)])
  shifted: bits[16] = concat(bit_slice.14, literal.13, id=15, pos=[(0,9,16)])
  CRC16_POLY: bits[16] = literal(value=4129, id=3, pos=[(0,5,19)])
  feedback: bits[1] = xor(bit_slice.4, bit, id=5, pos=[(0,8,20)])
  xor.8: bits[16] = xor(shifted, CRC16_POLY, id=8, pos=[(0,12,4)])
  ret sel.9: bits[16] = sel(feedback, cases=[shifted, xor.8], id=9, pos=[(0,11,2)])
}

Previously, it generates

module __crc16_attach_single_proc__crc16_step(
  input wire [15:0] crc,
  input wire bit,
  output wire [15:0] out
);
  wire feedback;
  wire [15:0] shifted;
  assign feedback = crc[15] ^ bit;
  assign shifted = {crc[14:0], 1'h0};
  assign out = feedback ? {crc[14:0] ^ 15'h0810, 1'h1} : shifted;
endmodule

Now it leads to

module __crc16_attach_single_proc__crc16_step(
  input wire [15:0] crc,
  input wire bit,
  output wire [15:0] out
);
  wire feedback;
  wire [15:0] shifted;
  assign feedback = crc[15] ^ bit;
  assign shifted = {crc[14:0], 1'h0};
  assign out = shifted ^ {16{feedback}} & 16'h1021;
endmodule

For our target it is especially better for FPGA LUT and sometimes also better in synthesis (although some synthesis tools can finally synthesize both functions in the same way).

I'm not confident I have enough permission to provide PPA :(

@mag-mga
mag-mga requested a review from allight September 10, 2026 08:56
@ericastor

Copy link
Copy Markdown
Contributor

The code seems solid, and this is an interesting proposal - though I think I agree with @allight that it's starting to look like it'd be worth splitting out the "select -> bitwise operations" transforms into their own pass. Are you able to share a concrete example of something where this makes a difference to optimized IR quality or to PPA?

We had a hot CRC function inside many loops (something like that below)

top fn __crc16_attach_single_proc__crc16_step(crc: bits[16] id=1, bit: bits[1] id=2) -> bits[16] {
  bit_slice.14: bits[15] = bit_slice(crc, start=0, width=15, id=14, pos=[(0,9,16)])
  literal.13: bits[1] = literal(value=0, id=13, pos=[(0,9,16)])
  bit_slice.4: bits[1] = bit_slice(crc, start=15, width=1, id=4, pos=[(0,8,20)])
  shifted: bits[16] = concat(bit_slice.14, literal.13, id=15, pos=[(0,9,16)])
  CRC16_POLY: bits[16] = literal(value=4129, id=3, pos=[(0,5,19)])
  feedback: bits[1] = xor(bit_slice.4, bit, id=5, pos=[(0,8,20)])
  xor.8: bits[16] = xor(shifted, CRC16_POLY, id=8, pos=[(0,12,4)])
  ret sel.9: bits[16] = sel(feedback, cases=[shifted, xor.8], id=9, pos=[(0,11,2)])
}

...

For our target it is especially better for FPGA LUT and sometimes also better in synthesis (although some synthesis tools can finally synthesize both functions in the same way).

I'm not confident I have enough permission to provide PPA :(

Oh, no worries there! I meant the motivating example, rather than PPA specifics, so I appreciate your sharing it. I'll review this again, and thanks for reworking it as requested.

@ericastor ericastor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is great, thank you!

# This is mostly to clean up extraneous bit-slice/concats etc.
"fixedpoint_simp",
"dce",
# Convert binary selects whose arms differ by a fully-known xor term into

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As I mentioned, I'm not actually sure this is true - so we'll check it, and decide where it should live overall. Thanks!

// Returns the (on_false, on_true) arm pair of a single-bit binary mux: either
// two explicit cases, or one explicit case plus a default value. Returns
// std::nullopt for any other select shape.
std::optional<std::pair<Node*, Node*>> GetBinaryMuxArms(Select* sel) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As a followup, we might want to switch this to use our GenericSelect framework! But for now, this is sensible.

@ericastor

ericastor commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Oh darn, our CI checks are aborting on the lack of a newline at the end of your new files. Are you able to fix that?

I might just do it on import, though.

@mag-mga

mag-mga commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Oh darn, our CI checks are aborting on the lack of a newline at the end of your new files. Are you able to fix that?

I might just do it on import, though.

@ericastor ,
I've added newlines as a separate commit

copybara-service Bot pushed a commit that referenced this pull request Sep 11, 2026
@mag-mga

mag-mga commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

I think we should close this MR because it is already merged to main branch

@mag-mga mag-mga closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants