Skip to main content

binius_frontend/
stat.rs

1// Copyright 2025 Irreducible Inc.
2
3//! Circuit statistics module for analyzing constraint counts and circuit complexity.
4
5use std::fmt;
6
7use binius_core::{ConstraintSystem, Operand, ShiftedValueIndex};
8
9use crate::compiler::circuit::Circuit;
10
11/// Various stats of a circuit that affect the prover performance.
12pub struct CircuitStat {
13	/// Number of gates in the circuit.
14	pub n_gates: usize,
15	/// Number of instructions in the evaluation form of circuit.
16	///
17	/// Directly proportional to performance of witness filling.
18	pub n_eval_insn: usize,
19	/// Number of AND constraints in the circuit.
20	///
21	/// Affects performance of AND reduction.
22	pub n_and_constraints: usize,
23	/// Number of MUL constraints in the circuit.
24	///
25	/// Affects performance of intmul reduction phase.
26	pub n_mul_constraints: usize,
27	/// Number of distinct value indices with non-zero shift in the circuit.
28	///
29	/// Every use of a value with a distinct type and amount is counted here.
30	///
31	/// Affects performance of shift reduction phase.
32	pub distinct_shifted_value_indices: usize,
33	/// Number of distinct value indices with zero shift in the circuit.
34	///
35	/// Affects performance of shift reduction phase.
36	pub distinct_unshifted_value_indices: usize,
37	/// Length of the value vector.
38	///
39	/// Affects performance of committing.
40	pub value_vec_len: usize,
41	/// Number of constant values used by the circuit.
42	pub n_const: usize,
43	/// Number of public input values in the circuit.
44	pub n_inout: usize,
45	/// Number of private input values in the circuit.
46	pub n_witness: usize,
47	/// Number of internal values in the circuit.
48	///
49	/// Internal values are values produced by gates.
50	pub n_internal: usize,
51	/// Number of scratch values in the circuit.
52	///
53	/// Those values are not committed, those only exist during witness generation.
54	pub n_scratch: usize,
55	/// Allocated size for AND constraints (power of 2)
56	pub and_allocated: usize,
57	/// Allocated size for MUL constraints (power of 2)
58	pub mul_allocated: usize,
59	/// Allocated size for public section (power of 2)
60	pub public_allocated: usize,
61	/// Allocated size for private section.
62	///
63	/// This is the space available for witness and internal values. Note that unlike
64	/// `public_allocated` and the total committed length, this is NOT necessarily a
65	/// power of two. It's simply the difference between the total committed length
66	/// (power of 2) and the public section size (power of 2). For example, if total
67	/// is 8192 and public is 128, private is 8064.
68	pub private_allocated: usize,
69}
70
71impl CircuitStat {
72	/// Creates a new `CircuitStat` instance by collecting statistics from the given circuit.
73	pub fn collect(circuit: &Circuit) -> Self {
74		// Clone the constraint system so we can prepare it
75		let mut cs = circuit.constraint_system().clone();
76
77		// Store original counts before padding
78		let n_and_constraints = cs.n_and_constraints();
79		let n_mul_constraints = cs.n_mul_constraints();
80		let (distinct_shifted_value_indices, distinct_unshifted_value_indices) =
81			traverse_constraint_system(&cs);
82
83		// validate_and_prepare will pad constraints to power of 2
84		cs.validate_and_prepare()
85			.expect("constraint system should be valid");
86
87		// Now we have the actual allocated sizes after padding
88		let and_allocated = cs.n_and_constraints();
89		let mul_allocated = cs.n_mul_constraints();
90
91		// The public section size is already determined by the layout
92		let n_const = cs.value_vec_layout.n_const;
93		let n_inout = cs.value_vec_layout.n_inout;
94		let public_allocated = cs.value_vec_layout.offset_witness;
95		let total_allocated = cs.value_vec_layout.committed_total_len;
96		let private_allocated = total_allocated - public_allocated;
97
98		Self {
99			n_gates: circuit.n_gates(),
100			n_eval_insn: circuit.n_eval_insn(),
101			n_and_constraints,
102			n_mul_constraints,
103			value_vec_len: total_allocated,
104			distinct_shifted_value_indices,
105			distinct_unshifted_value_indices,
106			n_const,
107			n_inout,
108			n_witness: cs.value_vec_layout.n_witness,
109			n_internal: cs.value_vec_layout.n_internal,
110			n_scratch: cs.value_vec_layout.n_scratch,
111			and_allocated,
112			mul_allocated,
113			public_allocated,
114			private_allocated,
115		}
116	}
117}
118
119impl fmt::Display for CircuitStat {
120	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121		// Helper to format numbers with commas
122		fn fmt_num(n: usize) -> String {
123			let s = n.to_string();
124			let mut result = String::new();
125			for (i, c) in s.chars().rev().enumerate() {
126				if i > 0 && i % 3 == 0 {
127					result.push(',');
128				}
129				result.push(c);
130			}
131			result.chars().rev().collect()
132		}
133
134		// Helper to create a simple progress bar
135		fn progress_bar(used: usize, total: usize) -> String {
136			let percent = (used as f64 / total as f64 * 100.0) as usize;
137			let filled = percent / 10;
138			let mut bar = String::from("[");
139			for i in 0..10 {
140				if i < filled {
141					bar.push('▓');
142				} else {
143					bar.push('░');
144				}
145			}
146			bar.push(']');
147			bar
148		}
149
150		// Helper to get log2 of a power of 2
151		fn log2(n: usize) -> u32 {
152			n.trailing_zeros()
153		}
154
155		// Use pre-calculated values
156		let public_used = self.n_const + self.n_inout;
157		let private_used = self.n_witness + self.n_internal;
158		let total_used = public_used + private_used;
159
160		// Gates & Instructions
161		writeln!(f, "Gates & Instructions")?;
162		writeln!(f, "├─ Number of gates: {}", fmt_num(self.n_gates))?;
163		writeln!(f, "└─ Number of evaluation instructions: {}", fmt_num(self.n_eval_insn))?;
164		writeln!(f)?;
165
166		// Constraints
167		writeln!(f, "Constraints")?;
168		let and_percent = self.n_and_constraints as f64 / self.and_allocated as f64 * 100.0;
169		let and_spare = self.and_allocated - self.n_and_constraints;
170		writeln!(
171			f,
172			"├─ AND constraints: {} used ({:.1}% of 2^{})",
173			fmt_num(self.n_and_constraints),
174			and_percent,
175			log2(self.and_allocated)
176		)?;
177		writeln!(
178			f,
179			"│  {} spare: {}",
180			progress_bar(self.n_and_constraints, self.and_allocated),
181			fmt_num(and_spare)
182		)?;
183
184		let mul_percent = if self.mul_allocated > 0 {
185			self.n_mul_constraints as f64 / self.mul_allocated as f64 * 100.0
186		} else {
187			0.0
188		};
189		let mul_spare = self.mul_allocated - self.n_mul_constraints;
190		writeln!(
191			f,
192			"├─ MUL constraints: {} used ({:.1}% of 2^{})",
193			fmt_num(self.n_mul_constraints),
194			mul_percent,
195			log2(self.mul_allocated)
196		)?;
197		writeln!(
198			f,
199			"│  {} spare: {}",
200			progress_bar(self.n_mul_constraints, self.mul_allocated),
201			fmt_num(mul_spare)
202		)?;
203		writeln!(
204			f,
205			"└─ Distinct value indices: {}",
206			fmt_num(self.distinct_shifted_value_indices + self.distinct_unshifted_value_indices)
207		)?;
208		writeln!(
209			f,
210			"   ├─ Distinct shifted value indices: {}",
211			fmt_num(self.distinct_shifted_value_indices)
212		)?;
213		writeln!(
214			f,
215			"   └─ Distinct unshifted value indices: {}",
216			fmt_num(self.distinct_unshifted_value_indices)
217		)?;
218		writeln!(f)?;
219
220		// Value Vector
221		writeln!(f, "Value Vector")?;
222
223		// Public Section
224		let public_percent = public_used as f64 / self.public_allocated as f64 * 100.0;
225		let public_spare = self.public_allocated - public_used;
226		writeln!(
227			f,
228			"├─ Public Section: {} used ({:.1}% of 2^{})",
229			fmt_num(public_used),
230			public_percent,
231			log2(self.public_allocated)
232		)?;
233		writeln!(
234			f,
235			"│  {} spare: {}",
236			progress_bar(public_used, self.public_allocated),
237			fmt_num(public_spare)
238		)?;
239		writeln!(f, "│  ├─ Constants: {}", fmt_num(self.n_const))?;
240		writeln!(f, "│  └─ Inout: {}", fmt_num(self.n_inout))?;
241
242		// Private Section (no allocated size shown since it's not a power of 2)
243		let private_percent = private_used as f64 / self.private_allocated as f64 * 100.0;
244		let private_spare = self.private_allocated - private_used;
245		writeln!(
246			f,
247			"├─ Private Section: {} used ({:.1}%)",
248			fmt_num(private_used),
249			private_percent
250		)?;
251		writeln!(
252			f,
253			"│  {} spare: {}",
254			progress_bar(private_used, self.private_allocated),
255			fmt_num(private_spare)
256		)?;
257		writeln!(f, "│  ├─ Witness: {}", fmt_num(self.n_witness))?;
258		writeln!(f, "│  └─ Internal: {}", fmt_num(self.n_internal))?;
259
260		// Total Committed
261		let total_percent = total_used as f64 / self.value_vec_len as f64 * 100.0;
262		let total_spare = self.value_vec_len - total_used;
263		writeln!(
264			f,
265			"├─ Total Committed: {} used ({:.1}% of 2^{})",
266			fmt_num(total_used),
267			total_percent,
268			log2(self.value_vec_len)
269		)?;
270		writeln!(
271			f,
272			"│  {} spare: {}",
273			progress_bar(total_used, self.value_vec_len),
274			fmt_num(total_spare)
275		)?;
276
277		// Scratch
278		writeln!(f, "└─ Scratch (uncommitted): {}", fmt_num(self.n_scratch))?;
279		writeln!(f)?;
280
281		Ok(())
282	}
283}
284
285/// Traverses the constraint system and returns the number of distinct value indices that
286/// are shifted and unshifted, respectively.
287fn traverse_constraint_system(cs: &ConstraintSystem) -> (usize, usize) {
288	use std::collections::HashSet;
289	let mut cx = Cx {
290		shifted_terms: HashSet::new(),
291		unshifted_terms: HashSet::new(),
292	};
293	for and in &cs.and_constraints {
294		visit_operand(&and.a, &mut cx);
295		visit_operand(&and.b, &mut cx);
296		visit_operand(&and.c, &mut cx);
297	}
298	for mul in &cs.mul_constraints {
299		visit_operand(&mul.a, &mut cx);
300		visit_operand(&mul.b, &mut cx);
301		visit_operand(&mul.lo, &mut cx);
302		visit_operand(&mul.hi, &mut cx);
303	}
304	return (cx.shifted_terms.len(), cx.unshifted_terms.len());
305
306	struct Cx {
307		shifted_terms: HashSet<ShiftedValueIndex>,
308		unshifted_terms: HashSet<ShiftedValueIndex>,
309	}
310
311	fn visit_operand(operand: &Operand, cx: &mut Cx) {
312		for term in operand {
313			if term.amount == 0 {
314				cx.unshifted_terms.insert(*term);
315			} else {
316				cx.shifted_terms.insert(*term);
317			}
318		}
319	}
320}