[旧日谈]基于IEEE754的快速EXP函数估计算法
前置知识 - exp函数变换
首先我们来转换一下exp函数表达式,令其更趋向于我们所需要的表达法。
我们需要求的式为exp(x)=exsexp(x)=e^xsexp(x)=exs
我们观察此式exe^xex
则有
eln(ax)=axe^{\ln(a^x)}=a^xeln(ax)=ax
这里令a=2,则有
eln(2)∗b=2be^{\ln(2)*b}=2^beln(2)∗b=2b
令x=ln(2)∗bx=\ln(2)*bx=ln(2)∗b,则有:
ex=2x/ln(2)e^x=2^{x/\ln(2)}ex=2x/ln(2)
记住这个推论,接下来我们会用到exp的这种表达式来进行推导。当然了这里的a取多少都行,因为我们接下来的计算需要基于IEEE 754约定,所以我们就需要在这里取2.
前置知识:IEEE 754的浮点数表达法:
IEEE 754 浮点数由三部分组成:
- 符号位(Sign Bit):1 位,表示数的正负(0 为正,1 为负)。
- 指数位(Exponent):若干位,表示数的指数部分。
- 尾数位(Mantissa):若干位,表示数的小数部分。
一般常见的两种分别是单精度浮点数与双精度浮点数,其位数描述是这样的:
类型 | 符号位 | 指数位 | 尾数位 | 总位数 |
---|---|---|---|---|
单精度浮点数 | 1 | 8 | 23 | 32 |
双精度浮点数 | 1 | 11 | 52 | 64 |
浮点数的具体计算方式: |
Value=(−1)Sign∗(1+Mantissa)∗2Exponent−biasValue = (-1)^{Sign} *(1 + Mantissa)* 2^{Exponent-bias} Value=(−1)Sign∗(1+Mantissa)∗2Exponent−bias
前置知识:指数偏移bias
首先要知道双精度浮点数的内存上表达,内存中表达如图所示:
为什么要做这样一个偏移?其实这个偏移是刚好把s和e拼在了一起,当成一个完整的12bit的数字来看待,这样当我们将这个指数当成一个无符号的整数来看待时,直接加上一个溢出的值,这样就可以避免复杂 处理和判断了。
举个例子,以一个32位的单精度浮点数为例
情况1:正指数
- 实际指数E=3
- 存储的指数e=E+127=130
- 二进制表示:130=10000010
情况2 :零指数
- 实际指数E=0
- 存储的指数e=E+127=127
- 二进制表示:127=01111111
情况3:负指数
- 实际指数E=-3
- 存储的指数e=E+127=124
- 二进制表示:124=01111100
这样我们就可以把有符号的指数转换成无符号的存储值来看待了
核心算法推导
目标表达式构造
我们需要构造浮点数使其满足:
2x/ln2=(1+m)⋅2e−1023(1) 2^{x/\ln2} = (1 + m) \cdot 2^{e - 1023} \quad (1) 2x/ln2=(1+m)⋅2e−1023(1)
对两边取对数得:
xln2=(e−1023)+log2(1+m)(2) \frac{x}{\ln2} = (e - 1023) + \log_2(1 + m) \quad (2) ln2x=(e−1023)+log2(1+m)(2)
整数分解与线性插值
将输入参数分解为整数和小数部分:
xln2=k+δ其中{k=⌊x/ln2⌋δ∈[0,1) \frac{x}{\ln2} = k + \delta \quad \text{其中} \quad \begin{cases} k = \lfloor x/\ln2 \rfloor \\ \delta \in [0,1) \end{cases} ln2x=k+δ其中{k=⌊x/ln2⌋δ∈[0,1)
此时指数表达式可拆分为:
2k+δ=2k⋅2δ 2^{k+\delta} = 2^k \cdot 2^\delta 2k+δ=2k⋅2δ
指数部分处理
- 整数部分:通过调整浮点数的指数位实现
- 小数部分:通过尾数位进行线性插值近似
位操作实现
指数位设置
由公式(2)可得指数存储值:
e=k+1023=⌊x/ln2⌋+1023 e = k + 1023 = \lfloor x/\ln2 \rfloor + 1023 e=k+1023=⌊x/ln2⌋+1023
尾数位设置
将小数部分δ\deltaδ映射到尾数位:
- δ\deltaδ的二进制小数形式直接填充52位尾数
- 对应内存操作:将δ\deltaδ左移52位
数学证明
一、指数位设置的严格性证明
命题
当设置浮点数指数位为:
e=⌊x/ln2⌋+1023
e = \lfloor x/\ln2 \rfloor + 1023
e=⌊x/ln2⌋+1023
时,该浮点数的指数部分将精确表示2k2^k2k(其中k=⌊x/ln2⌋k = \lfloor x/\ln2 \rfloork=⌊x/ln2⌋)
证明
根据IEEE754双精度浮点数规范:
- 存储的指数值需满足:
estored=Eactual+1023(Eactual∈Z) e_{stored} = E_{actual} + 1023 \quad (E_{actual} \in \mathbb{Z}) estored=Eactual+1023(Eactual∈Z) - 实际指数计算为:
Eactual=estored−1023 E_{actual} = e_{stored} - 1023 Eactual=estored−1023
将xln2\frac{x}{\ln2}ln2x分解为整数和小数部分:
xln2=k+δ(k∈Z, δ∈[0,1))
\frac{x}{\ln2} = k + \delta \quad (k \in \mathbb{Z},\ \delta \in [0,1))
ln2x=k+δ(k∈Z, δ∈[0,1))
则:
2x/ln2=2k⋅2δ
2^{x/\ln2} = 2^k \cdot 2^\delta
2x/ln2=2k⋅2δ
此时:
Eactual=k⇒estored=k+1023
E_{actual} = k \quad \Rightarrow \quad e_{stored} = k + 1023
Eactual=k⇒estored=k+1023
证毕。
二、尾数位线性插值的误差分析
命题
将δ\deltaδ的二进制小数直接填充到尾数位,等价于用一阶泰勒展开近似:
2δ≈1+δ(δ∈[0,1))
2^\delta \approx 1 + \delta \quad (\delta \in [0,1))
2δ≈1+δ(δ∈[0,1))
其最大相对误差为:
ϵmax=2δ−(1+δ)2δ≤4.67%(当δ=1时)
\epsilon_{max} = \frac{2^\delta - (1+\delta)}{2^\delta} \leq 4.67\% \quad (\text{当} \delta=1 \text{时})
ϵmax=2δ2δ−(1+δ)≤4.67%(当δ=1时)
证明
-
泰勒展开分析:
- 泰勒展开式:
2δ=eδln2=1+δln2+(δln2)22+⋯ 2^\delta = e^{\delta \ln2} = 1 + \delta \ln2 + \frac{(\delta \ln2)^2}{2} + \cdots 2δ=eδln2=1+δln2+2(δln2)2+⋯ - 一阶截断:
2δ≈1+δln2(ln2≈0.693) 2^\delta \approx 1 + \delta \ln2 \quad (\ln2 \approx 0.693) 2δ≈1+δln2(ln2≈0.693)
- 泰勒展开式:
-
实际误差计算:
- 真实值:2δ2^\delta2δ
- 近似值:1+δ1 + \delta1+δ
- 相对误差:
ϵ=∣2δ−(1+δ)2δ∣ \epsilon = \left| \frac{2^\delta - (1+\delta)}{2^\delta} \right| ϵ=2δ2δ−(1+δ)
δ 真实值 近似值 相对误差 0.0 1.000 1.000 0.00% 0.5 1.414 1.500 6.07% 1.0 2.000 2.000 0.00% -
误差上界推导:
令函数:
f(δ)=2δ−(1+δ) f(\delta) = 2^\delta - (1+\delta) f(δ)=2δ−(1+δ)
求导得极值点:
f′(δ)=2δln2−1=0⇒δ=log2(1ln2)≈0.528 f'(\delta) = 2^\delta \ln2 - 1 = 0 \quad \Rightarrow \quad \delta = \log_2\left(\frac{1}{\ln2}\right) \approx 0.528 f′(δ)=2δln2−1=0⇒δ=log2(ln21)≈0.528
此时最大误差:
ϵmax≈20.528−1.52820.528≈4.67% \epsilon_{max} \approx \frac{2^{0.528} - 1.528}{2^{0.528}} \approx 4.67\% ϵmax≈20.52820.528−1.528≈4.67%
三、内存位操作的数学等价性
命题
将δ\deltaδ左移52位的操作等价于构造尾数:
m=δ−⌊252δ⌋252
m = \delta - \frac{\lfloor 2^{52} \delta \rfloor}{2^{52}}
m=δ−252⌊252δ⌋
证明
-
二进制小数表示:
任何δ∈[0,1)\delta \in [0,1)δ∈[0,1)可表示为:
δ=∑i=152bi2−i+ϵ(bi∈{0,1}, ϵ<2−52) \delta = \sum_{i=1}^{52} b_i 2^{-i} + \epsilon \quad (b_i \in \{0,1\},\ \epsilon < 2^{-52}) δ=i=1∑52bi2−i+ϵ(bi∈{0,1}, ϵ<2−52) -
左移操作解析:
- 左移52位相当于计算:
δ×252=整数部分+小数部分 \delta \times 2^{52} = \text{整数部分} + \text{小数部分} δ×252=整数部分+小数部分 - 取整数部分:
⌊δ×252⌋=∑i=152bi252−i \lfloor \delta \times 2^{52} \rfloor = \sum_{i=1}^{52} b_i 2^{52-i} ⌊δ×252⌋=i=1∑52bi252−i
- 左移52位相当于计算:
-
尾数构造:
IEEE754尾数的实际存储值为:
m=⌊δ×252⌋252 m = \frac{\lfloor \delta \times 2^{52} \rfloor}{2^{52}} m=252⌊δ×252⌋
因此:
1+m=1+δ−ϵ(ϵ<2−52) 1 + m = 1 + \delta - \epsilon \quad (\epsilon < 2^{-52}) 1+m=1+δ−ϵ(ϵ<2−52)
四、参数c的最优性证明
命题
当选择修正参数:
c=220ln2(ln(ln2)+1)≈60,801
c = \frac{2^{20}}{\ln2} \left( \ln(\ln2) + 1 \right) \approx 60,801
c=ln2220(ln(ln2)+1)≈60,801
时,可最小化近似误差的均方根(RMS)
证明
-
误差函数定义:
定义相对误差函数:
r(y)=EXP(y)−eyey r(y) = \frac{\text{EXP}(y) - e^y}{e^y} r(y)=eyEXP(y)−ey -
积分均方误差:
RMS=1ymax−ymin∫yminymaxr(y)2dy \text{RMS} = \sqrt{\frac{1}{y_{max}-y_{min}} \int_{y_{min}}^{y_{max}} r(y)^2 dy} RMS=ymax−ymin1∫yminymaxr(y)2dy -
Lambert W函数解:
通过求解误差积分的最小值,得到方程:
W(−eγln22)=−(γ+1) W\left(-\frac{e^{\gamma} \ln2}{2}\right) = -(\gamma + 1) W(−2eγln2)=−(γ+1)
其中γ=cln2/220\gamma = c \ln2 / 2^{20}γ=cln2/220,WWW为Lambert W函数。 -
数值解:
使用迭代法求得:
γ≈0.0411⇒c≈60,801 \gamma \approx 0.0411 \quad \Rightarrow \quad c \approx 60,801 γ≈0.0411⇒c≈60,801
结论
通过上述数学证明,我们严格验证了:
- 指数位的设置能精确表达整数幂次
- 尾数位的线性插值引入可控误差
- 参数c的最优选择使均方误差最小化
这种基于IEEE754内存位操作的EXP算法,在保证计算效率的同时,提供了理论严谨的误差控制机制。
从数学推导到代码实现的映射解析
一、核心公式到代码的转换
1. 核心公式回顾
算法核心公式:
i=220ln2⏟EXP_A⋅y+(1072693248−60801)⏟基值修正
i = \underbrace{\frac{2^{20}}{\ln2}}_{EXP\_A} \cdot y + \underbrace{(1072693248 - 60801)}_{基值修正}
i=EXP_Aln2220⋅y+基值修正(1072693248−60801)
2. 代码要素分解
数学要素 | 代码实现 | 数值说明 |
---|---|---|
220/ln22^{20}/\ln2220/ln2 | #define EXP_A (1048576/M_LN2) | 1048576=2²⁰ |
基值修正项 | 1072693248 - EXP_C | 1072693248=0x3FF00000 |
整体位操作 | eco.n.i = ... | 直接操作双精度浮点高位内存 |
二、关键内存布局操作
1. 双精度浮点内存结构
// 内存布局(小端序):
struct {
uint32_t j; // 低32位:尾数低位(实际未使用)
uint32_t i; // 高32位:符号位+指数位+尾数高位
} n;
```c
// C语言实现示例
union {
double d;
struct {
uint32_t i; // 高位字(含符号位和指数位)
uint32_t j; // 低位字(尾数位)
} n;
} eco;
#define EXP_A (1048576 / M_LN2) // 2^20 / ln2 ≈ 1512775.916
#define EXP_C 60801 // 最优误差修正参数
#define EXP(y) (eco.n.i = EXP_A*(y) + (1072693248 - EXP_C), eco.d)
或者有一种更好解读的方式,就是:
inline double fast_exp(double y) {
double d;
*(reinterpret_cast<int*>(&d) + 0) = 0;
*(reinterpret_cast<int*>(&d) + 1) = static_cast<int>(1512775 * y + 1072632447);
// 2^20 / ln2 ≈ 1512775.916 其中最优误差修正参数为60801
//1072693248 实际是 0x3FF00000 的十进制形式,其作用包括:
//1. 初始化指数偏置:将基准指数设为 1023(对应实际指数 0)
//2. 尾数清零:隐式设置尾数为 1.0(规格化数的隐含前导1)
//3. 快速位操作:通过整数加法直接修改指数位
return d;
}