Skip to content

Commit aa56499

Browse files
perf(felt252): compute felt252_div via a runtime binding (#1648)
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 289c67c commit aa56499

3 files changed

Lines changed: 113 additions & 41 deletions

File tree

src/libfuncs/felt252.rs

Lines changed: 29 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,45 @@ 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 =
173+
helper
174+
.init_block()
175+
.alloca1(context, location, i256, layout_i256.align())?;
176+
let rhs_ptr =
177+
helper
178+
.init_block()
179+
.alloca1(context, location, i256, layout_i256.align())?;
180+
let dst_ptr =
181+
helper
182+
.init_block()
183+
.alloca1(context, location, i256, layout_i256.align())?;
184+
185+
let lhs_i256 = entry.extui(lhs, i256, location)?;
186+
let rhs_i256 = entry.extui(rhs, i256, location)?;
187+
entry.store(context, location, lhs_ptr, lhs_i256)?;
188+
entry.store(context, location, rhs_ptr, rhs_i256)?;
189+
169190
let runtime_bindings_meta = metadata
170191
.get_mut::<RuntimeBindingsMeta>()
171192
.to_native_assert_error(
172193
"Unable to get the RuntimeBindingsMeta from MetadataStorage",
173194
)?;
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(
195+
runtime_bindings_meta.felt252_div(
179196
context,
180197
helper.module,
181198
entry,
199+
[dst_ptr, lhs_ptr, rhs_ptr],
182200
location,
183-
[rhs, prime],
184-
ExtendedEuclideanWidth::U252,
185201
)?;
186202

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-
203+
let result = entry.load(context, location, dst_ptr, i256)?;
212204
entry.trunci(result, felt252_ty, location)?
213205
}
214206
};

src/metadata/runtime_bindings.rs

Lines changed: 29 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
}
@@ -380,6 +380,31 @@ impl RuntimeBindingsMeta {
380380
))
381381
}
382382

383+
/// Register if necessary, then invoke `cairo_native__felt252_div`, which
384+
/// computes `(lhs / rhs) mod STARK_PRIME`. The pointers reference 32-byte
385+
/// little-endian felt252 buffers; the result is written to `dst_ptr`.
386+
pub fn felt252_div<'c, 'a>(
387+
&mut self,
388+
context: &'c Context,
389+
module: &Module,
390+
block: &'a Block<'c>,
391+
[dst_ptr, lhs_ptr, rhs_ptr]: [Value<'c, '_>; 3],
392+
location: Location<'c>,
393+
) -> Result<OperationRef<'c, 'a>>
394+
where
395+
'c: 'a,
396+
{
397+
let function =
398+
self.build_function(context, module, block, location, RuntimeBinding::Felt252Div)?;
399+
400+
Ok(block.append_operation(
401+
OperationBuilder::new("llvm.call", location)
402+
.add_operands(&[function])
403+
.add_operands(&[dst_ptr, lhs_ptr, rhs_ptr])
404+
.build()?,
405+
))
406+
}
407+
383408
/// Builds, if necessary, the circuit operation function, used to perform
384409
/// circuit arithmetic operations.
385410
///
@@ -1005,6 +1030,7 @@ pub fn setup_runtime(find_symbol_ptr: impl Fn(&str) -> Option<*mut c_void>) {
10051030
RuntimeBinding::U128SquareRoot,
10061031
RuntimeBinding::U256SquareRoot,
10071032
RuntimeBinding::Felt252Mul,
1033+
RuntimeBinding::Felt252Div,
10081034
#[cfg(feature = "with-cheatcode")]
10091035
RuntimeBinding::VtableCheatcode,
10101036
] {

src/runtime.rs

Lines changed: 55 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
};
@@ -104,6 +104,28 @@ pub extern "C" fn cairo_native__felt252_mul(dst: &mut [u8; 32], lhs: &[u8; 32],
104104
*dst = felt_raw_to_le_bytes(&product);
105105
}
106106

107+
/// Compute `lhs / rhs` in the STARK field, i.e. `lhs * rhs⁻¹ mod STARK_PRIME`,
108+
/// and store the canonical little-endian result in `dst`.
109+
///
110+
/// Mirrors the raw-representation trick in [`cairo_native__felt252_mul`]: the
111+
/// `from_raw`/`to_raw` conversions are free, so only `rhs` pays a Montgomery
112+
/// multiplication. `field_div` flips `rhs`'s exponent, so the `R` factors land
113+
/// exactly as in the multiply case:
114+
/// - `from_raw(canonical(a))` has value `a * R⁻¹` (free),
115+
/// - dividing by `from_bytes_le(b)` (value `b`) gives value `a * b⁻¹ * R⁻¹`,
116+
/// - whose raw representation is exactly the canonical `a / b`, read by `to_raw`.
117+
///
118+
/// `rhs` originates from a `NonZero<felt252>`, so it is guaranteed nonzero and
119+
/// `field_div`'s Montgomery inverse never fails.
120+
pub extern "C" fn cairo_native__felt252_div(dst: &mut [u8; 32], lhs: &[u8; 32], rhs: &[u8; 32]) {
121+
// value = a * R⁻¹ (free: reinterpret canonical bytes as raw limbs).
122+
let lhs = felt_from_raw_le_bytes(lhs);
123+
// value = b (one Montgomery multiplication).
124+
let rhs = NonZeroFelt::from_felt_unchecked(Felt::from_bytes_le(rhs));
125+
// value = a * b⁻¹ * R⁻¹, so its raw representation is the canonical `a / b`.
126+
*dst = felt_raw_to_le_bytes(&lhs.field_div(&rhs));
127+
}
128+
107129
/// Build a `Felt` by interpreting a 32-byte little-endian buffer as the felt's
108130
/// raw internal (Montgomery) representation. The inverse of
109131
/// [`felt_raw_to_le_bytes`]. Zero-cost: no canonical<->Montgomery conversion.
@@ -1056,6 +1078,38 @@ mod tests {
10561078
}
10571079
}
10581080

1081+
/// The raw-representation `cairo_native__felt252_div` must equal the
1082+
/// canonical field quotient for every nonzero divisor.
1083+
#[test]
1084+
fn felt252_div_matches_field_quotient() {
1085+
let cases = [
1086+
Felt::ZERO,
1087+
Felt::ONE,
1088+
Felt::THREE,
1089+
Felt::from(-1),
1090+
Felt::from(-2),
1091+
Felt::from(1234567890123456789u64),
1092+
Felt::from_hex_unchecked(
1093+
"0x4d6e41de886ac83938da3456ccf1481182687989ead34d9d35236f0864575a0",
1094+
),
1095+
Felt::MAX,
1096+
];
1097+
for &a in &cases {
1098+
for &b in &cases {
1099+
if b == Felt::ZERO {
1100+
continue;
1101+
}
1102+
let mut dst = [0u8; 32];
1103+
cairo_native__felt252_div(&mut dst, &a.to_bytes_le(), &b.to_bytes_le());
1104+
assert_eq!(
1105+
Felt::from_bytes_le(&dst),
1106+
a.field_div(&NonZeroFelt::from_felt_unchecked(b)),
1107+
"felt252_div({a:#x}, {b:#x}) gave the wrong quotient"
1108+
);
1109+
}
1110+
}
1111+
}
1112+
10591113
pub fn felt252_short_str(value: &str) -> Felt {
10601114
let values: Vec<_> = value
10611115
.chars()

0 commit comments

Comments
 (0)