Skip to content

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 finite double value) ≈ 1.798 × 10³⁰⁸
  • Maximum x for finite exp(x) ≈ 709.782
  • Maximum x for finite sinh(x) or cosh(x) ≈ 710.475
  • For x >= 20, both functions are well approximated by exp(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)

c
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:

c
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 that x - v subtraction is exact (no rounding error)
  • scale approximates exp(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:

c
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:

c
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:

c
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):

MethodMax Error (ULP)Std Dev (ULP)Time (μs)
Double-double decomposition0.6120.2890.0147
Exact subtraction + scaling1.0070.4080.0199
Simple squaring1.9180.7510.0169
Logarithmic identity495.70871.1940.0197

Implementation Recommendations

Best Overall Solution

c
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

c
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

  1. Platform Dependence: Results may vary based on exp() implementation quality and hardware support
  2. FMA Availability: Use fma() when available for improved accuracy
  3. Range Checking: Always validate that x is within the appropriate range
  4. 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.