Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: simplify simple conditionals for brillig #7205

Open
wants to merge 33 commits into
base: master
Choose a base branch
from

Conversation

guipublic
Copy link
Contributor

@guipublic guipublic commented Jan 28, 2025

Description

Problem*

Resolves #6394

Summary*

Add a pass which simplify simple if statements in brillig functions

Additional Context

The PR is working fine now, the memory consumption alert does not seem to be up-to-date.
The report from the last commit is correct: https://github.com/noir-lang/noir/actions/runs/13119725684/artifacts/2528887358

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[bot]

This comment was marked as duplicate.

github-actions[bot]

This comment was marked as off-topic.

Copy link
Contributor

github-actions bot commented Jan 31, 2025

Changes to number of Brillig opcodes executed

Generated at commit: 23eeb5324966486e69744999b97b00383b54c515, compared to commit: 87196e9419f9c12bc7739024e2f649dcbd3e7340

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
brillig_conditional_inliner_max +4 ❌ +14.81%
brillig_conditional_inliner_zero +4 ❌ +14.81%
binary_operator_overloading_inliner_max +26 ❌ +13.98%
brillig_block_parameter_liveness_inliner_max -1,983 ✅ -68.05%
brillig_block_parameter_liveness_inliner_min -1,983 ✅ -68.05%
brillig_block_parameter_liveness_inliner_zero -1,983 ✅ -68.05%

Full diff report 👇
Program Brillig opcodes (+/-) %
brillig_conditional_inliner_max 31 (+4) +14.81%
brillig_conditional_inliner_zero 31 (+4) +14.81%
binary_operator_overloading_inliner_max 212 (+26) +13.98%
brillig_conditional_inliner_min 45 (+3) +7.14%
trait_impl_base_type_inliner_zero 75 (+5) +7.14%
trait_impl_base_type_inliner_min 275 (+12) +4.56%
u128_inliner_max 25,191 (+908) +3.74%
slice_regex_inliner_max 3,361 (+108) +3.32%
slice_regex_inliner_zero 4,167 (+108) +2.66%
u128_inliner_zero 41,881 (+908) +2.22%
inline_decompose_hint_brillig_call_inliner_min 280 (+6) +2.19%
embedded_curve_ops_inliner_min 680 (+14) +2.10%
slices_inliner_max 3,342 (+66) +2.01%
embedded_curve_ops_inliner_zero 416 (+8) +1.96%
u128_inliner_min 49,421 (+908) +1.87%
slice_regex_inliner_min 8,743 (+106) +1.23%
binary_operator_overloading_inliner_zero 232 (+2) +0.87%
binary_operator_overloading_inliner_min 404 (+2) +0.50%
derive_inliner_min 617 (+2) +0.33%
derive_inliner_zero 332 (+1) +0.30%
array_sort_inliner_min 680 (+2) +0.29%
sha256_var_witness_const_regression_inliner_zero 7,007 (+6) +0.09%
sha256_inliner_zero 11,439 (+9) +0.08%
6_inliner_zero 4,464 (+3) +0.07%
conditional_regression_short_circuit_inliner_zero 4,544 (+3) +0.07%
sha256_var_witness_const_regression_inliner_max 6,452 (+3) +0.05%
sha256_inliner_max 13,685 (+3) +0.02%
sha256_var_size_regression_inliner_max 15,725 (-1) -0.01%
array_dynamic_blackbox_input_inliner_min 22,394 (-4) -0.02%
array_dynamic_blackbox_input_inliner_zero 21,307 (-4) -0.02%
array_dynamic_blackbox_input_inliner_max 17,525 (-4) -0.02%
poseidon_bn254_hash_inliner_zero 182,200 (-285) -0.16%
poseidon_bn254_hash_width_3_inliner_zero 182,200 (-285) -0.16%
poseidon_bn254_hash_inliner_max 151,454 (-285) -0.19%
poseidon_bn254_hash_width_3_inliner_max 151,454 (-285) -0.19%
6_array_inliner_min 2,553 (-8) -0.31%
6_array_inliner_zero 2,483 (-8) -0.32%
6_array_inliner_max 1,529 (-8) -0.52%
main_bool_arg_inliner_max 68 (-2) -2.86%
main_bool_arg_inliner_min 68 (-2) -2.86%
main_bool_arg_inliner_zero 68 (-2) -2.86%
dont_deduplicate_call_inliner_max 21 (-2) -8.70%
dont_deduplicate_call_inliner_min 21 (-2) -8.70%
dont_deduplicate_call_inliner_zero 21 (-2) -8.70%
brillig_block_parameter_liveness_inliner_max 931 (-1,983) -68.05%
brillig_block_parameter_liveness_inliner_min 931 (-1,983) -68.05%
brillig_block_parameter_liveness_inliner_zero 931 (-1,983) -68.05%

Copy link
Contributor

github-actions bot commented Jan 31, 2025

Changes to Brillig bytecode sizes

Generated at commit: 23eeb5324966486e69744999b97b00383b54c515, compared to commit: 87196e9419f9c12bc7739024e2f649dcbd3e7340

🧾 Summary (10% most significant diffs)

Program Brillig opcodes (+/-) %
regression_5435_inliner_max -9 ✅ -21.95%
regression_5435_inliner_min -9 ✅ -21.95%
regression_5435_inliner_zero -9 ✅ -21.95%
brillig_block_parameter_liveness_inliner_max -3,996 ✅ -81.57%
brillig_block_parameter_liveness_inliner_min -3,996 ✅ -81.57%
brillig_block_parameter_liveness_inliner_zero -3,996 ✅ -81.57%

Full diff report 👇
Program Brillig opcodes (+/-) %
slice_regex_inliner_zero 1,730 (+26) +1.53%
slices_inliner_max 2,160 (+28) +1.31%
slice_regex_inliner_min 2,151 (+24) +1.13%
slice_regex_inliner_max 2,345 (+26) +1.12%
regression_5045_inliner_min 226 (+2) +0.89%
inline_decompose_hint_brillig_call_inliner_min 245 (+2) +0.82%
embedded_curve_ops_inliner_min 517 (+2) +0.39%
u128_inliner_min 2,283 (-2) -0.09%
u128_inliner_zero 1,978 (-2) -0.10%
poseidon_bn254_hash_inliner_max 5,267 (-6) -0.11%
poseidon_bn254_hash_width_3_inliner_max 5,267 (-6) -0.11%
poseidon_bn254_hash_width_3_inliner_zero 4,696 (-6) -0.13%
poseidon_bn254_hash_inliner_zero 4,696 (-6) -0.13%
embedded_curve_ops_inliner_zero 360 (-1) -0.28%
sha256_var_size_regression_inliner_max 1,719 (-5) -0.29%
u128_inliner_max 2,686 (-10) -0.37%
trait_impl_base_type_inliner_min 249 (-1) -0.40%
array_dynamic_blackbox_input_inliner_min 1,278 (-7) -0.54%
binary_operator_overloading_inliner_min 356 (-2) -0.56%
array_sort_inliner_min 353 (-2) -0.56%
derive_inliner_zero 331 (-2) -0.60%
derive_inliner_min 589 (-4) -0.67%
array_dynamic_blackbox_input_inliner_zero 1,025 (-7) -0.68%
array_sort_inliner_max 290 (-2) -0.68%
array_sort_inliner_zero 290 (-2) -0.68%
array_dynamic_blackbox_input_inliner_max 839 (-7) -0.83%
binary_operator_overloading_inliner_zero 226 (-2) -0.88%
trait_impl_base_type_inliner_zero 65 (-1) -1.52%
brillig_conditional_inliner_min 45 (-1) -2.17%
6_array_inliner_min 445 (-10) -2.20%
6_array_inliner_zero 435 (-10) -2.25%
6_array_inliner_max 376 (-10) -2.59%
binary_operator_overloading_inliner_max 383 (-15) -3.77%
main_bool_arg_inliner_max 65 (-3) -4.41%
main_bool_arg_inliner_min 65 (-3) -4.41%
main_bool_arg_inliner_zero 65 (-3) -4.41%
dont_deduplicate_call_inliner_max 23 (-3) -11.54%
dont_deduplicate_call_inliner_min 23 (-3) -11.54%
dont_deduplicate_call_inliner_zero 23 (-3) -11.54%
regression_5435_inliner_max 32 (-9) -21.95%
regression_5435_inliner_min 32 (-9) -21.95%
regression_5435_inliner_zero 32 (-9) -21.95%
brillig_block_parameter_liveness_inliner_max 903 (-3,996) -81.57%
brillig_block_parameter_liveness_inliner_min 903 (-3,996) -81.57%
brillig_block_parameter_liveness_inliner_zero 903 (-3,996) -81.57%

@guipublic guipublic marked this pull request as ready for review January 31, 2025 17:11
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Execution Time'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 06d26db Previous: a9e9850 Ratio
global_var_regression_entry_points 0.009 s 0.007 s 1.29

This comment was automatically generated by workflow using github-action-benchmark.

CC: @TomAFrench

@guipublic guipublic requested a review from a team February 4, 2025 10:15
Copy link
Contributor

@vezenovm vezenovm left a comment

Choose a reason for hiding this comment

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

Did an initial scan. Do you know why we are regressing in brillig_conditional?

if mapping.contains_key(k) {
unreachable!("cannot merge key");
}
if mapping.contains_key(v) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We want to check contains_key for the value here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, if the two mapping both map to the same value, I think that's fine.
However, if the value of one is the key of the other, then one mapping is somehow overwriting what the other mapping is doing, and I disallow this. Especially because mappings are constructed in reverse order, so a mapping overwriting the 'next one' is probably bad.

BinaryOp::Lt => 5,
BinaryOp::And
| BinaryOp::Or
| BinaryOp::Xor => 1, //todo
Copy link
Contributor

Choose a reason for hiding this comment

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

This TODO doesn't have a description and there are a couple TODOs in this function. Could you make issues to handle them if we are not going to in this PR?

Copy link
Contributor Author

@guipublic guipublic Feb 5, 2025

Choose a reason for hiding this comment

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

Happy to address these in this PR. The point is to provide realistic brillig cost for each opcode. The idea is to improve execution speed so the cost should be the runtime cost in whatever unit as along as it is the same for all opcodes. My numbers can certainly be improved.
I added a warning instead of a todo at the beginning of the function to indicate that the numbers are estimates and can be improved.

compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
compiler/noirc_evaluator/src/ssa/opt/basic_conditional.rs Outdated Show resolved Hide resolved
@TomAFrench
Copy link
Member

Do you know why we are regressing in brillig_conditional?

Agreed that it would be good to have more clarity on this. We've got some serious benefits when conditionally mutating large structs but there's quite a few regressions.

@guipublic
Copy link
Contributor Author

Do you know why we are regressing in brillig_conditional?

Agreed that it would be good to have more clarity on this. We've got some serious benefits when conditionally mutating large structs but there's quite a few regressions.

I am not sure where these numbers come exactly, doing nargo info on brillig_conditional, I get 31 opcodes with the simplification VS 32 without: brillig_conditional | conditional | N/A | N/A | 32 |
WIth binary_operator_overloading, I get the same number, with or without.

@TomAFrench
Copy link
Member

The regression is in the number of opcodes executed (i.e. the execution trace length). This can be shown by nargo info --profile-execution.

@guipublic
Copy link
Contributor Author

The regression is in the number of opcodes executed (i.e. the execution trace length). This can be shown by nargo info --profile-execution.

Oh right, then this is expected, since the optimisation execute both branches, it is expected to get more opcode executed, it's just that we avoid jumping around. For a number of cases, the numbers are better because of handling of arrays

| Instruction::EnableSideEffectsIf { .. }
| Instruction::IncrementRc { .. }
| Instruction::DecrementRc { .. }
| Instruction::MakeArray { .. }
Copy link
Contributor

Choose a reason for hiding this comment

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

We want to get a more accurate instruction count here. MakeArray is either several store and add instructions or a runtime loop for large non-nested arrays

.

Copy link
Contributor Author

@guipublic guipublic Feb 6, 2025

Choose a reason for hiding this comment

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

I put 20 as the cost then, since it seems to be the maximum cost (10 stores and ~10 index increments)

github-actions[bot]

This comment was marked as duplicate.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Compilation Time'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: 88faf2c Previous: 819a53a Ratio
private-kernel-inner 2.404 s 1.958 s 1.23

This comment was automatically generated by workflow using github-action-benchmark.

CC: @TomAFrench

Comment on lines +133 to +143
source_a: MemoryAddress,
source_b: MemoryAddress,
condition: MemoryAddress,
) {
debug_println!(
self.enable_debug_trace,
" {} = CONDITIONAL MOV {} then {}, else {}",
destination,
condition,
source_a,
source_b
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
source_a: MemoryAddress,
source_b: MemoryAddress,
condition: MemoryAddress,
) {
debug_println!(
self.enable_debug_trace,
" {} = CONDITIONAL MOV {} then {}, else {}",
destination,
condition,
source_a,
source_b
source_then: MemoryAddress,
source_else: MemoryAddress,
condition: MemoryAddress,
) {
debug_println!(
self.enable_debug_trace,
" {} = CONDITIONAL MOV {} then {}, else {}",
destination,
condition,
source_then,
source_else

I'd consider putting an if in there as well. Perhaps MOV_IF like JUMP_IF?

@@ -213,6 +216,10 @@ struct Context<'f> {
/// us from unnecessarily inserting extra instructions, and keeps ids unique which
/// helps simplifications.
not_instructions: HashMap<ValueId, ValueId>,

/// Flag to tell the context to not issue 'enable_side_effect' instructions during flattening.
/// This should set to true only by flatten_single(), when none instruction is known to fail.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// This should set to true only by flatten_single(), when none instruction is known to fail.
/// This should be set to true only by `flatten_single()`, when no instruction is known to fail.


impl Ssa {
#[tracing::instrument(level = "trace", skip(self))]
pub(crate) fn flatten_basic_conditionals(mut self) -> Ssa {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be helpful to add a doctoring with a basic example of what this pass achieves. For example when we walk someone through the passes, it's a quick way to explain what we mean without having to look up unit tests. Maybe examples are not realistic in more complex cases, and having them here but not there is just irregular, but at sentence or two of what it's looking for and what tradeoffs it's considering would be great IMO.

Comment on lines +45 to +46
// Returns the blocks of the simple conditional sub-graph whose input block is the entry.
// Returns None if the input block is not the entry block of a simple conditional.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Returns the blocks of the simple conditional sub-graph whose input block is the entry.
// Returns None if the input block is not the entry block of a simple conditional.
/// Returns the blocks of the simple conditional sub-graph whose input block is the entry.
/// Returns None if the input block is not the entry block of a simple conditional.

}
}
} else if right_successors_len == 1 && next_right == Some(left) {
// Right branch joins the right branch, it is a if/else statement with no then
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Right branch joins the right branch, it is a if/else statement with no then
// Right branch joins the left branch, it is a if/else statement with no then

let (block_then, block_else) = if left == *then_destination {
(Some(left), None)
} else if left == *else_destination {
(None, Some(left))
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a bit confusing that the comment above says "it is a if/then statement with no else" but then here we can have an else and no then. (I'm not sure what that means tbh).

//0. initialize the context for flattening a 'single conditional'
let mut queue = vec![];
self.target_block = conditional.block_entry;
self.no_predicate = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see this being reset anywhere. Ostensibly the Context could outlive the call to flatten_single_conditional, in which case this should go back to where it was before the call, no?

// Manually set the terminator of the entry block to the one of the exit block
let terminator =
self.inserter.function.dfg[conditional.block_exit].terminator().unwrap().clone();
let mut next_blocks = VecDeque::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be blind, but where is this consumed?

Comment on lines +297 to +298
if !queue.contains(&incoming_block) {
queue.push(incoming_block);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see see these values being popped: the 3rd section handles the exit block, and there is no looping 👀

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.

Flatten simple if-else statements in brillig to avoid unnecessary jump statements
4 participants