-
Notifications
You must be signed in to change notification settings - Fork 248
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
Rework inc_rc
, dec_rc
and how arrays are handled in brillig in general
#7287
Comments
Just to make sure I understand, you're not suggesting that I'm asking because I have difficulty understanding some of the SSA generated from the test that fails in #7297 For example Take this example: maybe_mutuse std::mem::array_refcount;
fn main() {
let mut array = [0, 1, 2];
maybe_mut(array, false);
maybe_mut(array, true);
println(array[0]);
println(array_refcount(array));
}
fn maybe_mut(mut array: [Field; 3], should_mutate: bool) {
if should_mutate {
array[0] = 4;
}
println(array[0]);
} It prints 3 as the final refcount in brillig(inline) fn maybe_mut f1 {
b0(v0: [Field; 3], v1: u1):
inc_rc v0
v2 = allocate -> &mut [Field; 3]
store v0 at v2
jmpif v1 then: b1, else: b2
b1():
v3 = load v2 -> [Field; 3]
v6 = array_set v3, index u32 0, value Field 4
store v6 at v2
jmp b2()
b2():
v7 = load v2 -> [Field; 3]
v8 = array_get v7, index u32 0 -> Field
call f2(v8)
return
} Does your proposal address the speculative nature of I wonder if we could have a concept of (The deduplication of increments is a counter example for where my assumption that observable side effects - the printing of the ref count in this instance - should be the same between the initial and final SSA breaks down). |
No, this proposal is to remove
I've considered trying to track when the arrays may be dropped in the past but as soon as the array gets stored in a mutable reference - which is the case with most arrays being mutated anyway - we lose almost all ability to track them since we'd have to delegate to mem2reg. You could also say we do have a drop of sorts since we do issue some
|
The deduplication of consecutive RC instructions happens here,
As another example of why it's difficult to handle this in SSA when the RC is also in the hands of the Brillig VM, here's the SSA of such a function from my experiment:
So it originally inserted an
Thanks for making that clear. I feared that for the same reason as above, if |
For the record, the RC rules that would make more sense to me would be along the following lines:
Then we could let the SSA figure out which inc/dec pairs it can get rid of for optimisation, but it would make it clear when the RC is increased and why. Returning the array or storing it in a data structure should cancel the decrement. Is this 🌈 and 🦄 thinking? |
One more observation about placing the
This might be fixed by #7297 (comment) |
Not necessarily but there are a couple of issues / practical concerns. The scheme you gave is more or less how they're intended to behave but have been changed over time as we were pressured to create smaller brillig bytecode sizes. Increasing the ref counts on each immutable function was leading to a bytecode explosion since it happens much more often and we cannot usually optimize out these cases. A related issue is that Noir doesn't have the concept of an immutable borrow which would normally be the case you'd keep the ref count the same - where a conceptual clone like we'd have where you pass by value you do normally increase it. So we wanted to change where ref counts are to find places where less of them were needed and they were easier to optimize out. That's why we have both inc & dec within a function since then it is easier to identify when we can remove them as a pair. Versus if we had the inc outside the function we can't do that unless the function is inlined.
How do you know if the array was returned or stored in a data structure (and returned)? Usually references are involved so you won't know if the array in this reference is the same or not. Our answer for this currently is to say if the function returns any types containing an array of the same type then we have to pessimistically assume the array in question may be returned. This should lead to counts drifting up, not down, but I point it out as something to keep in mind that we probably can't be perfect here. Anyways - this was mostly to explain why we have our current system / how we got here since I think once you know that its easier to make changes to fix it or tear it down if necessary. EDIT: Clarification on why we increment on mutable values currently. In the current scheme to minimize unnecessary inc/decs we:
|
Ah, some details I left out from our current scheme:
|
Honestly, I don't see the correctness argument for that optimization - I think we should remove it entirely. Especially since counts are underflowing, one reason could be that these extra inc_rcs are required.
Yeah, this seems bad. It makes me think that we need a larger redesign as well since right not trying to fix it up seems to me like it'd just be "garbage in, garbage out." Move & Copy Elision Optimization scheme I think going forward for more confidence in correctness & still having some optimizations we could:
|
Sounds good!
I hope that this particular issue will be fixed by #7297 |
Expose immutable references directly scheme Spoke with @michaeljklein and @aakoshh and another option we're more strongly considering now is to add immutable references to Noir. If we do that:
This is something I'd support experimenting with. We would have to update libraries to take references where possible but could compare performance afterward. |
inc_rc
& dec_rc
with normal operations on integer referencesinc_rc
, dec_rc
and how arrays are handled in brillig in general
Problem
inc_rc
anddec_rc
are a common source of bugs in SSA code generated by the compiler. They require special handling since they are effectively implicit variables attached to each array in unconstrained code.Happy Case
Now that runtimes are separated before SSA we could represent arrays differently in acir & brillig. In Acir they can continue to be represented as normal arrays, while in brillig we could lower them as a tuple of
(&mut Field, [T; N])
where the first element of the tuple is the reference count represented as a mutable field value, and the second is the array itself. If we do this, reference counts now need much fewer special handling or optimizations since they'll be handled by mem2reg along with other references.This could make it more difficult for optimizations like deduplication to insert extra
inc_rc
equivalents since they'd have to find the integer that represents the array's reference count instead of it always being attached to the array.Uncertain how much code improvement this will bring or if it will improve optimizations so I labeled this as "experiment" with rather than a "lets definitely do this."
EDIT: See comments below for some alternative approaches.
Workaround
None
Workaround Description
No response
Additional Context
No response
Project Impact
None
Blocker Context
No response
Would you like to submit a PR for this Issue?
None
Support Needs
No response
The text was updated successfully, but these errors were encountered: