☰
手撕官方expf!拆整数+查表+插值,关闭FPU快9倍
2026/9/29 22:08:41 网站建设 项目流程

前三篇文章里,我用定点查表法把sinf()的计算速度提升了14倍,把atan2f()提升了8倍,把sqrtf()提升了2.46倍。今天我们来手撕expf()——这个在数字滤波、概率计算、衰减模型里无处不在的函数。

01 为什么是exp?

在嵌入式开发中,exp的出场频率比你想象的高:

- 数字滤波:一阶低通滤波器的系数计算

- 传感器线性化:热敏电阻、光电二极管的非线性校正

- 概率计算:贝叶斯推断、softmax

- 衰减模型:电池放电曲线、RC充放电

- 音频处理:指数包络、动态范围压缩

标准库的expf()在Cortex-M4上虽然可以用FPU加速部分运算,但内部有大量分支和范围检查。然而实际工程应用中,输出通常在某个固定范围之内,根据实际情况,制定定点fast_exp()用拆分整数+小数加查表和插值的方式,可以做到更快、更可控。

02 exp的特殊难点:动态范围极大

和sin()、atan2()、sqrt()不同,exp()有一个天然的难点:它的值域跨度极大。

输入 x

-10

-1

0

1

5

10

exp(x)

0.000045

0.368

1.0

2.718

148.4

22026

Q15 格式只能表示-1.0 ~ 0.99997,根本装不下exp()的全部结果。所以定点exp() 不能像sin()那样直接查表输出,必须做范围压缩。

03 核心思路:拆整数+小数

指数函数有一个天然性质:

其中:

- k 是整数部分

- r 是小数部分,范围 |r|≤ln2/2 ≈ 0.3466

- 2^k 用移位实现

- exp(r) 在[-0.3466, 0.3466] 范围内,值域约为[0.707, 1.414],Q15 装不下 1.414

关键改进:我们不用四舍五入取k,而是用ceil 向上取整:

这样exp(r) in (0.5, 1.0],Q15 刚好装得下,不需要任何额外的缩放。

所以流程是:

输入 x (Q6.10)

计算 k = ceil(x / ln2)

计算 r = x - k·ln2 (范围 (-ln2, 0])

查表求 exp(r) (Q15, 范围 (0.5, 1.0])

输出尾数 exp(r) 和指数 k

实际值 = 尾数 / 32768.0 × 2^k

关键洞察:exp()的定点实现不是查表求exp(),而是查表求exp(r) + 移位求2^k。移位是零成本的,所以真正耗时的只有查表那一步。

04 fast_exp实现

exp_table[258]:exp(r) 的 Q15 值,r = (i - 256) / 256,i = 0~256

extern const uint16_t exp_table[258];

// 常数,Q15 格式

#define INV_LN2_Q15 47274 // (1/ln2) × 2^15 ≈ 47274

#define LN2_Q15 22713 // ln2 × 2^15 ≈ 22713

typedef struct {

int16_t mantissa; // Q15,范围 [0.3679, 1.0]

int16_t exponent; // 2 的幂次

} exp_result_t;

/**

* @brief 快速指数函数

* @param x 输入,Q6.10 格式(实际值 = x / 1024)

* @return exp_result_t,实际值 = mantissa / 32768.0 × 2^exponent

*

* 推荐输入范围:x ∈ [-32768, 32767](实际值 [-32,31.999],

* /

exp_result_t fast_exp(int16_t x) {

// 1. 计算 t = x / ln2,Q6.10 格式(乘法代替除法,四舍五入)

int32_t t = ((int32_t)x * INV_LN2_Q15 + 16384) >> 15;

// 2. 计算 k = ceil(t / 256),无分支向上取整

int32_t k = (t + 1023) >> 10;

// 3. 计算 r = x - k × ln2,Q6.10 格式

int32_t r_q6_10 = (int32_t)x - ((k * LN2_Q15 + 16) >> 5)+1024;

int32_t idx = r_q6_10 >> 2;

int32_t frac = r_q6_10 & 0x03;

// 4. 查表

exp_result_t result;

int32_t res = (int32_t)exp_table[idx+1] - exp_table[idx];

res = (res * frac +2)>>2;

res += exp_table[idx];

result.mantissa = (int16_t)res;

result.exponent = (int16_t)k;

return result;

}

关于除法

代码中用乘法 x * INV_LN2_Q15 代替了除法。在 Cortex-M4 上,乘法只需 1~2 个周期,比除法快得多。如果芯片没有硬件除法(如 M0),这个优化更加明显。

关于执行时间

查表+移位的方式没有循环,每次调用的执行时间都是固定的,对实时控制非常友好。

05 使用示例

// 计算 exp(1.0)

// x = 1.0 × 1024 = 1024

exp_result_t r = fast_exp(1024);

// 结果:r.mantissa ≈ 22259,r.exponent = 2

// 实际值 = 22259 / 32768.0 × 2^2 ≈ 2.71716

// 计算 exp(-0.5)

// x = -0.5 × 1024 = -512

exp_result_t r2 = fast_exp(-512);

// 结果:r2.mantissa = 19875,r2.exponent = 0

// 实际值 = 19875/32768 ≈ 0.60654

// 计算 exp(0)

exp_result_t r3 = fast_exp(0);

// 结果:r3.mantissa = 32767,r3.exponent = 0

// 实际值 = 32767 / 32768.0 × 1 ≈ 0.99997

06 精度分析

fast_exp 的误差主要来自三个环节:

误差来源

量级

说明

查表+插值

0.04%

256点查表,r∈(-ln2,0]

Q15尾数量化

0.003%

表格值存储为Q15

输入Q6.10量化

0.05%

输入x的分辨率1/1024

总相对误差

0.05%-0.09%

主要瓶颈是输入量化

注意:exp() 的误差应该用相对误差衡量,因为输出值域跨度极大(从1.24x10^-14到7.9x10^13),绝对误差没有意义。如果要降低总误差,需要提高输入格式的位宽,例如改用 Q16.16。

07 性能实测

测试平台:GD32F303VGT6 @120MHz,DWT周期计数,32768次调用。

函数

开启FPU(周期)

关闭FPU(周期)

expf()

3669947

20164341

fast_exp()

2228224

2228224

08 源码与工程

完整的Keil工程已上传Gitee,包含:

- GD32F303VGT6的完整工程

- fast_exp() 完整源码

- 性能测试代码(使用DWT周期计数)

- 精度测试代码

Gitee地址:https://gitee.com/jervis_luo/gd32_fast_exp.git

09 总结

对比维度

expf()(开FPU)

expf()(关FPU)

fast_exp()

32768次耗时

3,669,947

20,164,341

2,228,224

最大相对误差

~1e-7

~1e-7

0.05%-0.09%

是否需要FPU

是

否

否

适用芯片

M4/M7

M0/M3/M4/M7

M0/M3/M4/M7

三点核心收获:

1. 定点exp()的核心是范围压缩——exp(x) = 2^k · exp(r),用ceil取整保证r≤0,让exp(r)落在Q15范围内。

2. 误差用相对误差衡量——exp的值域跨度极大,绝对误差没有意义;总误差主要来自输入量化。

3. 执行时间固定——没有循环和分支预测失败,对实时控制非常友好。

系列文章

1. [fast_sin:比官方sinf快14倍]

2. [fast_atan2:FOC角度计算优化:128点查表法让atan2f快8倍]

3. [fast_sqrt:FOC矢量幅值计算优化:256点查表+牛顿迭代,让sqrtf快2.5倍]

4. [手撕官方expf!拆整数+查表+插值,关闭FPU快9倍](本文)

后记:写完这篇文章后,我把 fast_exp() 用在了自己做的电池管理模块里。原本用 expf() 计算SOC曲线时,每个采样周期要花几十微秒;换上 fast_exp() 后,直接降到几微秒。整个模块的采样率从1kHz提升到了10kHz。这大概就是优化的乐趣所在。

如果你对定点数学库感兴趣,欢迎关注我。这个系列到这里已经覆盖了sin()、atan2()、sqrt()、exp() 四个函数,后续如果大家感兴趣,我可以继续写 fast_log()、fast_pow() 或者矩阵运算的定点实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询