Skip to content

square() for arithmetic types: multiply instead of calling std::pow(x, 2) - #3368

Closed
sims1253 wants to merge 1 commit into
stan-dev:developfrom
sims1253:square-pow-to-mul
Closed

square() for arithmetic types: multiply instead of calling std::pow(x, 2)#3368
sims1253 wants to merge 1 commit into
stan-dev:developfrom
sims1253:square-pow-to-mul

Conversation

@sims1253

@sims1253 sims1253 commented Aug 23, 2026

Copy link
Copy Markdown

With default toolchain settings (gcc/clang default to -fmath-errno), std::pow must set errno on domain/range/overflow errors, so the optimizer cannot rewrite it to a multiply in general. glibc's pow is a branchy multi-path implementation (~105 instructions per call measured: 3,473,268 Ir over 33,078 calls) where x * x is one instruction.

On the measured GP regression model, square() accounts for 57 pow calls per gradient and 8.9% of gradient instructions (callgrind): 32,889 of 33,078 executed pow calls come from gp_exp_quad_cov (55 kernel pairs for N = 11, plus square(sigma) and square(l)).

Widen to double first, then multiply. The template is enabled for all arithmetic T; a naive x * x would compute an int product for integral x (overflow for |x| > 46,341 where the promoted path does not) and round twice for float x. Widening first reproduces the previous promote-then-round semantics exactly, minus the libm call

Eval

GP regression model (gp_exp_quad_cov, n = 11 → 55 kernel pairs), matched binaries, identical inputs, gcc 16.2.1, glibc, Zen 3. Medians of 3 interleaved reps; callgrind on a fixed seeded run (warmup 50 / samples 50, 577 gradient calls in both arms):

metric stock patched delta
Ir / gradient (callgrind, deterministic) 66,950 60,864 −9.1%
pow instructions in run 3,473,268 19,923 −99.4% (residual is sampler-side Adam)
µs / logp_grad call (warmup stanza) 6.681 5.820 −12.9%
µs / logp_grad call (sampling stanza) 6.655 5.640 −15.2%

Checklist

  • Copyright holder: Maximilian Scholz
    The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
    - Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
    - Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

  • the basic tests are passing

    • unit tests pass (to run, use: ./runTests.py test/unit)
    • header checks pass, (make test-headers)
    • dependencies checks pass, (make test-math-dependencies)
    • docs build, (make doxygen)
    • code passes the built in C++ standards checks (make cpplint)
  • the code is written in idiomatic C++ and changes are documented in the doxygen

  • the new changes are tested

…(x, 2)

The arithmetic overload of square() calls std::pow(x, 2) even though its
own doc comment says the implementation is 'just x * x'. The pow call
cannot be constant-folded as well as a multiply and shows up in hot
paths: in a GP regression model (gp_exp_quad_cov distances, n=11),
square() accounts for 57 pow calls per gradient and 8.9% of gradient
instructions; replacing them measured -9.1% Ir/gradient and -13/-15%
per-call wall with bit-identical results (glibc).

Widen to double first, then multiply: identical to the previous
std::pow(x, 2) (a correctly-rounded square equals the rounded product)
including the promoted-to-double semantics for integral arguments,
where a raw x * x could overflow, and avoiding double-rounding drift
for float arguments. On libms without a correctly-rounded pow results
may shift by at most 1 ulp.

Same treatment for the two scalar-var squared_distance overloads,
which used std::pow(a - b, 2) for the value and (a - b) again inside
the callback; the difference is now computed once and squared.
@WardBrian

Copy link
Copy Markdown
Member

Please consult the Stan AI policy for contributions at https://github.com/stan-dev/stan/wiki/AI-Contribution-Policy. Fully agentic PRs are not accepted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants