perf: inline fe_mul into point operations — 10% faster verify

Move fe_mul/fe_sqr to static inline in field.h when ASM is available
(FE_MUL_ASM=1). This allows the compiler to inline the entire field
multiply directly into gej_double and gej_add_ge, eliminating function
call boundaries.

Before: gej_double had 9 function calls (to fe_mul/fe_sqr)
After:  gej_double has 2 function calls (fe_half only)

The compiler can now:
- Keep intermediate results in registers across multiply boundaries
- Schedule MULX instructions across adjacent field operations
- Eliminate push/pop register saves at call boundaries

gej_double: 738 → 1311 instructions (larger but no call overhead)

Impact:
  verifyFast: 35.1µs → 31.6µs (10% faster, 1.19x vs ACINQ)
  verify:     39.7µs → 38.4µs (0.98x vs ACINQ — essentially tied!)
  sign:       15.2µs → 14.2µs (1.48x vs ACINQ)
  batch(200): 6.2µs → 4.5µs per event (7.9x vs ACINQ)

https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY
This commit is contained in:
Claude
2026-04-12 03:02:52 +00:00
parent 854bf9379a
commit 3ae1fb36d2
2 changed files with 25 additions and 5 deletions
+8 -5
View File
@@ -125,11 +125,13 @@ void reduce_wide(secp256k1_fe *r, const uint64_t w[8]) {
* Only neg/half/isZero/cmp/toBytes need explicit normalize. */
}
void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
#if FE_MUL_ASM
fe_mul_asm(r, a, b);
return;
#elif HAVE_INT128
/* fe_mul and fe_sqr are static inline in field.h when ASM is available.
* They get inlined directly into gej_double/gej_add_ge callers,
* eliminating ~9 function call boundaries per doublePoint. */
#else
void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
#if HAVE_INT128
/* Inline mul + reduce to avoid function call overhead and enable
* the compiler to keep intermediates in registers. */
uint64_t a0=a->d[0], a1=a->d[1], a2=a->d[2], a3=a->d[3];
@@ -209,8 +211,9 @@ void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) {
fe_mul(r, a, a);
}
#endif /* !FE_MUL_ASM */
#else /* Portable fallback */
#else /* Portable fallback (no HAVE_INT128) */
static inline void mul64(uint64_t *hi, uint64_t *lo, uint64_t a, uint64_t b) {
uint64_t a_lo = a & 0xFFFFFFFF, a_hi = a >> 32;
+17
View File
@@ -152,8 +152,25 @@ static inline void fe_negate(secp256k1_fe *r, const secp256k1_fe *a, int m) {
/* ==================== Function declarations ==================== */
/* Field multiply and square — declared here, defined in field.c.
* On platforms with ASM (x86_64 MULX, ARM64 CE), fe_mul dispatches
* to the inline fe_mul_asm which the compiler can inline into callers
* within the same compilation unit. For cross-unit inlining (point.c
* calling fe_mul), we rely on LTO or the static inline below. */
#include "field_asm.h"
#if FE_MUL_ASM
/* Use the ASM version directly as static inline so point.c can inline it */
static inline void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b) {
fe_mul_asm(r, a, b);
}
static inline void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a) {
fe_mul_asm(r, a, a);
}
#else
void fe_mul(secp256k1_fe *r, const secp256k1_fe *a, const secp256k1_fe *b);
void fe_sqr(secp256k1_fe *r, const secp256k1_fe *a);
#endif
void fe_inv(secp256k1_fe *r, const secp256k1_fe *a);
int fe_sqrt(secp256k1_fe *r, const secp256k1_fe *a);
void fe_half(secp256k1_fe *r, const secp256k1_fe *a);