-
Notifications
You must be signed in to change notification settings - Fork 204
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: replace boolean AND
s with multiplication
#1954
Conversation
For instance
requires 2784 gates without this optimisation but afterwards requires just 11. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change looks good, but have you considered adding it during ssa-gen instead of acir gen so that further optimizations can potentially be triggered afterward?
I wonder if we can have an optimization when we convert something to bits to only convert N bits for a |
No I didn't but this seems like a good excuse to get more familiar with the internals of that. Seems to me like we want to insert a snippet like
before L162 in noir/crates/noirc_evaluator/src/ssa_refactor/ir/dfg.rs Lines 155 to 169 in 720b1be
Does this look sensible to you? |
I think it should probably go within the |
Ah, I looked at that but wasn't sure if I could return a non-constant value out. So I then just need to return something like |
Why does this require 2784 gates? |
Barretenberg is using a plookup version of AND/XOR afaik so I guess that has some constant lower bound on the number of gates it uses. |
Compilation error comes from this line. https://github.com/noir-lang/noir/blob/master/noir_stdlib/src/ec/swcurve.nr#L38-L39 Seems like we don't update some values correctly when we swap out the instruction. I need to dig into this more though. |
Minimal program for reproducing the panic: use dep::std::ec::tecurve::affine::Curve as AffineCurve;
use dep::std::ec::tecurve::affine::Point as Gaffine;
use dep::std::ec::tecurve::curvegroup::Curve;
use dep::std::ec::tecurve::curvegroup::Point as G;
use dep::std::ec::swcurve::affine::Point as SWGaffine;
use dep::std::ec::swcurve::curvegroup::Point as SWG;
use dep::std::ec::montcurve::affine::Point as MGaffine;
use dep::std::ec::montcurve::curvegroup::Point as MG;
fn main() {
// This test only makes sense if Field is the right prime field.
if 21888242871839275222246405745257275088548364400416034343698204186575808495617 == 0
{
// Define Baby Jubjub (ERC-2494) parameters in affine representation
let bjj_affine = AffineCurve::new(168700, 168696, Gaffine::new(995203441582195749578291179787384436505546430278305826713579947235728471134,5472060717959818805561601436314318772137091100104008585924551046643952123905));
// Test addition
let p1_affine = Gaffine::new(17777552123799933955779906779655732241715742912184938656739573121738514868268, 2626589144620713026669568689430873010625803728049924121243784502389097019475);
let p2_affine = Gaffine::new(16540640123574156134436876038791482806971768689494387082833631921987005038935, 20819045374670962167435360035096875258406992893633759881276124905556507972311);
let p3_affine = bjj_affine.add(p1_affine, p2_affine);
// Test CurveGroup equivalents
let bjj = bjj_affine.into_group(); // Baby Jubjub
let p1 = p1_affine.into_group();
let p2 = p2_affine.into_group();
let p3 = p3_affine.into_group();
// Test SWCurve equivalents of the above
// First the affine representation
let bjj_swcurve_affine = bjj_affine.into_swcurve();
let p1_swcurve_affine = bjj_affine.map_into_swcurve(p1_affine);
let p2_swcurve_affine = bjj_affine.map_into_swcurve(p2_affine);
let p3_swcurve_affine = bjj_affine.map_into_swcurve(p3_affine);
// Addition
assert(
p3_swcurve_affine.eq(
bjj_swcurve_affine.add(
p1_swcurve_affine,
p2_swcurve_affine
)
)
);
// Check that these points are on the curve
assert(
bjj_swcurve_affine.contains(bjj_swcurve_affine.gen) &
bjj_swcurve_affine.contains(p1_swcurve_affine) &
bjj_swcurve_affine.contains(p2_swcurve_affine) &
bjj_swcurve_affine.contains(p3_swcurve_affine)
);
// Then the CurveGroup representation
let bjj_swcurve = bjj.into_swcurve();
let p1_swcurve = bjj.map_into_swcurve(p1);
let p2_swcurve = bjj.map_into_swcurve(p2);
let p3_swcurve = bjj.map_into_swcurve(p3);
// Addition
assert(p3_swcurve.eq(bjj_swcurve.add(p1_swcurve,p2_swcurve)));
}
}
|
I'm getting a panic on this line due to one of the inputs to the multiplication instruction being a noir/crates/noirc_evaluator/src/ssa_refactor/acir_gen/mod.rs Lines 490 to 492 in 88a4f74
I'm not clear on how changing |
The plookup versions of different ops are going to have a constant lower bound around 3k gates. Do we know if it is always cheaper to do a multiply rather than an AND? Like if this optimization is used in a larger circuit that is significantly over the 3k lower bound? |
If I use the program in new and old SSA I get fn main(array: [u1; 1000]) -> pub u1 {
let mut result = 1;
for element in array {
result &= element;
}
result
}
We can see that we actually have this multiplication optimisation in the old SSA code which causes it to outperform. In case this was an artifact of only having boolean ANDs and we would get better scaling with the plookup AND when we have a mix of boolean and non-boolean types I repeated with fn main(array: [u1; 1000], byte_array: [u8; 1000]) -> pub u8 {
let mut result = 1;
for element in array {
result &= element;
};
let mut byte_result: u8 = 5;
for element in byte_array {
byte_result &= element;
};
(result as u8) * byte_result
} which gives the results
|
Awesome. Thank you for the clear comparison |
No, unfortunately. My thought was the same as yours, as long as the arguments of the instruction remained the same it shouldn't require extra handling. Let me know if you want me to try to look into this as well. |
* master: (53 commits) chore: Update `noir-source-resolver` to v1.1.3 (#1912) chore: Document `GeneratedAcir::more_than_eq_comparison` (#2085) chore: refresh ACIR test artifacts (#2091) feat: Add `deprecated` attribute (#2041) chore(ssa refactor): Implement `acir_gen` errors (#2071) chore: use witnesses from the generated acir in the ABI (#2095) fix: Fix methods not mutating fields (#2087) chore(nargo): Use Display impl for InputValue (#1990) feat: Make arrays and slices polymorphic over each other (#2070) feat: Remove an unnecessary witness in `mul_with_witness` (#2078) chore: document truncate (#2082) fix: avoid potential panic in `two_complement` (#2081) chore: Cleanup integration tests (#2074) chore: replace `Type::TypeVariable`, `Type::PolymorphicInteger`, and … (#2065) chore!: Require package names in `Nargo.toml` files (#2056) fix: Avoid non-determinism in defunctionalization (#2069) chore: change 'unnecessary pub' error to a warning (#2064) feat!: Update to ACVM 0.21.0 (#2051) chore: Rename execute tests for an accurate description (#2063) chore: Restore lost integration test (#2062) ...
New minimal reproduction. use dep::std::ec::tecurve::affine::Curve as AffineCurve;
use dep::std::ec::tecurve::affine::Point as Gaffine;
use dep::std::ec::tecurve::curvegroup::Curve;
use dep::std::ec::tecurve::curvegroup::Point as G;
use dep::std::ec::swcurve::affine::Point as SWGaffine;
use dep::std::ec::swcurve::curvegroup::Point as SWG;
use dep::std::ec::montcurve::affine::Point as MGaffine;
use dep::std::ec::montcurve::curvegroup::Point as MG;
fn main() {
// Define Baby Jubjub (ERC-2494) parameters in affine representation
let bjj_affine = AffineCurve::new(168700, 168696, Gaffine::new(995203441582195749578291179787384436505546430278305826713579947235728471134,5472060717959818805561601436314318772137091100104008585924551046643952123905));
// Test addition
let p1_affine = Gaffine::new(17777552123799933955779906779655732241715742912184938656739573121738514868268, 2626589144620713026669568689430873010625803728049924121243784502389097019475);
let p2_affine = Gaffine::new(16540640123574156134436876038791482806971768689494387082833631921987005038935, 20819045374670962167435360035096875258406992893633759881276124905556507972311);
let p3_affine = bjj_affine.add(p1_affine, p2_affine);
// Test SWCurve equivalents of the above
// First the affine representation
let bjj_swcurve_affine = bjj_affine.into_swcurve();
let p1_swcurve_affine = bjj_affine.map_into_swcurve(p1_affine);
let p2_swcurve_affine = bjj_affine.map_into_swcurve(p2_affine);
let p3_swcurve_affine = bjj_affine.map_into_swcurve(p3_affine);
let p3_swcurve_affine_from_add = bjj_swcurve_affine.add(
p1_swcurve_affine,
p2_swcurve_affine
);
// Check that these points are on the curve
assert(
bjj_swcurve_affine.contains(p1_swcurve_affine)
);
} |
This now panics with
with the printed SSA being
@jfecher I don't think I can meaningfully debug this to be honest. It seems odd to me that we wouldn't create a numeric constant rather than a param for an function argument if we know it's value at compile time though, should we be rewriting these? |
@TomAFrench found the error: #2117. Not sure why this PR specifically was triggering it though. |
* master: feat!: Support workspaces and package selection on every nargo command (#1992) chore: Make a more clear error for slices passed to std::println (#2113) feat: Implement type aliases (#2112) feat: Add `Option<T>` to noir stdlib (#1781) feat: Format strings for prints (#1952) feat(acir_gen): RecursiveAggregation opcode and updates to black box func call generation (#2097) fix: Mutating a variable no longer mutates its copy (#2057) fix: Implement `.len()` in Acir-Gen (#2077) chore: clippy fixes (#2101)
@jfecher can you rereview please? Your requested changes should be addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to finally merge this in 🙂
* master: (50 commits) fix(globals): Accurately filter literals for resolving globals (#2126) feat: Optimize away constant calls to black box functions (#1981) fix: Rename `Option::value` to `Option::_value` (#2127) feat: replace boolean `AND`s with multiplication (#1954) chore: create a `const` to hold the panic message (#2122) feat: Add support for bitshifts by distances known at runtime (#2072) feat: Add additional `BinaryOp` simplifications (#2124) fix: flattening pass no longer overwrites previously mapped condition values (#2117) chore(noirc_driver): Unify crate preparation (#2119) feat!: Support workspaces and package selection on every nargo command (#1992) chore: Make a more clear error for slices passed to std::println (#2113) feat: Implement type aliases (#2112) feat: Add `Option<T>` to noir stdlib (#1781) feat: Format strings for prints (#1952) feat(acir_gen): RecursiveAggregation opcode and updates to black box func call generation (#2097) fix: Mutating a variable no longer mutates its copy (#2057) fix: Implement `.len()` in Acir-Gen (#2077) chore: clippy fixes (#2101) chore: Update `noir-source-resolver` to v1.1.3 (#1912) chore: Document `GeneratedAcir::more_than_eq_comparison` (#2085) ...
* master: chore: rename `ssa_refactor` module to `ssa` (#2129) chore: Use `--show-output` flag on execution rather than compilation (#2116) fix(globals): Accurately filter literals for resolving globals (#2126) feat: Optimize away constant calls to black box functions (#1981) fix: Rename `Option::value` to `Option::_value` (#2127) feat: replace boolean `AND`s with multiplication (#1954) chore: create a `const` to hold the panic message (#2122) feat: Add support for bitshifts by distances known at runtime (#2072) feat: Add additional `BinaryOp` simplifications (#2124) fix: flattening pass no longer overwrites previously mapped condition values (#2117) chore(noirc_driver): Unify crate preparation (#2119) feat!: Support workspaces and package selection on every nargo command (#1992) chore: Make a more clear error for slices passed to std::println (#2113) feat: Implement type aliases (#2112) feat: Add `Option<T>` to noir stdlib (#1781) feat: Format strings for prints (#1952) feat(acir_gen): RecursiveAggregation opcode and updates to black box func call generation (#2097) fix: Mutating a variable no longer mutates its copy (#2057) fix: Implement `.len()` in Acir-Gen (#2077)
* master: chore: rename `ssa_refactor` module to `ssa` (#2129) chore: Use `--show-output` flag on execution rather than compilation (#2116) fix(globals): Accurately filter literals for resolving globals (#2126) feat: Optimize away constant calls to black box functions (#1981) fix: Rename `Option::value` to `Option::_value` (#2127) feat: replace boolean `AND`s with multiplication (#1954) chore: create a `const` to hold the panic message (#2122) feat: Add support for bitshifts by distances known at runtime (#2072) feat: Add additional `BinaryOp` simplifications (#2124) fix: flattening pass no longer overwrites previously mapped condition values (#2117) chore(noirc_driver): Unify crate preparation (#2119) feat!: Support workspaces and package selection on every nargo command (#1992) chore: Make a more clear error for slices passed to std::println (#2113) feat: Implement type aliases (#2112) feat: Add `Option<T>` to noir stdlib (#1781) feat: Format strings for prints (#1952) feat(acir_gen): RecursiveAggregation opcode and updates to black box func call generation (#2097) fix: Mutating a variable no longer mutates its copy (#2057) fix: Implement `.len()` in Acir-Gen (#2077)
* master: chore: rename `ssa_refactor` module to `ssa` (#2129) chore: Use `--show-output` flag on execution rather than compilation (#2116) fix(globals): Accurately filter literals for resolving globals (#2126) feat: Optimize away constant calls to black box functions (#1981) fix: Rename `Option::value` to `Option::_value` (#2127) feat: replace boolean `AND`s with multiplication (#1954) chore: create a `const` to hold the panic message (#2122) feat: Add support for bitshifts by distances known at runtime (#2072) feat: Add additional `BinaryOp` simplifications (#2124) fix: flattening pass no longer overwrites previously mapped condition values (#2117) chore(noirc_driver): Unify crate preparation (#2119)
Description
Problem*
Resolves
Summary*
The Noir program below
previously would output the ACIR
This is suboptimal for a couple of reasons:
x & 1 == x
so these gates should be optimised awayx & y
where neither are comptime then it's cheaper to multiply them together than do anAND
.I've then added a special case for
AND
between boolean variables which performs a multiplication. This results in the same program being optimized away such that we get the ACIR:Documentation
This PR requires documentation updates when merged.
Additional Context
PR Checklist*
cargo fmt
on default settings.