What Actually Runs When You Call Math.sin(). Java source, fdlibm, and what HotSpot does instead

Not every operation in a Java program has a corresponding machine instruction. Some operations can be lowered directly to hardware, while others require a software algorithm. For library methods, the API contract determines which results are acceptable, and that contract in turn determines which implementation substitutions the runtime may perform.

1.2.1. Machine Support and Software Algorithms

Programmers are used to thinking of arithmetic operations as processor operations. For basic floating-point arithmetic, that model largely holds: addition, subtraction, multiplication, and division map directly to machine instructions. Square root is the clearest further example.

Transcendental functions break the model. Sine, cosine, exponential, logarithm, and general power are normally computed by software algorithms assembled from ordinary arithmetic. Those algorithms perform argument reduction, evaluate polynomial or table approximations, reconstruct the result, and handle special values such as NaN, infinities, and signed zero.

Historical Exception: x87

x86 still includes historical x87 instructions for transcendental functions, among them FSINFCOSFPTAN, and the logarithm instruction FYL2X. Modern compilers and math libraries rarely use them.

The x87 instruction set relies on an outdated stack-based register model and processes one value at a time. Its transcendental instructions also have limited accuracy. FSIN, for example, reduces its argument using a 66-bit approximation of π, so its precision deteriorates for large arguments and for values close to a multiple of π.

Current implementations therefore use software algorithms built from ordinary floating-point instructions rather than the x87 transcendental instructions.

On current instruction sets, the operations relevant here fall into three groups.

Table 2. Machine support for different operations
Operation Typical machine support How the result is obtained

+-*/ on float and double

Direct

One instruction per operation, with the rounding required by Java

Square root (sqrt)

Direct

One correctly rounded square-root instruction

Fused multiply-add (fma)

Architecture-dependent

One instruction with a single final rounding where the processor provides FMA; otherwise a software fallback

Rounding operations (rintfloorceil)

Architecture-dependent

Typically lowered to one or a small number of instructions on current instruction sets

Exponential and logarithmic functions (exploglog10)

No generally used direct instruction

Argument reduction followed by polynomial or table approximation

General power (pow)

No generally used direct instruction

Special-case handling followed by logarithm and exponential computation

Sine, cosine, and tangent (sincostan)

No generally used direct instruction

Argument reduction followed by polynomial approximation

Inverse trigonometric functions (asinacosatanatan2)

No generally used direct instruction

Software algorithm throughout

Hyperbolic functions (sinhcoshtanh)

No generally used direct instruction

Software algorithms built largely on the exponential function

Cube root and hypotenuse (cbrthypot)

No generally used direct instruction

Software algorithms; hypot(…​) scales its arguments to avoid intermediate overflow

The processor is not short of specialized instructions; they simply apply to different operations. Methods such as Integer.bitCount(…​)numberOfLeadingZeros(…​), and reverseBytes(…​) can each lower to a single instruction, while Math.sin(…​) remains a library operation.

That difference has a cost. A floating-point multiplication takes only a few cycles, whereas a transcendental function usually requires many instructions. Math.sin(…​) becomes more expensive still when a large argument requires the slow path of argument reduction. A machine instruction is therefore only one possible implementation mechanism. Where no suitable instruction exists, an implementation must provide an algorithm. Where an instruction does exist, the runtime may still need a fallback for platforms that do not provide it. The cost of software algorithms also gives the runtime an incentive to replace a default implementation with a faster one, but only as far as the contract of the method allows.

1.2.2. Contracts: Math and StrictMath

Machine support does not determine the API contract. For the numerical library methods discussed here, Math and StrictMath define two different contractual models. Java once distinguished between strict and non-strict evaluation of floating-point expressions. JEP 306[1] removed that distinction in Java 17. Floating-point expressions are now always evaluated strictly, and the strictfp modifier has become a no-op for which javac issues a warning.

This change did not make Math and StrictMath equivalent. The two mechanisms address different concerns:

  • strictfp governed the evaluation of floating-point expressions.

  • StrictMath governs the results returned by selected library methods.

1.2.2.1. StrictMath: A Specified Result

For a defined set of functions, StrictMath requires the result produced by the specified fdlibm algorithm when executed according to Java’s arithmetic rules. The name fdlibm stands for Freely Distributable Math Library, a collection of algorithms for IEEE 754 floating-point arithmetic developed at Sun in the 1990s.

For the affected StrictMath methods, the relationship to fdlibm is not an OpenJDK implementation choice. The class documentation says so outright, and pins the binding down in remarkable detail, right down to the file naming convention of the C sources.

To help ensure portability of Java programs, the definitions of some of the numeric functions in this package require that they produce the same results as certain published algorithms. These algorithms are available from the well-known network library netlib as the package „Freely Distributable Math Library,“ fdlibm. These algorithms, which are written in the C programming language, are then to be understood to be transliterated into Java and executed with all floating-point and integer operations following the rules of Java arithmetic. The following transformations are used in the transliteration: […]

The Java math library is defined with respect to fdlibm version 5.3 with a fix to pow so that its error bounds conform to the quality of implementation criteria for pow. Where fdlibm provides more than one definition for a function (such as acos), use the „IEEE 754 core function“ version (residing in a file whose name begins with the letter e). The methods which require fdlibm semantics are sincostanasinacosatanexploglog10cbrtatan2powsinhcoshtanhasinhacoshatanhhypotexpm1, and log1p.

— Java SE 27 and JDK 27 Early-Access API Specificationjava.lang.StrictMath

The API specification is one of the three documents that make up the Java SE platform specification, alongside the language specification and the virtual machine specification. The fdlibm requirement therefore binds every conforming implementation.

What is required is a result, not a construction. The documentation describes the C algorithms as transliterated into Java and executed under Java’s arithmetic rules. This constrains what leaves the method without prescribing how a particular implementation must be organized internally. OpenJDK implements this contract through Java ports in the package-private java.lang.FdLibm class. A call such as StrictMath.sin(…​) therefore follows this default source-level path:

public static double sin(double a) {
    return FdLibm.Sin.compute(a);
}

History

In older JDK releases, several StrictMath methods were declared native and eventually called fdlibm routines written in C. The migration away from the native implementation happened gradually, with individual algorithms translated from C to Java over several releases. The transcendental functions followed in JDK 21. The class documentation tracks the change: where it once described the C algorithms as executed under Java’s floating-point rules, it now describes them as transliterated into Java.

The Java ports remain closely based on fdlibm 5.3. The original library is no longer maintained as an independent project, but descendants of its code survive in FreeBSD’s msun, musl libc, Julia’s openlibm, and the Go standard library.

A virtual machine may compile or inline this code, but it may not substitute an algorithm that produces a different result.

StrictMath therefore guarantees reproducibility of the specified result. It does not necessarily guarantee a result that is closer to the exact mathematical value than the result returned by Math.

1.2.2.2. Math: Constrained Implementation Freedom

For many transcendental functions, Math specifies an error bound and, where applicable, monotonicity requirements. It does not require the fdlibm result. IEEE 754-2019 defines functions such as sine, cosine, exponential, logarithm, and power as recommended operations with correctly rounded semantics, but it does not require every programming environment to provide them. Java generally specifies weaker, method-specific guarantees for these functions.[2]

The Java source implementation commonly delegates to StrictMath:

@IntrinsicCandidate
public static double sin(double a) {
    return StrictMath.sin(a);
}

This delegation defines the default implementation. It does not restrict the runtime to that implementation. A virtual machine may substitute another algorithm whenever the replacement continues to satisfy the Math contract.

Square root is the important exception. IEEE 754 requires square root to be correctly rounded. For a given input and rounding mode, there is exactly one admissible floating-point result. Java reflects that requirement in the contract of Math.sqrt(…​): the result must be correctly rounded rather than merely remain within an ulp bound. Math.sqrt(…​) and StrictMath.sqrt(…​) therefore return the same result. The documented list of methods bound to fdlibm semantics shows the same fact from the other side: sqrt does not appear on it.

For functions such as sin(…​)cos(…​), and pow(…​), no comparable requirement eliminates the implementation freedom.

1.2.3. Runtime Implementation

A Java method body describes the default source-level path, but not necessarily the code executed after compilation. As discussed earlier, HotSpot may recognize selected methods as intrinsics and replace their Java-level invocation during JIT compilation. The source code extract above shows that Math.sin(…​) is marked with @IntrinsicCandidate; whether HotSpot actually provides an intrinsic for the method is determined by its internal table of known intrinsics. For a supported call, the JIT compiler may emit compiler-generated code or invoke a runtime stub instead of following the Java-level path.

HotSpot provides an intrinsic for Math.sin(…​), but not for StrictMath.sin(…​). For Math.sin(…​), HotSpot may therefore replace the default delegation path with a runtime stub that implements the function using platform-specific instruction sequences. On x86-64, the relevant stubs are based on routines contributed from Intel’s scalar Compiler Math Library (LIBM)[3] and implement the function using SSE and AVX instruction sequences. The implementation is incorporated directly into HotSpot; the JVM does not dynamically link against the external Intel library at runtime. This substitution is permitted because Math.sin(…​) does not require the fdlibm result. It requires only a result within the specified error bound and with the required monotonicity properties.

A direct call to StrictMath.sin(…​), by contrast, remains bound to the specified result. The JIT compiler may compile and inline FdLibm.Sin.compute(…​), but it may not substitute an algorithm that changes that result.

Square root illustrates how the contract distinction remains separate from the hardware/software distinction. Both Math.sqrt(…​) and StrictMath.sqrt(…​) carry @IntrinsicCandidate, and HotSpot defines an intrinsic for each. A hardware square-root instruction and a conforming software implementation must agree bit for bit, so a single sqrtsd instruction can replace either method body without weakening either contract.

An intrinsic is therefore not hardware taking over from software. It is one implementation replacing another under the constraints of the API contract. The complete path is therefore:

operation → API contract → permitted implementation → generated machine code

Choosing Between Math and StrictMath

Math is the default choice. It gives the runtime freedom to select an implementation that satisfies the documented error bounds while taking advantage of platform-specific optimizations.

For methods with specified fdlibm semantics, StrictMath is appropriate when results must be reproducible bit for bit across machines, virtual machines, or vendors. Typical cases include deterministic simulations, distributed computations whose nodes must produce identical values, and golden-value tests that run on multiple platforms.

What separates the two classes is implementation freedom on one side and a specified numerical result on the other, not speed on one side and accuracy on the other.

Ähnliche Beiträge

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert