binius_m3/builder/
stat.rs

1// Copyright 2025 Irreducible Inc.
2
3use std::fmt;
4
5use binius_field::TowerField;
6use binius_math::EvalCost;
7use binius_utils::{checked_arithmetics::log2_strict_usize, sparse_index::SparseIndex};
8
9use super::{Table, TablePartition};
10
11struct Constraint {
12	name: String,
13	degree: usize,
14	eval_cost: EvalCost,
15}
16
17#[derive(Default)]
18struct PerPartition {
19	constraints: Vec<Constraint>,
20}
21
22#[derive(Default)]
23struct PerTowerLevel {
24	/// Index from the log2 V (`values_per_row`) to the struct holding a set of constraints.
25	per_v: SparseIndex<PerPartition>,
26}
27
28/// Table statistics.
29pub struct TableStat {
30	name: String,
31	/// Index from the log2 tower level to the struct holding a set of partitions.
32	per_tower_level: SparseIndex<PerTowerLevel>,
33	bits_per_row_committed: usize,
34	bits_per_row_virtual: usize,
35	/// Total number of flushed entries across all table partitions
36	total_flush_count: u64,
37}
38
39impl TableStat {
40	pub(super) fn new<F: TowerField>(table: &Table<F>) -> Self {
41		let mut bits_per_row_committed = 0;
42		let mut bits_per_row_virtual = 0;
43		for column in &table.columns {
44			let bits_per_column = 1 << column.shape.log_cell_size();
45			if matches!(column.col, super::ColumnDef::Committed { .. }) {
46				bits_per_row_committed += bits_per_column;
47			} else {
48				bits_per_row_virtual += bits_per_column;
49			}
50		}
51
52		// Calculate total flush count across all partitions
53		let mut total_flush_count = 0u64;
54		for (_, partition) in table.partitions.iter() {
55			for flush in &partition.flushes {
56				total_flush_count += flush.multiplicity as u64;
57			}
58		}
59
60		let mut me = Self {
61			name: table.name.clone(),
62			per_tower_level: SparseIndex::new(),
63			bits_per_row_committed,
64			bits_per_row_virtual,
65			total_flush_count,
66		};
67
68		for (_, partition) in table.partitions.iter() {
69			let &TablePartition {
70				values_per_row,
71				zero_constraints,
72				..
73			} = &partition;
74			let v_log2 = log2_strict_usize(*values_per_row);
75			for zero_constraint in zero_constraints {
76				let tower_level = zero_constraint.tower_level;
77				let name = zero_constraint.name.clone();
78				let degree = zero_constraint.expr.degree();
79				let eval_cost = zero_constraint.expr.eval_cost();
80				me.add_constraint(
81					tower_level,
82					v_log2,
83					Constraint {
84						name,
85						degree,
86						eval_cost,
87					},
88				);
89			}
90		}
91
92		me
93	}
94
95	fn add_constraint(&mut self, tower_level: usize, v_log2: usize, c: Constraint) {
96		let per_tower_level = self
97			.per_tower_level
98			.entry(tower_level)
99			.or_insert_with(PerTowerLevel::default);
100		let per_partition = per_tower_level
101			.per_v
102			.entry(v_log2)
103			.or_insert_with(PerPartition::default);
104		per_partition.constraints.push(c);
105	}
106
107	/// Returns the table name.
108	pub fn name(&self) -> &str {
109		&self.name
110	}
111
112	/// Returns the approximate cost of [`assert_zero`][zero_constraint] constraints.
113	///
114	/// The dominating factor of the proving cost of zero constraints is a product of:
115	///
116	/// - The degree of the constraint expression.
117	/// - The number of multiplications in the expression.
118	/// - The tower level over which the constraint expression is defined. Note that sub-byte towers
119	///   are counted the same way as full 8-bit fields.
120	/// - The `values_per_row` number.
121	///
122	/// This is an approximate number and there are potentially other factors that can influence
123	/// the cost.
124	///
125	/// [zero_constraint]: super::TableBuilder::assert_zero
126	pub fn assert_zero_cost_approx(&self) -> usize {
127		let mut cost = 0;
128		for (tower_level, per_tower_level) in self.per_tower_level.iter() {
129			for (v_log2, per_partition) in per_tower_level.per_v.iter() {
130				for constraint in &per_partition.constraints {
131					let values_per_row = 1 << v_log2;
132					// Scale the constraint cost by the number of bits in the constrained elements.
133					// Treat the minimum number of bits as 8 because the B8 field is the smallest
134					// that we can evaluate zerocheck constraints over, using the univariate skip
135					// strategy.
136					let bits = (1 << tower_level).max(8) * values_per_row;
137					let degree = constraint.degree;
138					let mult_cost_approx = constraint.eval_cost.mult_cost_approx();
139					cost += bits * degree * mult_cost_approx;
140				}
141			}
142		}
143		cost
144	}
145
146	/// Returns the number of committed bits per row of this table. Committed bits are contributed
147	/// by the [`add_committed`][add_committed] columns.
148	///
149	/// Committed bits increase the proof size and generally lead to faster verification time
150	/// relative to comparable virtual bits.
151	///
152	/// [add_committed]: super::TableBuilder::add_committed
153	pub fn bits_per_row_committed(&self) -> usize {
154		self.bits_per_row_committed
155	}
156
157	/// Returns the number of virtual bits per row of this table. Virtual bits are pretty much any
158	/// non-[`add_committed`][`add_committed`] columns. E.g. [`add_shifted`][`add_shifted`],
159	/// [`add_computed`][`add_computed`], etc.
160	///
161	/// Virtual bits do not affect the proof size but generally lead to slower verification time
162	/// relative to committed bits.
163	///
164	/// [`add_committed`]: super::TableBuilder::add_committed
165	/// [`add_shifted`]: super::TableBuilder::add_shifted
166	/// [`add_computed`]: super::TableBuilder::add_computed
167	pub fn bits_per_row_virtual(&self) -> usize {
168		self.bits_per_row_virtual
169	}
170
171	/// Returns the total number of flushed entries across all table partitions.
172	///
173	/// This represents the number of times column values are flushed to channels,
174	/// accounting for multiplicity. Higher flush counts generally indicate higher
175	/// proving costs as flushes are a significant factor in proof generation time.
176	pub fn total_flush_count(&self) -> u64 {
177		self.total_flush_count
178	}
179}
180
181impl fmt::Display for TableStat {
182	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183		writeln!(f, "table '{}':", self.name)?;
184		writeln!(f, "* bits per row: {}", self.bits_per_row_committed + self.bits_per_row_virtual)?;
185		writeln!(f, "  committed: {}", self.bits_per_row_committed)?;
186		writeln!(f, "  virtual: {}", self.bits_per_row_virtual)?;
187		writeln!(f, "* total flush count: {}", self.total_flush_count)?;
188		writeln!(f, "* zero checks:")?;
189		for (tower_level, per_tower_level) in self.per_tower_level.iter() {
190			let bits = 1 << tower_level;
191			writeln!(f, "  B{bits}:")?;
192			for (v_log2, per_partition) in per_tower_level.per_v.iter() {
193				let values_per_row = 1 << v_log2;
194				writeln!(f, "    values_per_row={values_per_row}:")?;
195				for (i, constraint) in per_partition.constraints.iter().enumerate() {
196					let ordinal = i + 1;
197					let Constraint {
198						name,
199						degree,
200						eval_cost,
201					} = constraint;
202					let n_adds = eval_cost.n_adds;
203					let n_muls = eval_cost.n_muls;
204					let n_squares = eval_cost.n_squares;
205					writeln!(
206						f,
207						"      {ordinal}. {name}: deg={degree},  #+={n_adds}, #×={n_muls}, #²={n_squares}"
208					)?;
209				}
210			}
211		}
212
213		let cost = self.assert_zero_cost_approx();
214		writeln!(f, "Total approximate assert_zero costs: {cost}")?;
215		Ok(())
216	}
217}