Skip to content

perf(java): rolling min/max family runs 6-9x slower than C on a 100k random walk #415

Description

@mario4tier

Problem

The Java batch tier of the rolling min/max family (MIN, MAX, MINMAX, MIDPOINT, MIDPRICE, WILLR, the #147 Van Herk block scan) runs 6 to 9x slower than C on a 100k-bar random walk. MFI shows the same signature at 3x. Across the corpus Java batch is 1.13x C at the median, so these rows are the tail a Java user would notice, and the only place a JNI wrapper of the C library would beat the pure-Java jar.

Measurements

Host: laptop, AMD Ryzen 7 PRO 8840U (Zen 4), WSL2, OpenJDK 21.0.12. Not yet measured on the Intel devbox. C is the shipped CMake Release libta-lib.so; Java is the public Core tier from dev 745247fe5. One function per process, min of 25 rounds, best of 3 processes, period 14. Both arms produce identical output.

ns/bar:

function C 1k Java 1k Java/C C 100k Java 100k Java/C
MIN 0.74 2.48 3.3 0.79 7.31 9.3
MAX 0.80 2.20 2.8 0.79 7.43 9.4
MINMAX 1.52 4.02 2.6 1.64 14.01 8.6
MIDPOINT 1.52 3.97 2.6 1.59 13.53 8.5
MIDPRICE 1.52 4.06 2.7 1.63 14.03 8.6
WILLR 2.45 4.52 1.8 2.46 15.29 6.2
MFI 3.77 3.82 1.0 3.74 11.39 3.1
SMA (control) 1.27 1.28 1.0 1.34 1.33 1.0
BBANDS (control) 3.27 3.64 1.1 3.38 3.61 1.1

What the numbers say about the cause

  • C's cost per bar is the same at both sizes. Java's triples from 1k to 100k. A 1k series replayed thousands of times lets the branch predictor memorize it; 100k bars cannot be memorized. So the gap is data-dependent control flow in the C2 output, where GCC compiles the same min/max to branch-free code. A benchmark on a short repeated series understates this gap by about 3x.
  • Input shape confirms it (MIN, 100k): Java is 8.7 ns/bar on a random walk and 2.6 to 3.6 on constant or monotone input.
  • Spelling alone does not fix it. a < b ? a : b and -XX:+UseCMoveUnconditionally both measure the same as the current code. Math.min reaches 4.9 to 5.8 ns/bar on a random walk and still gets faster on monotone input (1.9), so C2 is not branch-free there either.
  • Math.min is not a drop-in anyway: it propagates NaN and orders -0.0 below 0.0, where the C min keeps the incumbent. Outputs must stay bit-identical to C.
  • The streaming rescan does not help: Java MinStream.update is 9.1 ns/bar on the same input.
  • A residual ~3x remains even with perfect prediction (2.6 vs 0.8 ns/bar), with an unidentified cause.

Deliverables

  1. The C2 disassembly of MIN_Impl (hsdis) on a random walk, identifying what the hot loops pay for: branches, range checks, or missed vectorization.
  2. A Java emission for the seven functions that gets the 100k random walk within 2x of C (proposed target), bit-identical to C including NaN and signed-zero inputs (--xlang-hash stays bitwise).
  3. No regression at 1k bars or on the constant/monotone shapes.

Not this mechanism

SAR/SAREXT (1.3 to 1.4x) and TRANGE (~1.9x) are slower at both sizes alike, so the size-dependent signature above does not apply. Rows in the corpus-wide ta_bench pass that did not reproduce in isolation: IMI (1.15x), AD (1.0x). DIV, SQRT and TYPPRICE run about 2x faster in Java than the shipped .so.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions