The Noir Profiler

NoirCon 1 at ETHDenver 2025 · February 2025 · 15:16

Aztec engineer Maxim Vezenov presents the Noir Profiler at NoirCon 1 during ETHDenver 2025. The tool helps developers see where their Noir circuits spend constraints and gates, so they can find bottlenecks and optimise their zero-knowledge programs before shipping them.

Speakers

Topics

From NoirCon 1 at ETHDenver 2025, hosted by Aztec Network.

Transcript

like Claire said there is another Workshop going on so if you're not familiar with Noir at all that that Workshop might be a bit better as it's getting started it'll be with our excellent Deval uh Z Pedro here I'll be going over a new tool that the Noir team is proud to announce and the Noir profiler so it's no profiler is essentially a sampling profiler that is is actually going to map your execution information to the source code so you'll actually be able to see where your bottlenecks are according to call stex when I say uh the execution data there's a few things here uh there's the assir so that's our intermediate

representation that allows Noir to be backend agnostic but then we also you need to be able to express your basically your constraints and your execution with that respective backend so that is the backend Gates and then we also have unconstrained execution which is the ability to execute Noir outside of your circuit and then ultimately inject information back into your circuit so I'm going to do a simple walkthrough of how you might actually use this profiler so right here we have like a very simple program all we're doing is we're zeroing out an array past some Dynamic pointer by whenever I say Dynamic it means we have witness inputs that that came from

Maine so we do provide some like intermediate kind simple ways to you know benchmark your program we have nargo info it'll tell you some information okay we have 24K AER op codes and when you see the brillig that is the IR for our unconstrained execution so you see a couple uh also brilic functions there that's because we have some uh hardcoded in the compile like very common unconstrained execution that we use to optimize things underneath the hood for you so this is just a simple inversion and and quotient uh we'll go over that more later um but yeah we need we need more information

here um especially if we're going to that that's a very simple program what if we start to get to some more complex programs so this is a very simple example of what the profile would look like if you're familiar at all with flame graphs this is this is this is that so you would run you would run your uh command and you would generate a flame graph that that Maps basically the assir op codes that are associated with specific source code so here we can see I've highlighted actually the memory operations that we get from that array right that we have and that takes up right now the majority of our program although we do have various other aere op codes here is like a simple GIF if you're not familiar with the flame graph

you can click into your call Stacks you can explore it this says this program is just main with a loop so the call Stacks aren't very aren't very large but obviously this can this can scale up with larger programs you can search using Rex I can search for the source code I can search for the op codes this makes it a lot easier now to actually like break down you know where where are the bottlenecks in my program and then also quickly touch on that yes we also generate flame graphs for your unconstrained execution uh this I when you generate with the the previous command I just ran you might see these other uh flame graphs here uh the we have these like directives which

might not be as useful for you but if you're starting to do some more advanced unconstrained execution and you're trying to optimize optimize basically your witness generation this might also be relevant to you but for this talk I'm mostly focused on just circuit optimizations and minimizing the amount of amount of gates you in your actual circuit so here's a simple example of the brillig uh one you'll notice though there's no source code because this one is from uh our our compiler hardcoded just a few op codes in order to do here we're doing a quotient yeah so now let's actually like optimize our circuit and and look at look at what

the flame graph might switch into so we're going to use unconstrained functions uh this is the ability to now you know exit our circuit and compute something outside of the circuit uh if you're unfamiliar with unconstrained functions I recommend going to the Noir docs where there's a primer on it but I'm not going to be going over that here so our original program we see now let's instead mutate that array in an unconstrained function so we're we've moved out of our circuit environment and this is essentially you could think of just normal execution this is Noir normal VM uh just executing as you would expect for any regular program and then inside of our actual circuit we're then going to constrain

what we expect that output to be so we want we don't want to we don't want to write in our circuit and do these expensive memory operations so instead we're going to say okay everything past that pointer has to be zero and then everything else we expect to equal that original array that we had we have to connect it to our original inputs or some kind of constant there's some other things you can see here with how we require you to call in constrain functions it's an an unsafe block with a safety comment because you need to be able to you need to actually constrain it once you get back to your circuit so here's an example of our now optimized flame graph uh we no longer have any memory operations which is what we want and you'll also notice we now have 18,000 op codes instead of 24K this

is this is pretty good um but we'll go over how it's actually this doesn't give us the full picture once again here's them just side by side and how the op codes look a little bit different the flame graph looks a little bit different and you can see now that the majority of our actual uh computation comes from just that comparison of the of the pointer we also let you actually have like the backend specific information as well so here we can actually Supply a backend binary to our profiler along with the command that when you you know the back end you were assuming has followed our API that is expected and you will then be able to generate a now

the same kind of graph but with backend Gates so we can see actually the memory operations it's for the AER it was like 22% of our like total uh stuff that we had to compute here it's actually 66% because we actually have to do a lot more things in the back end in order to do these Dynamic rights so this is this is a large chunk of our program this is our true bottleneck um so that same graph now with with the uh but with the optimized version we can see we're from 65k gates to 23k so this is actually a much larger uh optimization than the Asser like showed us and once again all memory operations have been removed and those like the

actual constrain part where we're just checking whether or not our array is is good uh yeah it's actually only like 88.6% um and you might notice though that there is like all these like range checks uh this because once again where it is important to have that backend specific context we're using ultrah honk for this which does has lookup tables under the hood and that's going to so all these range gates are kind of are using those lookup tables so you're going to see a large chunk of the program is due to those is due to that um so sometimes you even though once you start to get into the op iing for the specific backend you do kind of have to

start to get the the specific context there so here is them just next to each other once again and yeah you can see how we got rid of those memory Ops which is what we wanted to do so just in just to conclude um you know you're going to need both flame graphs to get a true picture of your optimizations as every backend is going to implement these these op codes slightly differently uh Groth 16 isn't going to have uh lookup tables for their for their range checks and you're not and then now we have backends like with world and uh other people are implementing their own backends you're going to need that specific context when

profiling um and I think any backend actually should be able to just easily integrate with the profiler uh it takes in a specific command and if you follow the API and you're working on a back end you can also integrate this tool for your own optim Iz ation in your own circuits you can use it now it was released in our most recent release uh beta point3 for 1.0 and then you can also go to the the documentation which has a lot of the similar stuff to this talk um yeah if there's any questions about the profiler feel free to to shoot now and that's it is there any

questions you ask what affects the pro do you mean the circuit size like the size of the program that's okay so he asked like what affects the proof size inir that's going to depend on your back end um there when we were using Ultra plunk previously and uh other other proving systems they have a constant proof size but Ultra honk actually it it's uh it's linear so or no like logarithmic it's like log n to your to your circuit so it's going to be it's going to grow with your with your circuit size as well um it depends on your back end yeah so the other backends also will have you know proof sizes that are different um so

once you actually start to get to these optimizations you are going to need more than just a sear and more than just the pure Noir program because as we as as I showed you it went from 24K aop codes to 18K aop codes but that didn't that didn't tell me how much I actually optimized my program for ultrah hunk I don't know what it would be like in a different in a different environment yeah so the proof size is not dependent just on Noir you're going to have to uh if you are by

fully integrated it would assume that you also have I guess the profiler you're following what we expect as input from a backend there's specific uh format it's not too complex it's just when you if you're integrating a backend when you add a certain aere op code you should also track how many backend Gates basically you're adding and that if if you integrate that Gates command is what we call it then you'll be integrated with the profiler so we just the profiler for the back for the backend Gates it'll take in a binary and then a command and then any extra flags that you might want for your command so it just it just executes that binary the back in binary uh we we compile down to a seir

which then can compile down to plish or R1 CS uh so right now the most supportive backend is the one that Aztec uses is ultra honk but you can integrate any backend that might conform to a seere so there is other people who are working on uh for their own means worldcoin is the most recent example they're working on like a Spartan and were back end that follows their needs better than than Ultra hunk for which follows Aztec needs so it's backend agnostic uh you mean like optimizing like the Air Generation in Noir uh not yet yeah we

welcome it though no no one has tried to optimize Our aere Generation yet but um maybe there's room for that any other questions all right yeah yeah so so the question was asking if I want to actually use my Noir program for multiple backends uh do I

risk like optimizing for one and not optimizing for the other yes basically basically yeah yeah if you if you really want to use the same Noir program for multiple back ends it it it will be different most likely um so even though Noir is backend Diagnostic and you can easily Port something like the the context does matter um once you actually want to get to proving so I mean that's a general I guess

uh it matters on your development cycle I would say but like because if you wanted this is this is once you're already getting to like optimizations and you're ready to productionize more if we're just talking about prototyping trying to work with a different backend I think it's much more you know yeah you can part Port your Noir program to the gr 16 backend now and start working with that if you want to start to get to you know production optimization this is more relevant because then you actually need to get your circuit as small as possible for your specific case so yeah if you want if if you hyper optimize your program for the specific backend yeah unfortunately becomes less agnostic but it could still be agnostic it just

might not be as efficient in the other version it's similar with other backends in traditional programming as well you it's basically do tar what do you target all right any if there's not any other questions I'll be around though um and happy to happy to talk