Skip to content

Commit

Permalink
feat: replace bitwise ANDs used for truncation with `Instruction::Tru…
Browse files Browse the repository at this point in the history
…ncate` (#4327)

…

# Description

## Problem\*

Resolves <!-- Link to GitHub Issue -->

## Summary\*

It's common for us to have new Noir developers to perform bitwise
operations in order to truncate a value before they cast to a smaller
type, leading to programs such as the below:

```rust
fn main(num: u72) -> pub [u8; 8] {
    let mut out: [u8; 8] = [0; 8];
    for i in 0..8 {
        out[i] = (num >> (56 - (i * 8)) as u72 & 0xff) as u8;
    }

    out
}
```

This is expensive and unnecessary as we perform truncation on casts
anyway. This PR then adds an optimisation which converts any bitwise AND
of the form `lhs & 0xff` into an equivalent truncation instruction.

```
Before
acir fn main f0 {
  b0(v0: u72):
    v277 = div v0, u72 2⁵⁶
    v278 = and v277, u72 255
    v279 = truncate v278 to 8 bits, max_bit_size: 72
    v280 = cast v279 as u8
    v281 = div v0, u72 2⁴⁸
    v282 = and v281, u72 255
    v283 = truncate v282 to 8 bits, max_bit_size: 72
    v284 = cast v283 as u8
    v285 = div v0, u72 2⁴⁰
    v286 = and v285, u72 255
    v287 = truncate v286 to 8 bits, max_bit_size: 72
    v288 = cast v287 as u8
    v289 = div v0, u72 2³²
    v290 = and v289, u72 255
    v291 = truncate v290 to 8 bits, max_bit_size: 72
    v292 = cast v291 as u8
    v293 = div v0, u72 2²⁴
    v294 = and v293, u72 255
    v295 = truncate v294 to 8 bits, max_bit_size: 72
    v296 = cast v295 as u8
    v297 = div v0, u72 2¹⁶
    v298 = and v297, u72 255
    v299 = truncate v298 to 8 bits, max_bit_size: 72
    v300 = cast v299 as u8
    v301 = div v0, u72 2⁸
    v302 = and v301, u72 255
    v303 = truncate v302 to 8 bits, max_bit_size: 72
    v304 = cast v303 as u8
    v305 = and v0, u72 255
    v306 = truncate v305 to 8 bits, max_bit_size: 72
    v307 = cast v306 as u8
    return [v280, v284, v288, v292, v296, v300, v304, v307]
}

+--------------+----------------------+
| ACIR Opcodes | Backend Circuit Size |
+--------------+----------------------+
| 70           | 5617                 |
+--------------+----------------------+
After
acir fn main f0 {
  b0(v0: u72):
    v242 = div v0, u72 2⁵⁶
    v243 = truncate v242 to 8 bits, max_bit_size: 72
    v244 = cast v243 as u8
    v245 = div v0, u72 2⁴⁸
    v246 = truncate v245 to 8 bits, max_bit_size: 72
    v247 = cast v246 as u8
    v248 = div v0, u72 2⁴⁰
    v249 = truncate v248 to 8 bits, max_bit_size: 72
    v250 = cast v249 as u8
    v251 = div v0, u72 2³²
    v252 = truncate v251 to 8 bits, max_bit_size: 72
    v253 = cast v252 as u8
    v254 = div v0, u72 2²⁴
    v255 = truncate v254 to 8 bits, max_bit_size: 72
    v256 = cast v255 as u8
    v257 = div v0, u72 2¹⁶
    v258 = truncate v257 to 8 bits, max_bit_size: 72
    v259 = cast v258 as u8
    v260 = div v0, u72 2⁸
    v261 = truncate v260 to 8 bits, max_bit_size: 72
    v262 = cast v261 as u8
    v263 = truncate v0 to 8 bits, max_bit_size: 72
    v264 = cast v263 as u8
    return [v244, v247, v250, v253, v256, v259, v262, v264]
}

+--------------+----------------------+
| ACIR Opcodes | Backend Circuit Size |
+--------------+----------------------+
| 61           | 3801                 |
+--------------+----------------------+


```
The ANDs then completely disappear as the truncation instructions they
get converted into are merged into the existing truncations.

## Additional Context



## Documentation\*

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[Exceptional Case]** Documentation to be submitted in a separate
PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
TomAFrench committed Feb 12, 2024
1 parent 514a7b3 commit eb67ff6
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 0 deletions.
26 changes: 26 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,32 @@ impl Binary {
let instruction = Instruction::binary(BinaryOp::Mul, self.lhs, self.rhs);
return SimplifyResult::SimplifiedToInstruction(instruction);
}
if operand_type.is_unsigned() {
// It's common in other programming languages to truncate values to a certain bit size using
// a bitwise AND with a bit mask. However this operation is quite inefficient inside a snark.
//
// We then replace this bitwise operation with an equivalent truncation instruction.
match (lhs, rhs) {
(Some(bitmask), None) | (None, Some(bitmask)) => {
// This substitution requires the bitmask to retain all of the lower bits.
// The bitmask must then be one less than a power of 2.
let bitmask_plus_one = bitmask.to_u128() + 1;
if bitmask_plus_one.is_power_of_two() {
let value = if lhs.is_some() { self.rhs } else { self.lhs };
let num_bits = bitmask_plus_one.ilog2();
return SimplifyResult::SimplifiedToInstruction(
Instruction::Truncate {
value,
bit_size: num_bits,
max_bit_size: operand_type.bit_size(),
},
);
}
}

_ => (),
}
}
}
BinaryOp::Or => {
if lhs_is_zero {
Expand Down
2 changes: 2 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"bincode",
"bindgen",
"bitand",
"bitmask",
"blackbox",
"boilerplate",
"boilerplates",
Expand Down Expand Up @@ -83,6 +84,7 @@
"higher-kinded",
"Hindley-Milner",
"idents",
"ilog",
"impls",
"indexmap",
"injective",
Expand Down

0 comments on commit eb67ff6

Please sign in to comment.