From 4757922e9ae7bcaee52decb3c18f59ec8c4b7739 Mon Sep 17 00:00:00 2001 From: TomerStarkware Date: Tue, 30 Jun 2026 12:12:23 +0300 Subject: [PATCH] perf(felt252): compute felt252_div via a runtime binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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, 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) --- src/libfuncs/felt252.rs | 66 ++++++++++++++------------------ src/metadata/runtime_bindings.rs | 32 ++++++++++++++-- src/runtime.rs | 56 ++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 41 deletions(-) diff --git a/src/libfuncs/felt252.rs b/src/libfuncs/felt252.rs index 8a7944766..612dc3456 100644 --- a/src/libfuncs/felt252.rs +++ b/src/libfuncs/felt252.rs @@ -3,10 +3,7 @@ use super::LibfuncHelper; use crate::{ error::{panic::ToNativeAssertError, Result}, - metadata::{ - runtime_bindings::{ExtendedEuclideanWidth, RuntimeBindingsMeta}, - MetadataStorage, - }, + metadata::{runtime_bindings::RuntimeBindingsMeta, MetadataStorage}, utils::{felt_to_unsigned, get_integer_layout, ProgramRegistryExt, PRIME}, }; use cairo_lang_sierra::{ @@ -72,7 +69,6 @@ pub fn build_binary_operation<'ctx, 'this>( &info.branch_signatures()[0].vars[0].ty, )?; let i256 = IntegerType::new(context, 256).into(); - let i512 = IntegerType::new(context, 512).into(); let (op, lhs, rhs) = match info { Felt252BinaryOperationConcrete::WithVar(operation) => { @@ -166,49 +162,45 @@ pub fn build_binary_operation<'ctx, 'this>( entry.trunci(result, felt252_ty, location)? } Felt252BinaryOperator::Div => { + // The field division (a modular inverse followed by a multiply) is + // delegated to a runtime binding. Lowering it inline produces an + // extended-euclidean loop plus an i512 multiply and remainder that + // the LLVM backend legalizes into huge sequences of limb + // operations, making codegen of division-heavy contracts + // pathologically slow. + let layout_i256 = get_integer_layout(256); + let lhs_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + let rhs_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + let dst_ptr = + helper + .init_block() + .alloca1(context, location, i256, layout_i256.align())?; + + let lhs_i256 = entry.extui(lhs, i256, location)?; + let rhs_i256 = entry.extui(rhs, i256, location)?; + entry.store(context, location, lhs_ptr, lhs_i256)?; + entry.store(context, location, rhs_ptr, rhs_i256)?; + let runtime_bindings_meta = metadata .get_mut::() .to_native_assert_error( "Unable to get the RuntimeBindingsMeta from MetadataStorage", )?; - - let prime = entry.const_int_from_type(context, location, PRIME.clone(), felt252_ty)?; - - // Find 1 / rhs. - let euclidean_result = runtime_bindings_meta.extended_euclidean_algorithm( + runtime_bindings_meta.felt252_div( context, helper.module, entry, + [dst_ptr, lhs_ptr, rhs_ptr], location, - [rhs, prime], - ExtendedEuclideanWidth::U252, )?; - // Here we omit checking if inverse is actually the inverse, - // satisfying gcd(a,b) == 1, because we are using a prime as the - // modulus. This ensures that for any value of a, included in the - // field, gcd(a,b) == 1. - let prime = entry.const_int_from_type(context, location, PRIME.clone(), i512)?; - let inverse = { - let inverse = - entry.extract_value(context, location, euclidean_result, felt252_ty, 1)?; - entry.extui(inverse, i512, location)? - }; - - // Peform lhs * (1 / rhs) - let lhs = entry.extui(lhs, i512, location)?; - let result = entry.muli(lhs, inverse, location)?; - // Apply modulo and convert result to felt252 - let result_mod = entry.append_op_result(arith::remui(result, prime, location))?; - let is_out_of_range = - entry.cmpi(context, CmpiPredicate::Uge, result, prime, location)?; - let result = entry.append_op_result(arith::select( - is_out_of_range, - result_mod, - result, - location, - ))?; - + let result = entry.load(context, location, dst_ptr, i256)?; entry.trunci(result, felt252_ty, location)? } }; diff --git a/src/metadata/runtime_bindings.rs b/src/metadata/runtime_bindings.rs index 09d619fa3..742fe9780 100644 --- a/src/metadata/runtime_bindings.rs +++ b/src/metadata/runtime_bindings.rs @@ -54,6 +54,7 @@ enum RuntimeBinding { U128SquareRoot, U256SquareRoot, Felt252Mul, + Felt252Div, CircuitArithOperation, DictIntoEntries, QM31Add, @@ -89,6 +90,7 @@ impl RuntimeBinding { Self::U128SquareRoot => "cairo_native__u128_square_root", Self::U256SquareRoot => "cairo_native__u256_square_root", Self::Felt252Mul => "cairo_native__felt252_mul", + Self::Felt252Div => "cairo_native__felt252_div", Self::CircuitArithOperation => "cairo_native__circuit_arith_operation", Self::DictIntoEntries => "cairo_native__dict_into_entries", Self::QM31Add => "cairo_native__libfunc__qm31__qm31_add", @@ -137,6 +139,7 @@ impl RuntimeBinding { Self::U128SquareRoot => cairo_native__u128_square_root as *const (), Self::U256SquareRoot => cairo_native__u256_square_root as *const (), Self::Felt252Mul => cairo_native__felt252_mul as *const (), + Self::Felt252Div => cairo_native__felt252_div as *const (), Self::ArenaAlloc => cairo_native__arena_alloc as *const (), Self::ExtendedEuclideanAlgorithm(_) | Self::CircuitArithOperation => return None, #[cfg(feature = "with-cheatcode")] @@ -150,7 +153,6 @@ impl RuntimeBinding { #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] pub enum ExtendedEuclideanWidth { U31, - U252, U256, U384, } @@ -159,7 +161,6 @@ impl ExtendedEuclideanWidth { const fn symbol(self) -> &'static str { match self { Self::U31 => "cairo_native__u31_extended_euclidean_algorithm", - Self::U252 => "cairo_native__u252_extended_euclidean_algorithm", Self::U256 => "cairo_native__u256_extended_euclidean_algorithm", Self::U384 => "cairo_native__u384_extended_euclidean_algorithm", } @@ -168,7 +169,6 @@ impl ExtendedEuclideanWidth { const fn bits(self) -> u32 { match self { Self::U31 => 31, - Self::U252 => 252, Self::U256 => 256, Self::U384 => 384, } @@ -380,6 +380,31 @@ impl RuntimeBindingsMeta { )) } + /// Register if necessary, then invoke `cairo_native__felt252_div`, which + /// computes `(lhs / rhs) mod STARK_PRIME`. The pointers reference 32-byte + /// little-endian felt252 buffers; the result is written to `dst_ptr`. + pub fn felt252_div<'c, 'a>( + &mut self, + context: &'c Context, + module: &Module, + block: &'a Block<'c>, + [dst_ptr, lhs_ptr, rhs_ptr]: [Value<'c, '_>; 3], + location: Location<'c>, + ) -> Result> + where + 'c: 'a, + { + let function = + self.build_function(context, module, block, location, RuntimeBinding::Felt252Div)?; + + Ok(block.append_operation( + OperationBuilder::new("llvm.call", location) + .add_operands(&[function]) + .add_operands(&[dst_ptr, lhs_ptr, rhs_ptr]) + .build()?, + )) + } + /// Builds, if necessary, the circuit operation function, used to perform /// circuit arithmetic operations. /// @@ -1005,6 +1030,7 @@ pub fn setup_runtime(find_symbol_ptr: impl Fn(&str) -> Option<*mut c_void>) { RuntimeBinding::U128SquareRoot, RuntimeBinding::U256SquareRoot, RuntimeBinding::Felt252Mul, + RuntimeBinding::Felt252Div, #[cfg(feature = "with-cheatcode")] RuntimeBinding::VtableCheatcode, ] { diff --git a/src/runtime.rs b/src/runtime.rs index 72adbf212..9388a88b2 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -16,7 +16,7 @@ use num_traits::{ToPrimitive, Zero}; use starknet_curve::curve_params::BETA; use starknet_types_core::{ curve::{AffinePoint, ProjectivePoint}, - felt::Felt, + felt::{Felt, NonZeroFelt}, hash::StarkHash, qm31::QM31, }; @@ -104,6 +104,28 @@ pub extern "C" fn cairo_native__felt252_mul(dst: &mut [u8; 32], lhs: &[u8; 32], *dst = felt_raw_to_le_bytes(&product); } +/// Compute `lhs / rhs` in the STARK field, i.e. `lhs * rhs⁻¹ mod STARK_PRIME`, +/// and store the canonical little-endian result in `dst`. +/// +/// Mirrors the raw-representation trick in [`cairo_native__felt252_mul`]: the +/// `from_raw`/`to_raw` conversions are free, so only `rhs` pays a Montgomery +/// multiplication. `field_div` flips `rhs`'s exponent, so the `R` factors land +/// exactly as in the multiply case: +/// - `from_raw(canonical(a))` has value `a * R⁻¹` (free), +/// - dividing by `from_bytes_le(b)` (value `b`) gives value `a * b⁻¹ * R⁻¹`, +/// - whose raw representation is exactly the canonical `a / b`, read by `to_raw`. +/// +/// `rhs` originates from a `NonZero`, so it is guaranteed nonzero and +/// `field_div`'s Montgomery inverse never fails. +pub extern "C" fn cairo_native__felt252_div(dst: &mut [u8; 32], lhs: &[u8; 32], rhs: &[u8; 32]) { + // value = a * R⁻¹ (free: reinterpret canonical bytes as raw limbs). + let lhs = felt_from_raw_le_bytes(lhs); + // value = b (one Montgomery multiplication). + let rhs = NonZeroFelt::from_felt_unchecked(Felt::from_bytes_le(rhs)); + // value = a * b⁻¹ * R⁻¹, so its raw representation is the canonical `a / b`. + *dst = felt_raw_to_le_bytes(&lhs.field_div(&rhs)); +} + /// Build a `Felt` by interpreting a 32-byte little-endian buffer as the felt's /// raw internal (Montgomery) representation. The inverse of /// [`felt_raw_to_le_bytes`]. Zero-cost: no canonical<->Montgomery conversion. @@ -1056,6 +1078,38 @@ mod tests { } } + /// The raw-representation `cairo_native__felt252_div` must equal the + /// canonical field quotient for every nonzero divisor. + #[test] + fn felt252_div_matches_field_quotient() { + let cases = [ + Felt::ZERO, + Felt::ONE, + Felt::THREE, + Felt::from(-1), + Felt::from(-2), + Felt::from(1234567890123456789u64), + Felt::from_hex_unchecked( + "0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0", + ), + Felt::MAX, + ]; + for &a in &cases { + for &b in &cases { + if b == Felt::ZERO { + continue; + } + let mut dst = [0u8; 32]; + cairo_native__felt252_div(&mut dst, &a.to_bytes_le(), &b.to_bytes_le()); + assert_eq!( + Felt::from_bytes_le(&dst), + a.field_div(&NonZeroFelt::from_felt_unchecked(b)), + "felt252_div({a:#x}, {b:#x}) gave the wrong quotient" + ); + } + } + } + pub fn felt252_short_str(value: &str) -> Felt { let values: Vec<_> = value .chars()