https://quadst.rip/ln-approx
泰勒级数收敛很慢。
这个 SO 答案向我指出了Remez 算法,虽然我并不完全理解它,但近似的简单性和准确性确实看起来非常好。
他们在答案中仅给出了范围$[1,2]$的 4 阶近似值: −1.7417939+(2.8212026+(−1.4699568+(0.44717955−0.056570851x)x)x)x 但是我也想尝试找出 3 阶,因为在我的应用程序中,我可以用稍微低一点的精度(和更快的速度)来完成。我没有答案中提到的 Maple 软件,但我找到了这篇博客文章,其中作者使用 C++ 的Boost 库内置函数计算了 atan2 近似值。 −1.49278+(2.11263+(−0.729104+0.10969x)x)x
▲ 误差太小,人眼无法从图中观察到
如果我们用四阶多项式代替三阶多项式,即 return -1.7417939+(2.8212026+(-1.4699568+(0.44717955-0.056570851*x)*x)*x)*x+0.6931471806*t; 我们甚至可以将(绝对)误差缩小到: avg_err=0.000039, max_err=0.000061 是的!小数点后有四个零。
现在为了测试算法的速度,我们可以使用下面的代码: #include <sys/time.h>
#include <stdlib.h>
int main(){
srand(time(NULL));
float* ans = malloc(sizeof(float)*100);
unsigned int N = 1000000000;
{
struct timeval t0, t1;
unsigned int i;
gettimeofday(&t0, NULL);
for(i = 0; i < N; i++){
float y = log((float)i/1000.0);
ans[i%100]=y;
}
gettimeofday(&t1, NULL);
printf("%u <math.h> log() calls in %.10f seconds\n", i, t1.tv_sec - t0.tv_sec + 1E-6 * (t1.tv_usec - t0.tv_usec));
}
{
struct timeval t0, t1;
unsigned int i;
gettimeofday(&t0, NULL);
for(i = 0; i < N; i++){
float y = ln((float)i/1000.0);
ans[i%100]=y;
}
gettimeofday(&t1, NULL);
printf("%u ln() calls in %.10f seconds\n", i, t1.tv_sec - t0.tv_sec + 1E-6 * (t1.tv_usec - t0.tv_usec));
}
printf("spot check: %f\n",ans[rand()%100]);
return 0;
}
注意我们如何初始化一个小数组,在计算对数时修改数组的元素,最后随机抽样并打印数组的一个元素。这是为了防止 C 编译器变得“太聪明”,并优化整个循环:我们正在进行“抽查”,以确保gcc 没有通过跳过所有工作来作弊! 编译并运行: gcc -O3 ln.c ; time ./a.out
在我的旧 MacBook 上,我得到的是: 1000000000 <math.h> log() calls in 10.5289920000 seconds
1000000000 ln() calls in 2.8090880000 seconds
速度几乎快了四倍! 如果我们使用四阶多项式: 1000000000 <math.h> log() calls in 10.5589010000 seconds
1000000000 ln() calls in 3.2228910000 seconds
但速度仍是原来的三倍多! |