Computing exp(x)/2 for Large x Values
Problem Statement
When implementing hyperbolic sine (sinh(x)
) and cosine (cosh(x)
) functions for large values of x
, a critical issue arises: exp(x)
overflows the double
data type range before division by 2 can occur. This happens because:
DBL_MAX
(maximum finitedouble
value) ≈ 1.798 × 10³⁰⁸- Maximum
x
for finiteexp(x)
≈ 709.782 - Maximum
x
for finitesinh(x)
orcosh(x)
≈ 710.475 - For
x >= 20
, both functions are well approximated byexp(x)/2
The challenge is computing exp(x)/2
accurately when x
is between approximately 709.782 and 710.475, where exp(x)
would overflow but exp(x)/2
remains representable.
Solutions
1. Logarithmic Identity Approach (Simple but Inaccurate)
double simple_exp_div_2(double x) {
return exp(x - log(2.0));
}
This method uses the mathematical identity exp(x)/2 = exp(x - log(2))
. While it avoids overflow, it introduces significant error (≈495 ULP) due to precision loss in the subtraction operation.
WARNING
This approach has high error and is not recommended for precision-sensitive applications.
2. Exact Subtraction with Optimized Scaling
The optimal solution uses exact subtraction combined with carefully chosen constants to minimize error:
double precise_exp_div_2(double x) {
// Constants optimized for minimal error
static const double v = 0x1.62e4307c58800p-1; // 0.69314719694034466
static const double scale = 0x1.000000465a709000p+1 / 2; // exp(v)/2
return exp(x - v) * scale;
}
Key insights:
v
is chosen so thatx - v
subtraction is exact (no rounding error)scale
approximatesexp(v)/2
with extremely high precision- The next 11 bits of
exp(v)
beyond double precision are zero, minimizing error - Achieves error of ≈1.00781 ULP
3. Double-Double Decomposition Method
Mark Dickinson's approach uses decomposition with exact operations:
double metd_exp_div_2(double x) {
// Double-double approximation to 1024 * log(2)
static const double c1 = 0x1.62e42fefa39efp+9;
static const double c2 = 0x1.acp-46;
return exp(x - c1 - c2) * 0x1p+1023;
}
Advantages:
- Both subtractions are exact (by Sterbenz's lemma)
- Multiplication by power of two is exact
- Error bounded by ≈0.612 ULP with good
exp()
implementation - Extremely fast execution
4. Fused Multiply-Add Optimization
Building on the exact subtraction method with FMA support:
double fma_exp_div_2(double x) {
static const double v = 0x1.62e42fefa3c00p-1;
static const double scale_minus_1 = 5.870760753453405e-14;
double y = exp(x - v);
return fma(y, scale_minus_1, y);
}
This formulation uses the identity y * scale = y + y * scale_minus_1
to improve accuracy.
5. Simple Squaring Method
For applications where simplicity outweighs precision requirements:
double simple_square_exp_div_2(double x) {
double s = exp(x / 2);
return 0.5 * s * s;
}
Characteristics:
- Error ≈1.9 ULP
- No special constants required
- Good performance
Performance Comparison
The table below shows error measurements for various methods (lower ULP is better):
Method | Max Error (ULP) | Std Dev (ULP) | Time (μs) |
---|---|---|---|
Double-double decomposition | 0.612 | 0.289 | 0.0147 |
Exact subtraction + scaling | 1.007 | 0.408 | 0.0199 |
Simple squaring | 1.918 | 0.751 | 0.0169 |
Logarithmic identity | 495.708 | 71.194 | 0.0197 |
Implementation Recommendations
Best Overall Solution
double optimal_exp_div_2(double x) {
if (x < 710.12928648366403) {
// For x < 710.129, use 1024*log(2) decomposition
static const double c1 = 0x1.62e42fefa39efp+9;
static const double c2 = 0x1.abc9e3b39803fp-46;
return exp(x - c1 - c2) * 0x1p+1023;
} else {
// For larger x, use 1025*log(2) decomposition
static const double c1 = 0x1.633ce8fb9f87ep+9;
static const double c2 = -0x1.3ae594e9bd8bp-45;
return exp(x - c1 - c2) * 0x1p+1023 * 2;
}
}
This hybrid approach provides the best combination of accuracy (0.6 ULP error) and performance.
Simple Yet Effective Alternative
double simple_effective_exp_div_2(double x) {
double s = exp(x / 2);
return 0.5 * s * s;
}
Use this when code simplicity is prioritized and ≈1.9 ULP error is acceptable.
Key Considerations
- Platform Dependence: Results may vary based on
exp()
implementation quality and hardware support - FMA Availability: Use
fma()
when available for improved accuracy - Range Checking: Always validate that
x
is within the appropriate range - Error Analysis: Test implementations with rigorous error measurement
TIP
For most applications, the simple squaring method provides adequate accuracy with excellent simplicity. For scientific computing requiring maximum precision, use the double-double decomposition method.
The techniques presented here demonstrate how careful mathematical analysis and constant optimization can overcome floating-point limitations to compute exp(x)/2
accurately even near the limits of double precision.