Skip to content

feat: simplify jmpifs by reversing branches if condition is negated#5891

Merged
TomAFrench merged 19 commits into
masterfrom
tf/swap-jmpif-branches-to-avoid-negation
Nov 26, 2024
Merged

feat: simplify jmpifs by reversing branches if condition is negated#5891
TomAFrench merged 19 commits into
masterfrom
tf/swap-jmpif-branches-to-avoid-negation

Conversation

@TomAFrench

Copy link
Copy Markdown
Member

Description

Problem*

Resolves

Summary*

Currently if an if-else block has a negated condition we generate an extra instruction to calculate the condition rather than just swapping the "if" and "else" branches. I've added a check for this in the simplify_cfg pass.

Additional Context

Documentation*

Check one:

  • No documentation needed.
  • Documentation included in this PR.
  • [For Experimental Features] Documentation to be submitted in a separate PR.

PR Checklist*

  • I have tested the changes locally.
  • I have formatted the changes with Prettier and/or cargo fmt on default settings.

@github-actions

github-actions Bot commented Sep 3, 2024

Copy link
Copy Markdown
Contributor

Changes to Brillig bytecode sizes

Generated at commit: c8f26516d70bdfd25bb1a0c614fe8bf826a986ed, compared to commit: 7d7b9c977c65819cd8530fabec6caa68b34d879e

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
reference_counts -4 ✅ -1.16%
acir_inside_brillig_recursion -1 ✅ -1.22%
brillig_recursion -1 ✅ -1.30%
fold_fibonacci -1 ✅ -1.30%
references -4 ✅ -2.48%

Full diff report 👇
Program Brillig opcodes (+/-) %
debug_logs 5,007 (-1) -0.02%
regression_5252 4,525 (-1) -0.02%
poseidonsponge_x5_254 4,163 (-1) -0.02%
u128 2,756 (-1) -0.04%
nested_array_dynamic 1,984 (-1) -0.05%
uhashmap 13,186 (-10) -0.08%
nested_array_in_slice 1,097 (-1) -0.09%
brillig_cow_regression 2,135 (-2) -0.09%
sha2_byte 2,720 (-3) -0.11%
ecdsa_secp256k1 895 (-1) -0.11%
array_dynamic_nested_blackbox_input 864 (-1) -0.12%
slice_dynamic_index 2,520 (-3) -0.12%
sha256_brillig_performance_regression 1,630 (-2) -0.12%
eddsa 10,201 (-14) -0.14%
regression_4449 724 (-1) -0.14%
conditional_regression_short_circuit 1,186 (-2) -0.17%
conditional_1 1,175 (-2) -0.17%
6 1,113 (-2) -0.18%
sha256_regression 6,529 (-12) -0.18%
aes128_encrypt 512 (-1) -0.19%
array_dynamic_blackbox_input 1,019 (-2) -0.20%
ram_blowup_regression 951 (-2) -0.21%
sha256 2,207 (-5) -0.23%
sha256_var_size_regression 1,701 (-4) -0.23%
sha256_var_witness_const_regression 1,229 (-3) -0.24%
sha256_var_padding_regression 4,751 (-12) -0.25%
hashmap 19,815 (-57) -0.29%
array_sort 292 (-1) -0.34%
slices 1,734 (-8) -0.46%
bigint 1,981 (-10) -0.50%
slice_regex 2,149 (-14) -0.65%
reference_counts 340 (-4) -1.16%
acir_inside_brillig_recursion 81 (-1) -1.22%
brillig_recursion 76 (-1) -1.30%
fold_fibonacci 76 (-1) -1.30%
references 157 (-4) -2.48%

@TomAFrench

Copy link
Copy Markdown
Member Author

I'm really unsure on why exactly this is causing test failures. The ordering of the branches in a jmpif shouldn't cause any changes to behaviour afaik.

@TomAFrench

Copy link
Copy Markdown
Member Author

This branch changes the order of the jmpif as expected in the simplify_cfg pass however the issue comes up in flattening as we end up merging the values incorrectly.

Master

acir(inline) fn main f0 {
  b0(v0: Field):
    v1 = allocate
    v2 = allocate
    store Field 0 at v2
    v5 = eq v0, Field 10
    v6 = not v5
    enable_side_effects v6
    v7 = load v2
    store Field 2 at v2
    store v7 at v2
    enable_side_effects u1 1
    v10 = cast v6 as Field
    v11 = cast v5 as Field
    v12 = mul v10, Field 2 // NOT(v5) * 2
    v13 = mul v11, v7 // v5 * 0
    v14 = add v12, v13
    store v14 at v2
    v15 = load v2
    v16 = eq v15, Field 2
    constrain v15 == Field 2
    return 
}

This branch

acir(inline) fn main f0 {
  b0(v0: Field):
    v1 = allocate
    v2 = allocate
    store Field 0 at v2
    v5 = eq v0, Field 10
    v6 = not v5
    enable_side_effects v5
    v7 = load v2
    store Field 2 at v2
    v9 = not v5 
    store v7 at v2
    enable_side_effects u1 1
    v11 = cast v5 as Field
    v12 = cast v9 as Field
    v13 = mul v11, Field 2 // v5 * 2
    v14 = mul v12, v7 // NOT(v5) * 0
    v15 = add v13, v14
    store v15 at v2
    v16 = load v2
    v17 = eq v16, Field 2
    constrain v16 == Field 2
    return 
}

@TomAFrench TomAFrench marked this pull request as draft September 4, 2024 12:52
@vezenovm

vezenovm commented Sep 4, 2024

Copy link
Copy Markdown
Contributor

This branch changes the order of the jmpif as expected in the simplify_cfg pass however the issue comes up in flattening as we end up merging the values incorrectly.

If we do not see an simple or obvious way to have this optimization work w/ flattening we could make this optimization just work on Brillig for now.

@jfecher

jfecher commented Sep 4, 2024

Copy link
Copy Markdown
Contributor

I'd like if we investigated why flattening isn't adapting to the new changes. Could be related to the bug I've been looking at.

@TomAFrench

Copy link
Copy Markdown
Member Author

Agreed, we should at least understand the root cause of this first.

TomAFrench and others added 6 commits September 10, 2024 13:42
* master: (60 commits)
  fix: suggest trait attributes in LSP (#5972)
  fix: Error when `quote` is used in runtime code (#5978)
  chore: document HashMap (#5984)
  fix: Restrict keccak256_injective test input to 8 bits (#5977)
  fix: Error when comptime functions are used in runtime code (#5976)
  chore: document BoundedVec (#5974)
  feat: add `Expr::as_let` (#5964)
  chore: remove 3 unused functions warnings in the stdlib (#5973)
  feat: let `nargo` and LSP work well in the stdlib (#5969)
  feat: show doc comments in LSP (#5968)
  feat: add a `panic` method to the stdlib (#5966)
  fix: LSP document symbol didn't work for primitive impls (#5970)
  fix(mem2reg): Handle aliases in function last store cleanup and additional alias unit test (#5967)
  fix: let `derive(Eq)` work for empty structs (#5965)
  feat: add `FunctionDefinition` methods `is_unconstrained` and `set_unconstrained` (#5962)
  feat: LSP autocompletion for attributes (#5963)
  feat: `Module::add_item` (#5947)
  feat: Add `StructDefinition::add_generic` (#5961)
  feat: Add `StructDefinition::name` (#5960)
  fix(mem2reg): Handle aliases better when setting a known value for a load (#5959)
  ...
@michaeljklein

michaeljklein commented Oct 22, 2024

Copy link
Copy Markdown
Contributor

I was able to narrow down one of the failing tests to the following:

This fails the final assertion:

// x == 5
fn main(x: Field) {
    let mut z = false;
    if x != 20 {
        z = true;
    }
    assert(x != 20);

    // fails here
    assert(z);
}

But this succeeds:

// x == 5
fn main(x: Field) {
    let mut z = false;
    if x != 20 {
        z = true;
    } else {
    }
    assert(x != 20);

    assert(z);
}

So there appears to be a bug around single-block if statements and a negated dynamic condition.

@michaeljklein

Copy link
Copy Markdown
Contributor

I was able to narrow the above case slightly to the following, which executes successfully on nargo nightly, but fails on this branch:

fn main(x: bool) {
    let mut z = false;
    if !x {
        z = true;
    }

    assert(!x);
    assert(z);
}
  • Adding an else {} still fails for this example
  • Using x in the if condition (with either x = true or x = false) succeeds

Dumping the SSA (skipping unused variables):

  • The simplification step still gives a correct SSA (one that would be expected to execute successfully)
// After Simplifying:
acir(inline) fn main f0 {
  // v0 = 0
  b0(v0: u1):
    v1 = allocate
    store u1 0 at v1
    // v1 = 0
    jmpif v0 then: b2, else: b1
  b2():
    // (skipped)
    store u1 0 at v1
    jmp b3()
  b3():
    constrain v0 == u1 0
    v6 = load v1
    // v6 = 1
    constrain v6 == u1 1
    return 
  b1():
    // branch taken
    store u1 1 at v1
    // v1 = 1
    jmp b3()
}

But flatting gives SSA that unconditionally fails (see final constrain):

// After Flattening:
acir(inline) fn main f0 {
  // v0 = 0
  b0(v0: u1):
    v1 = allocate
    store u1 0 at v1
    // v1 = 0
    enable_side_effects v0
    // no side effects
    v4 = load v1
    // v4 = 0
    store u1 1 at v1
    // not stored?
    v6 = not v0
    // v6 = 1
    store v4 at v1
    // not stored? either way, it's 0
    enable_side_effects v6
    store u1 0 at v1
    // v1 = 0
    enable_side_effects u1 1
    store v0 at v1
    // v1 = 0
    constrain v0 == u1 0
    v9 = load v1
    // v9 = 0
    constrain v9 == u1 1
    return 
}

@github-actions

github-actions Bot commented Nov 25, 2024

Copy link
Copy Markdown
Contributor

Changes to number of Brillig opcodes executed

Generated at commit: c8f26516d70bdfd25bb1a0c614fe8bf826a986ed, compared to commit: 7d7b9c977c65819cd8530fabec6caa68b34d879e

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
acir_inside_brillig_recursion -8 ✅ -3.04%
brillig_recursion -176 ✅ -3.89%
fold_fibonacci -176 ✅ -3.89%
references -13 ✅ -5.16%

Full diff report 👇
Program Brillig opcodes (+/-) %
slices 2,869 (+4) +0.14%
slice_regex 3,390 (+1) +0.03%
regression_5252 908,588 (-4) -0.00%
sha2_byte 46,688 (-2) -0.00%
array_dynamic_blackbox_input 18,177 (-2) -0.01%
sha256_var_size_regression 16,342 (-2) -0.01%
slice_dynamic_index 4,331 (-1) -0.02%
brillig_cow_regression 518,778 (-166) -0.03%
ram_blowup_regression 778,408 (-256) -0.03%
sha256_brillig_performance_regression 22,969 (-8) -0.03%
conditional_1 5,698 (-2) -0.04%
debug_logs 5,019 (-2) -0.04%
sha256_var_padding_regression 219,621 (-92) -0.04%
array_dynamic_nested_blackbox_input 4,512 (-2) -0.04%
sha256_regression 116,119 (-58) -0.05%
uhashmap 146,343 (-142) -0.10%
array_sort 561 (-2) -0.36%
eddsa 702,179 (-2,546) -0.36%
u128 24,961 (-178) -0.71%
aes128_encrypt 4,451 (-32) -0.71%
hashmap 52,939 (-978) -1.81%
reference_counts 309 (-8) -2.52%
acir_inside_brillig_recursion 255 (-8) -3.04%
brillig_recursion 4,353 (-176) -3.89%
fold_fibonacci 4,353 (-176) -3.89%
references 239 (-13) -5.16%

@TomAFrench

TomAFrench commented Nov 26, 2024

Copy link
Copy Markdown
Member Author

I've done some investigating on this and the error is due to the fact that we've moved the return terminator to the then branch rather than the else branch. This means that due to the fact that the line below gives [b2, b1, b2] which results in the work queue [b2, b1].

vec![self.branch_ends[if_entry], *else_destination, *then_destination]

We then end up processing the else block as if it were the then block so we apply the condition incorrectly.

@TomAFrench

Copy link
Copy Markdown
Member Author

The two options for solving this then are:

  • Prevent a return statement from existing within the else branch, that is we need to turn every if statement into and if-else with an empty else block. This avoids a return statement in the else block so it cannot be switched into being in the then block by this optimisation.
  • Prevent this optimisation from running in ACIR functions and just have it work on brillig (which can tolerate early returns).

Considering the lack of any change in circuit sizes, I'm inclined to go for the latter as it avoids making changes across the codebase (and relying on someone not undoing this change and accidentally causing a breakage).

@TomAFrench

Copy link
Copy Markdown
Member Author

Considering the lack of any change in circuit sizes

On second thoughts, this is due to the fact that at least one program has failed in each commit of this PR so we haven't measured this. Another solution would be to query the CFG to make sure that we don't merge in the else block before applying this optimization.

AztecBot pushed a commit to AztecProtocol/aztec-packages that referenced this pull request Nov 28, 2024
…ir#6635)

fix(ssa): don't deduplicate constraints in blocks that are not dominated (noir-lang/noir#6627)
chore: pin foundry version in CI (noir-lang/noir#6642)
feat(ssa): Deduplicate intrinsics with predicates (noir-lang/noir#6615)
chore: improve error message of `&T` (noir-lang/noir#6633)
fix: LSP code action wasn't triggering on beginning or end of identifier (noir-lang/noir#6616)
chore!: remove `ec` module from stdlib (noir-lang/noir#6612)
fix(LSP): use generic self type to narrow down methods to complete (noir-lang/noir#6617)
fix!: Disallow `#[export]` on associated methods (noir-lang/noir#6626)
chore: redo typo PR by donatik27 (noir-lang/noir#6575)
chore: redo typo PR by Dimitrolito (noir-lang/noir#6614)
feat: simplify `jmpif`s by reversing branches if condition is negated (noir-lang/noir#5891)
fix: Do not warn on unused functions marked with #[export] (noir-lang/noir#6625)
chore: Add panic for compiler error described in #6620 (noir-lang/noir#6621)
feat(perf): Track last loads per block in mem2reg and remove them if possible (noir-lang/noir#6088)
fix(ssa): Track all local allocations during flattening (noir-lang/noir#6619)
feat(comptime): Implement blackbox functions in comptime interpreter (noir-lang/noir#6551)
chore: derive PartialEq and Hash for FieldElement (noir-lang/noir#6610)
chore: ignore almost-empty directories in nargo_cli tests (noir-lang/noir#6611)
chore: remove temporary allocations from `num_bits` (noir-lang/noir#6600)
chore: Release Noir(1.0.0-beta.0) (noir-lang/noir#6562)
feat: Add `array_refcount` and `slice_refcount` builtins for debugging (noir-lang/noir#6584)
chore!: Require types of globals to be specified (noir-lang/noir#6592)
fix: don't report visibility errors when elaborating comptime value (noir-lang/noir#6498)
fix: preserve newlines between comments when formatting statements (noir-lang/noir#6601)
fix: parse a bit more SSA stuff (noir-lang/noir#6599)
chore!: remove eddsa from stdlib (noir-lang/noir#6591)
chore: Typo in oracles how to (noir-lang/noir#6598)
feat(ssa): Loop invariant code motion (noir-lang/noir#6563)
fix: remove `compiler_version` from new `Nargo.toml` (noir-lang/noir#6590)
feat: Avoid incrementing reference counts in some cases (noir-lang/noir#6568)
chore: fix typo in test name (noir-lang/noir#6589)
fix: consider prereleases to be compatible with pre-1.0.0 releases (noir-lang/noir#6580)
feat: try to inline brillig calls with all constant arguments  (noir-lang/noir#6548)
fix: correct type when simplifying `derive_pedersen_generators` (noir-lang/noir#6579)
feat: Sync from aztec-packages (noir-lang/noir#6576)
AztecBot pushed a commit to AztecProtocol/aztec-packages that referenced this pull request Nov 29, 2024
chore: refactor poseidon2 (noir-lang/noir#6655)
fix: correct types returned by constant EC operations simplified within SSA (noir-lang/noir#6652)
feat: Sync from aztec-packages (noir-lang/noir#6634)
fix: used signed division for signed modulo (noir-lang/noir#6635)
fix(ssa): don't deduplicate constraints in blocks that are not dominated (noir-lang/noir#6627)
chore: pin foundry version in CI (noir-lang/noir#6642)
feat(ssa): Deduplicate intrinsics with predicates (noir-lang/noir#6615)
chore: improve error message of `&T` (noir-lang/noir#6633)
fix: LSP code action wasn't triggering on beginning or end of identifier (noir-lang/noir#6616)
chore!: remove `ec` module from stdlib (noir-lang/noir#6612)
fix(LSP): use generic self type to narrow down methods to complete (noir-lang/noir#6617)
fix!: Disallow `#[export]` on associated methods (noir-lang/noir#6626)
chore: redo typo PR by donatik27 (noir-lang/noir#6575)
chore: redo typo PR by Dimitrolito (noir-lang/noir#6614)
feat: simplify `jmpif`s by reversing branches if condition is negated (noir-lang/noir#5891)
fix: Do not warn on unused functions marked with #[export] (noir-lang/noir#6625)
chore: Add panic for compiler error described in #6620 (noir-lang/noir#6621)
feat(perf): Track last loads per block in mem2reg and remove them if possible (noir-lang/noir#6088)
fix(ssa): Track all local allocations during flattening (noir-lang/noir#6619)
feat(comptime): Implement blackbox functions in comptime interpreter (noir-lang/noir#6551)
chore: derive PartialEq and Hash for FieldElement (noir-lang/noir#6610)
chore: ignore almost-empty directories in nargo_cli tests (noir-lang/noir#6611)
chore: remove temporary allocations from `num_bits` (noir-lang/noir#6600)
chore: Release Noir(1.0.0-beta.0) (noir-lang/noir#6562)
feat: Add `array_refcount` and `slice_refcount` builtins for debugging (noir-lang/noir#6584)
chore!: Require types of globals to be specified (noir-lang/noir#6592)
fix: don't report visibility errors when elaborating comptime value (noir-lang/noir#6498)
fix: preserve newlines between comments when formatting statements (noir-lang/noir#6601)
fix: parse a bit more SSA stuff (noir-lang/noir#6599)
chore!: remove eddsa from stdlib (noir-lang/noir#6591)
chore: Typo in oracles how to (noir-lang/noir#6598)
feat(ssa): Loop invariant code motion (noir-lang/noir#6563)
fix: remove `compiler_version` from new `Nargo.toml` (noir-lang/noir#6590)
feat: Avoid incrementing reference counts in some cases (noir-lang/noir#6568)
chore: fix typo in test name (noir-lang/noir#6589)
fix: consider prereleases to be compatible with pre-1.0.0 releases (noir-lang/noir#6580)
feat: try to inline brillig calls with all constant arguments  (noir-lang/noir#6548)
fix: correct type when simplifying `derive_pedersen_generators` (noir-lang/noir#6579)
feat: Sync from aztec-packages (noir-lang/noir#6576)
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.

4 participants