Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: fast path operations for small non-native values #1326

Merged
merged 14 commits into from
Nov 27, 2024
Merged
24 changes: 12 additions & 12 deletions internal/stats/latest_stats.csv
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,14 @@ pairing_bls12377,bls24_315,plonk,0,0
pairing_bls12377,bls24_317,plonk,0,0
pairing_bls12377,bw6_761,plonk,51280,51280
pairing_bls12377,bw6_633,plonk,0,0
pairing_bls12381,bn254,groth16,1429070,2382640
pairing_bls12381,bn254,groth16,1419904,2366999
pairing_bls12381,bls12_377,groth16,0,0
pairing_bls12381,bls12_381,groth16,0,0
pairing_bls12381,bls24_315,groth16,0,0
pairing_bls12381,bls24_317,groth16,0,0
pairing_bls12381,bw6_761,groth16,0,0
pairing_bls12381,bw6_633,groth16,0,0
pairing_bls12381,bn254,plonk,5629807,5285448
pairing_bls12381,bn254,plonk,5593770,5250897
pairing_bls12381,bls12_377,plonk,0,0
pairing_bls12381,bls12_381,plonk,0,0
pairing_bls12381,bls24_315,plonk,0,0
Expand All @@ -181,70 +181,70 @@ pairing_bls24315,bls24_315,plonk,0,0
pairing_bls24315,bls24_317,plonk,0,0
pairing_bls24315,bw6_761,plonk,0,0
pairing_bls24315,bw6_633,plonk,141249,141249
pairing_bn254,bn254,groth16,969638,1614382
pairing_bn254,bn254,groth16,963003,1603091
pairing_bn254,bls12_377,groth16,0,0
pairing_bn254,bls12_381,groth16,0,0
pairing_bn254,bls24_315,groth16,0,0
pairing_bn254,bls24_317,groth16,0,0
pairing_bn254,bw6_761,groth16,0,0
pairing_bn254,bw6_633,groth16,0,0
pairing_bn254,bn254,plonk,3798583,3560759
pairing_bn254,bn254,plonk,3771397,3534755
pairing_bn254,bls12_377,plonk,0,0
pairing_bn254,bls12_381,plonk,0,0
pairing_bn254,bls24_315,plonk,0,0
pairing_bn254,bls24_317,plonk,0,0
pairing_bn254,bw6_761,plonk,0,0
pairing_bn254,bw6_633,plonk,0,0
pairing_bw6761,bn254,groth16,3014749,4979960
pairing_bw6761,bn254,groth16,2592181,4256159
pairing_bw6761,bls12_377,groth16,0,0
pairing_bw6761,bls12_381,groth16,0,0
pairing_bw6761,bls24_315,groth16,0,0
pairing_bw6761,bls24_317,groth16,0,0
pairing_bw6761,bw6_761,groth16,0,0
pairing_bw6761,bw6_633,groth16,0,0
pairing_bw6761,bn254,plonk,11486969,10777222
pairing_bw6761,bn254,plonk,9920293,9270827
pairing_bw6761,bls12_377,plonk,0,0
pairing_bw6761,bls12_381,plonk,0,0
pairing_bw6761,bls24_315,plonk,0,0
pairing_bw6761,bls24_317,plonk,0,0
pairing_bw6761,bw6_761,plonk,0,0
pairing_bw6761,bw6_633,plonk,0,0
scalar_mul_G1_bn254,bn254,groth16,74345,117078
scalar_mul_G1_bn254,bn254,groth16,69013,108022
scalar_mul_G1_bn254,bls12_377,groth16,0,0
scalar_mul_G1_bn254,bls12_381,groth16,0,0
scalar_mul_G1_bn254,bls24_315,groth16,0,0
scalar_mul_G1_bn254,bls24_317,groth16,0,0
scalar_mul_G1_bn254,bw6_761,groth16,0,0
scalar_mul_G1_bn254,bw6_633,groth16,0,0
scalar_mul_G1_bn254,bn254,plonk,278909,261995
scalar_mul_G1_bn254,bn254,plonk,260289,244439
scalar_mul_G1_bn254,bls12_377,plonk,0,0
scalar_mul_G1_bn254,bls12_381,plonk,0,0
scalar_mul_G1_bn254,bls24_315,plonk,0,0
scalar_mul_G1_bn254,bls24_317,plonk,0,0
scalar_mul_G1_bn254,bw6_761,plonk,0,0
scalar_mul_G1_bn254,bw6_633,plonk,0,0
scalar_mul_P256,bn254,groth16,100828,161106
scalar_mul_P256,bn254,groth16,93170,148354
scalar_mul_P256,bls12_377,groth16,0,0
scalar_mul_P256,bls12_381,groth16,0,0
scalar_mul_P256,bls24_315,groth16,0,0
scalar_mul_P256,bls24_317,groth16,0,0
scalar_mul_P256,bw6_761,groth16,0,0
scalar_mul_P256,bw6_633,groth16,0,0
scalar_mul_P256,bn254,plonk,385060,359805
scalar_mul_P256,bn254,plonk,355345,331788
scalar_mul_P256,bls12_377,plonk,0,0
scalar_mul_P256,bls12_381,plonk,0,0
scalar_mul_P256,bls24_315,plonk,0,0
scalar_mul_P256,bls24_317,plonk,0,0
scalar_mul_P256,bw6_761,plonk,0,0
scalar_mul_P256,bw6_633,plonk,0,0
scalar_mul_secp256k1,bn254,groth16,75154,118312
scalar_mul_secp256k1,bn254,groth16,69860,109339
scalar_mul_secp256k1,bls12_377,groth16,0,0
scalar_mul_secp256k1,bls12_381,groth16,0,0
scalar_mul_secp256k1,bls24_315,groth16,0,0
scalar_mul_secp256k1,bls24_317,groth16,0,0
scalar_mul_secp256k1,bw6_761,groth16,0,0
scalar_mul_secp256k1,bw6_633,groth16,0,0
scalar_mul_secp256k1,bn254,plonk,281870,264753
scalar_mul_secp256k1,bn254,plonk,263180,247131
scalar_mul_secp256k1,bls12_377,plonk,0,0
scalar_mul_secp256k1,bls12_381,plonk,0,0
scalar_mul_secp256k1,bls24_315,plonk,0,0
Expand Down
3 changes: 0 additions & 3 deletions std/internal/limbcomposition/composition.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import (
//
// res = \sum_{i=0}^{len(inputs)} inputs[i] * 2^{nbBits * i}
func Recompose(inputs []*big.Int, nbBits uint, res *big.Int) error {
if len(inputs) == 0 {
return fmt.Errorf("zero length slice input")
}
if res == nil {
return fmt.Errorf("result not initialized")
}
Expand Down
4 changes: 2 additions & 2 deletions std/math/emulated/custommod.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ func (f *Field[T]) ModAdd(a, b *Element[T], modulus *Element[T]) *Element[T] {
for nextOverflow, err = f.addPreCond(a, b); errors.As(err, &target); nextOverflow, err = f.addPreCond(a, b) {
if errors.As(err, &target) {
if !target.reduceRight {
a = f.mulMod(a, f.shortOne(), 0, modulus)
a = f.mulMod(a, f.One(), 0, modulus)
} else {
b = f.mulMod(b, f.shortOne(), 0, modulus)
b = f.mulMod(b, f.One(), 0, modulus)
}
}
}
Expand Down
32 changes: 25 additions & 7 deletions std/math/emulated/element.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,31 @@ type Element[T FieldParams] struct {
evaluation frontend.Variable `gnark:"-"`
}

// ValueOf returns an Element[T] from a constant value.
// The input is converted to *big.Int and decomposed into limbs and packed into new Element[T].
// ValueOf returns an Element[T] from a constant value. This method is used for
// witness assignment. For in-circuit constant assignment use the
// [Field.NewElement] method.
//
// The input is converted into limbs according to the parameters of the field
// and returned as a new [Element[T]]. Note that it returns the value, not a
// reference, which is more convenient for witness assignment.
func ValueOf[T FieldParams](constant interface{}) Element[T] {
// in this method we set the isWitness flag to true, because we do not know
// the width of the input value. Even though it is valid to call this method
// in circuit without reference to `Field`, then the canonical way would be
// to call [Field.NewElement] method (which would set isWitness to false).
if constant == nil {
r := newConstElement[T](0)
r := newConstElement[T](0, true)
return *r
}
r := newConstElement[T](constant)
r := newConstElement[T](constant, true)
return *r
}

// newConstElement is shorthand for initialising new element using NewElement and
// taking pointer to it. We only want to have a public method for initialising
// an element which return a value because the user uses this only for witness
// creation and it mess up schema parsing.
func newConstElement[T FieldParams](v interface{}) *Element[T] {
func newConstElement[T FieldParams](v interface{}, isWitness bool) *Element[T] {
var fp T
// convert to big.Int
bValue := utils.FromInterface(v)
Expand All @@ -68,9 +77,18 @@ func newConstElement[T FieldParams](v interface{}) *Element[T] {
bValue.Mod(&bValue, fp.Modulus())
}

// decompose into limbs
// decompose into limbs. When set with isWitness, then we do not know at
// compile time the width of the input, so we allocate the maximum number of
// limbs. However, in-circuit we already do (we set it from actual
// constant), thus we can allocate the exact number of limbs.
var nbLimbs int
if isWitness {
nbLimbs = int(fp.NbLimbs())
} else {
nbLimbs = (bValue.BitLen() + int(fp.BitsPerLimb()) - 1) / int(fp.BitsPerLimb())
}
// TODO @gbotrel use big.Int pool here
blimbs := make([]*big.Int, fp.NbLimbs())
blimbs := make([]*big.Int, nbLimbs)
for i := range blimbs {
blimbs[i] = new(big.Int)
}
Expand Down
41 changes: 38 additions & 3 deletions std/math/emulated/emparams/emparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ func (twelveLimbPrimeField) NbLimbs() uint { return 12 }
func (twelveLimbPrimeField) BitsPerLimb() uint { return 64 }
func (twelveLimbPrimeField) IsPrime() bool { return true }

type oneLimbPrimeField struct{}

func (oneLimbPrimeField) NbLimbs() uint { return 1 }
func (oneLimbPrimeField) IsPrime() bool { return true }

// Goldilocks provides type parametrization for field emulation:
// - limbs: 1
// - limb width: 64 bits
Expand All @@ -51,11 +56,9 @@ func (twelveLimbPrimeField) IsPrime() bool { return true }
//
// 0xffffffff00000001 (base 16)
// 18446744069414584321 (base 10)
type Goldilocks struct{}
type Goldilocks struct{ oneLimbPrimeField }

func (fp Goldilocks) NbLimbs() uint { return 1 }
func (fp Goldilocks) BitsPerLimb() uint { return 64 }
func (fp Goldilocks) IsPrime() bool { return true }
func (fp Goldilocks) Modulus() *big.Int { return goldilocks.Modulus() }

// Secp256k1Fp provides type parametrization for field emulation:
Expand Down Expand Up @@ -366,3 +369,35 @@ func (Mod1e256) Modulus() *big.Int {
val, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16)
return val
}

// BabyBear provides type parametrization for field emulation:
// - limbs: 1
// - limb width: 31 bits
//
// The prime modulus for type parametrisation is:
//
// 15*2^27+1
// 0x78000001 (base 16)
// 2013265921 (base 10)
//
// The field has 2-adicity of 27.
type BabyBear struct{ oneLimbPrimeField }

func (BabyBear) BitsPerLimb() uint { return 31 }
func (BabyBear) Modulus() *big.Int { return big.NewInt(2013265921) }

// KoalaBear provides type parametrization for field emulation:
// - limbs: 1
// - limb width: 31 bits
//
// The prime modulus for type parametrisation is:
//
// 2^31-2^24+1
// 0x7f000001 (base 16)
// 2130706433 (base 10)
//
// The field has 2-adicity of 24.
type KoalaBear struct{ oneLimbPrimeField }

func (KoalaBear) BitsPerLimb() uint { return 31 }
func (KoalaBear) Modulus() *big.Int { return big.NewInt(2130706433) }
23 changes: 9 additions & 14 deletions std/math/emulated/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,46 +129,41 @@ func (f *Field[T]) NewElement(v interface{}) *Element[T] {
if e, ok := v.([]frontend.Variable); ok {
return f.packLimbs(e, true)
}
c := ValueOf[T](v)
return &c
// the input was not a variable, so it must be a constant. Create a new
// element from it while setting isWitness flag to false. This ensures that
// we use the minimal number of limbs necessary.
c := newConstElement[T](v, false)
return c
}

// Zero returns zero as a constant.
func (f *Field[T]) Zero() *Element[T] {
f.zeroConstOnce.Do(func() {
f.zeroConst = newConstElement[T](0)
f.zeroConst = f.newInternalElement([]frontend.Variable{}, 0)
})
return f.zeroConst
}

// One returns one as a constant.
func (f *Field[T]) One() *Element[T] {
f.oneConstOnce.Do(func() {
f.oneConst = newConstElement[T](1)
f.oneConst = f.newInternalElement([]frontend.Variable{1}, 0)
})
return f.oneConst
}

// shortOne returns one as a constant stored in a single limb.
func (f *Field[T]) shortOne() *Element[T] {
f.shortOneConstOnce.Do(func() {
f.shortOneConst = f.newInternalElement([]frontend.Variable{1}, 0)
})
return f.shortOneConst
}

// Modulus returns the modulus of the emulated ring as a constant.
func (f *Field[T]) Modulus() *Element[T] {
f.nConstOnce.Do(func() {
f.nConst = newConstElement[T](f.fParams.Modulus())
f.nConst = newConstElement[T](f.fParams.Modulus(), false)
})
return f.nConst
}

// modulusPrev returns modulus-1 as a constant.
func (f *Field[T]) modulusPrev() *Element[T] {
f.nprevConstOnce.Do(func() {
f.nprevConst = newConstElement[T](new(big.Int).Sub(f.fParams.Modulus(), big.NewInt(1)))
f.nprevConst = newConstElement[T](new(big.Int).Sub(f.fParams.Modulus(), big.NewInt(1)), false)
})
return f.nprevConst
}
Expand Down
12 changes: 10 additions & 2 deletions std/math/emulated/field_mul.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ func (mc *mulCheck[T]) cleanEvaluations() {
// mulMod returns a*b mod r. In practice it computes the result using a hint and
// defers the actual multiplication check.
func (f *Field[T]) mulMod(a, b *Element[T], _ uint, p *Element[T]) *Element[T] {
// fast path - if one of the inputs is on zero limbs (it is zero), then the result is also zero
if len(a.Limbs) == 0 || len(b.Limbs) == 0 {
return f.Zero()
}
f.enforceWidthConditional(a)
f.enforceWidthConditional(b)
f.enforceWidthConditional(p)
Expand All @@ -214,11 +218,15 @@ func (f *Field[T]) mulMod(a, b *Element[T], _ uint, p *Element[T]) *Element[T] {

// checkZero creates multiplication check a * 1 = 0 + k*p.
func (f *Field[T]) checkZero(a *Element[T], p *Element[T]) {
// fast path - the result is on zero limbs. This means that it is constant zero
if len(a.Limbs) == 0 {
return
}
// the method works similarly to mulMod, but we know that we are multiplying
// by one and expected result should be zero.
f.enforceWidthConditional(a)
f.enforceWidthConditional(p)
b := f.shortOne()
b := f.One()
k, r, c, err := f.callMulHint(a, b, false, p)
if err != nil {
panic(err)
Expand Down Expand Up @@ -485,7 +493,7 @@ func (f *Field[T]) MulConst(a *Element[T], c *big.Int) *Element[T] {
func(a, _ *Element[T], u uint) *Element[T] {
if ba, aConst := f.constantValue(a); aConst {
ba.Mul(ba, c)
return newConstElement[T](ba)
return newConstElement[T](ba, false)
}
limbs := make([]frontend.Variable, len(a.Limbs))
for i := range a.Limbs {
Expand Down
6 changes: 3 additions & 3 deletions std/math/emulated/field_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func (f *Field[T]) add(a, b *Element[T], nextOverflow uint) *Element[T] {
bb, bConst := f.constantValue(b)
if aConst && bConst {
ba.Add(ba, bb).Mod(ba, f.fParams.Modulus())
return newConstElement[T](ba)
return newConstElement[T](ba, false)
}

nbLimbs := max(len(a.Limbs), len(b.Limbs))
Expand Down Expand Up @@ -184,13 +184,13 @@ func (f *Field[T]) sub(a, b *Element[T], nextOverflow uint) *Element[T] {
bb, bConst := f.constantValue(b)
if aConst && bConst {
ba.Sub(ba, bb).Mod(ba, f.fParams.Modulus())
return newConstElement[T](ba)
return newConstElement[T](ba, false)
}

// first we have to compute padding to ensure that the subtraction does not
// underflow.
var fp T
nbLimbs := max(len(a.Limbs), len(b.Limbs))
nbLimbs := max(len(a.Limbs), len(b.Limbs), int(fp.NbLimbs()))
limbs := make([]frontend.Variable, nbLimbs)
padLimbs := subPadding(fp.Modulus(), fp.BitsPerLimb(), b.overflow, uint(nbLimbs))
for i := range limbs {
Expand Down
3 changes: 3 additions & 0 deletions std/math/emulated/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
// - [P256Fp] and [P256Fr]
// - [P384Fp] and [P384Fr]
// - [STARKCurveFp] and [STARKCurveFr]
// - [BabyBear] and [KoalaBear]
type FieldParams interface {
NbLimbs() uint // number of limbs to represent field element
BitsPerLimb() uint // number of bits per limb. Top limb may contain less than limbSize bits.
Expand All @@ -42,4 +43,6 @@ type (
BW6761Fr = emparams.BW6761Fr
STARKCurveFp = emparams.STARKCurveFp
STARKCurveFr = emparams.STARKCurveFr
BabyBear = emparams.BabyBear
KoalaBear = emparams.KoalaBear
)
6 changes: 6 additions & 0 deletions std/math/emulated/subtraction_padding.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ func subPadding(modulus *big.Int, bitsPerLimbs uint, overflow uint, nbLimbs uint
if modulus.Cmp(big.NewInt(0)) == 0 {
panic("modulus is zero")
}
// we ensure that the number of padding limbs is sufficient to represent the
// modulus
requiredLimbs := (uint(modulus.BitLen()) + bitsPerLimbs - 1) / bitsPerLimbs
if nbLimbs < requiredLimbs {
nbLimbs = requiredLimbs
}
// first, we build a number nLimbs, such that nLimbs > b;
// here b is defined by its bounds, that is b is an element with nbLimbs of (bitsPerLimbs+overflow)
// so a number nLimbs > b, is simply taking the next power of 2 over this bound .
Expand Down