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 finitedoublevalue) ≈ 1.798 × 10³⁰⁸- Maximum
xfor finiteexp(x)≈ 709.782 - Maximum
xfor 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:
vis chosen so thatx - vsubtraction is exact (no rounding error)scaleapproximatesexp(v)/2with 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
xis 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.