Skip to content

Commit b8d151f

Browse files
perf(felt252): compute felt252_div via a runtime binding
Route felt252 division through a runtime binding (cairo_native__felt252_div) instead of lowering it inline as an extended-euclidean modular inverse plus an i512 multiply-and-remainder. The inline form legalizes into huge limb sequences (the i512 remainder being the worst), making O3 codegen of division-heavy contracts pathologically slow — the same hazard the felt252 multiply binding removed. The binding uses starknet-types-core's field_div (optimized Montgomery inverse); rhs is a NonZero<felt252>, so it is guaranteed nonzero. Also removes the now-dead ExtendedEuclideanWidth::U252 variant, which was only ever constructed by the old division path (U31/U256/U384 remain in use by egcd / u256-invmod). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8e70357 commit b8d151f

3 files changed

Lines changed: 76 additions & 41 deletions

File tree

src/libfuncs/felt252.rs

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
use super::LibfuncHelper;
44
use crate::{
55
error::{panic::ToNativeAssertError, Result},
6-
metadata::{
7-
runtime_bindings::{ExtendedEuclideanWidth, RuntimeBindingsMeta},
8-
MetadataStorage,
9-
},
6+
metadata::{runtime_bindings::RuntimeBindingsMeta, MetadataStorage},
107
utils::{felt_to_unsigned, get_integer_layout, ProgramRegistryExt, PRIME},
118
};
129
use cairo_lang_sierra::{
@@ -72,7 +69,6 @@ pub fn build_binary_operation<'ctx, 'this>(
7269
&info.branch_signatures()[0].vars[0].ty,
7370
)?;
7471
let i256 = IntegerType::new(context, 256).into();
75-
let i512 = IntegerType::new(context, 512).into();
7672

7773
let (op, lhs, rhs) = match info {
7874
Felt252BinaryOperationConcrete::WithVar(operation) => {
@@ -166,49 +162,44 @@ pub fn build_binary_operation<'ctx, 'this>(
166162
entry.trunci(result, felt252_ty, location)?
167163
}
168164
Felt252BinaryOperator::Div => {
165+
// The field division (a modular inverse followed by a multiply) is
166+
// delegated to a runtime binding. Lowering it inline produces an
167+
// extended-euclidean loop plus an i512 multiply and remainder that
168+
// the LLVM backend legalizes into huge sequences of limb
169+
// operations, making codegen of division-heavy contracts
170+
// pathologically slow.
171+
let layout_i256 = get_integer_layout(256);
172+
let lhs_ptr = helper
173+
.init_block()
174+
.alloca1(context, location, i256, layout_i256.align())?;
175+
let rhs_ptr = helper
176+
.init_block()
177+
.alloca1(context, location, i256, layout_i256.align())?;
178+
let dst_ptr = helper
179+
.init_block()
180+
.alloca1(context, location, i256, layout_i256.align())?;
181+
182+
let lhs_i256 = entry.extui(lhs, i256, location)?;
183+
let rhs_i256 = entry.extui(rhs, i256, location)?;
184+
entry.store(context, location, lhs_ptr, lhs_i256)?;
185+
entry.store(context, location, rhs_ptr, rhs_i256)?;
186+
169187
let runtime_bindings_meta = metadata
170188
.get_mut::<RuntimeBindingsMeta>()
171189
.to_native_assert_error(
172190
"Unable to get the RuntimeBindingsMeta from MetadataStorage",
173191
)?;
174-
175-
let prime = entry.const_int_from_type(context, location, PRIME.clone(), felt252_ty)?;
176-
177-
// Find 1 / rhs.
178-
let euclidean_result = runtime_bindings_meta.extended_euclidean_algorithm(
192+
runtime_bindings_meta.felt252_div(
179193
context,
180194
helper.module,
181195
entry,
196+
dst_ptr,
197+
lhs_ptr,
198+
rhs_ptr,
182199
location,
183-
[rhs, prime],
184-
ExtendedEuclideanWidth::U252,
185200
)?;
186201

187-
// Here we omit checking if inverse is actually the inverse,
188-
// satisfying gcd(a,b) == 1, because we are using a prime as the
189-
// modulus. This ensures that for any value of a, included in the
190-
// field, gcd(a,b) == 1.
191-
let prime = entry.const_int_from_type(context, location, PRIME.clone(), i512)?;
192-
let inverse = {
193-
let inverse =
194-
entry.extract_value(context, location, euclidean_result, felt252_ty, 1)?;
195-
entry.extui(inverse, i512, location)?
196-
};
197-
198-
// Peform lhs * (1 / rhs)
199-
let lhs = entry.extui(lhs, i512, location)?;
200-
let result = entry.muli(lhs, inverse, location)?;
201-
// Apply modulo and convert result to felt252
202-
let result_mod = entry.append_op_result(arith::remui(result, prime, location))?;
203-
let is_out_of_range =
204-
entry.cmpi(context, CmpiPredicate::Uge, result, prime, location)?;
205-
let result = entry.append_op_result(arith::select(
206-
is_out_of_range,
207-
result_mod,
208-
result,
209-
location,
210-
))?;
211-
202+
let result = entry.load(context, location, dst_ptr, i256)?;
212203
entry.trunci(result, felt252_ty, location)?
213204
}
214205
};

src/metadata/runtime_bindings.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ enum RuntimeBinding {
5454
U128SquareRoot,
5555
U256SquareRoot,
5656
Felt252Mul,
57+
Felt252Div,
5758
CircuitArithOperation,
5859
DictIntoEntries,
5960
QM31Add,
@@ -89,6 +90,7 @@ impl RuntimeBinding {
8990
Self::U128SquareRoot => "cairo_native__u128_square_root",
9091
Self::U256SquareRoot => "cairo_native__u256_square_root",
9192
Self::Felt252Mul => "cairo_native__felt252_mul",
93+
Self::Felt252Div => "cairo_native__felt252_div",
9294
Self::CircuitArithOperation => "cairo_native__circuit_arith_operation",
9395
Self::DictIntoEntries => "cairo_native__dict_into_entries",
9496
Self::QM31Add => "cairo_native__libfunc__qm31__qm31_add",
@@ -137,6 +139,7 @@ impl RuntimeBinding {
137139
Self::U128SquareRoot => cairo_native__u128_square_root as *const (),
138140
Self::U256SquareRoot => cairo_native__u256_square_root as *const (),
139141
Self::Felt252Mul => cairo_native__felt252_mul as *const (),
142+
Self::Felt252Div => cairo_native__felt252_div as *const (),
140143
Self::ArenaAlloc => cairo_native__arena_alloc as *const (),
141144
Self::ExtendedEuclideanAlgorithm(_) | Self::CircuitArithOperation => return None,
142145
#[cfg(feature = "with-cheatcode")]
@@ -150,7 +153,6 @@ impl RuntimeBinding {
150153
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
151154
pub enum ExtendedEuclideanWidth {
152155
U31,
153-
U252,
154156
U256,
155157
U384,
156158
}
@@ -159,7 +161,6 @@ impl ExtendedEuclideanWidth {
159161
const fn symbol(self) -> &'static str {
160162
match self {
161163
Self::U31 => "cairo_native__u31_extended_euclidean_algorithm",
162-
Self::U252 => "cairo_native__u252_extended_euclidean_algorithm",
163164
Self::U256 => "cairo_native__u256_extended_euclidean_algorithm",
164165
Self::U384 => "cairo_native__u384_extended_euclidean_algorithm",
165166
}
@@ -168,7 +169,6 @@ impl ExtendedEuclideanWidth {
168169
const fn bits(self) -> u32 {
169170
match self {
170171
Self::U31 => 31,
171-
Self::U252 => 252,
172172
Self::U256 => 256,
173173
Self::U384 => 384,
174174
}
@@ -319,6 +319,34 @@ impl RuntimeBindingsMeta {
319319
.into())
320320
}
321321

322+
/// Register if necessary, then invoke `cairo_native__felt252_div`, which
323+
/// computes `(lhs / rhs) mod STARK_PRIME`. The pointers reference 32-byte
324+
/// little-endian felt252 buffers; the result is written to `dst_ptr`.
325+
#[allow(clippy::too_many_arguments)]
326+
pub fn felt252_div<'c, 'a>(
327+
&mut self,
328+
context: &'c Context,
329+
module: &Module,
330+
block: &'a Block<'c>,
331+
dst_ptr: Value<'c, '_>,
332+
lhs_ptr: Value<'c, '_>,
333+
rhs_ptr: Value<'c, '_>,
334+
location: Location<'c>,
335+
) -> Result<OperationRef<'c, 'a>>
336+
where
337+
'c: 'a,
338+
{
339+
let function =
340+
self.build_function(context, module, block, location, RuntimeBinding::Felt252Div)?;
341+
342+
Ok(block.append_operation(
343+
OperationBuilder::new("llvm.call", location)
344+
.add_operands(&[function])
345+
.add_operands(&[dst_ptr, lhs_ptr, rhs_ptr])
346+
.build()?,
347+
))
348+
}
349+
322350
/// Register if necessary, then invoke `cairo_native__u256_square_root` on
323351
/// the `u256` given by its low and high `u128` limbs, returning the `u128`
324352
/// result.
@@ -1005,6 +1033,7 @@ pub fn setup_runtime(find_symbol_ptr: impl Fn(&str) -> Option<*mut c_void>) {
10051033
RuntimeBinding::U128SquareRoot,
10061034
RuntimeBinding::U256SquareRoot,
10071035
RuntimeBinding::Felt252Mul,
1036+
RuntimeBinding::Felt252Div,
10081037
#[cfg(feature = "with-cheatcode")]
10091038
RuntimeBinding::VtableCheatcode,
10101039
] {

src/runtime.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use num_traits::{ToPrimitive, Zero};
1616
use starknet_curve::curve_params::BETA;
1717
use starknet_types_core::{
1818
curve::{AffinePoint, ProjectivePoint},
19-
felt::Felt,
19+
felt::{Felt, NonZeroFelt},
2020
hash::StarkHash,
2121
qm31::QM31,
2222
};
@@ -132,6 +132,21 @@ fn felt_raw_to_le_bytes(value: &Felt) -> [u8; 32] {
132132
buffer
133133
}
134134

135+
/// Compute `lhs / rhs` in the STARK field, i.e. `lhs * rhs⁻¹ mod STARK_PRIME`,
136+
/// and store the canonical little-endian result in `dst`.
137+
///
138+
/// `rhs` originates from a `NonZero<felt252>`, so it is guaranteed nonzero;
139+
/// `field_div` uses the optimized Montgomery inverse.
140+
pub extern "C" fn cairo_native__felt252_div(
141+
dst: &mut [u8; 32],
142+
lhs: &[u8; 32],
143+
rhs: &[u8; 32],
144+
) {
145+
let lhs = Felt::from_bytes_le(lhs);
146+
let rhs = NonZeroFelt::from_felt_unchecked(Felt::from_bytes_le(rhs));
147+
*dst = lhs.field_div(&rhs).to_bytes_le();
148+
}
149+
135150
/// Allocate `size` bytes with `align` alignment from the per-execution arena.
136151
pub unsafe extern "C" fn cairo_native__arena_alloc(size: u64, align: u64) -> *mut u8 {
137152
EXECUTION_ARENA.with(|arena| {

0 commit comments

Comments
 (0)