【发布时间】:2020-01-21 20:06:00
【问题描述】:
我写了一个 n-section 算法来寻找函数的根。工作原理与二分法完全相同,只是将范围分成N等份。这是我的 C 代码:
/*
Compile with: clang -O3 -o nsect nsect.c -Wall -DCOUNT=5000000 -DNSECT=? -funroll-loops
*/
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#define N 6
#ifndef COUNT
#error COUNT not defined!
#endif
#ifndef NSECT
#define NSECT 2
#endif
float polynomial[N];
float horner( const float poly[N], float x )
{
float val = poly[N-1];
for ( int i = N - 2; i >= 0; i-- )
val = poly[i] + x * val;
return val;
}
float f( float x )
{
return horner( polynomial, x );
}
float nsect( float a, float b, const float eps_x )
{
float fa = f( a );
float fb = f( b );
if ( fa == 0 ) return a;
else if ( fb == 0 ) return b;
else if ( fa * fb > 0 ) return 0;
float x[NSECT];
float fx[NSECT];
while ( b - a > eps_x )
{
x[0] = a;
if ( ( fx[0] = f( x[0] ) ) == 0 ) return x[0];
int found = 0;
for ( int i = 0; i < NSECT - 1; i++ )
{
x[i + 1] = a + ( b - a ) * (float)( i + 1 ) / NSECT;
if ( ( fx[i + 1] = f( x[i + 1] ) ) == 0 )
return x[i + 1];
else if ( fx[i] * fx[i + 1] < 0 )
{
a = x[i];
b = x[i + 1];
found = 1;
break;
}
}
if ( !found )
a = x[NSECT - 1];
}
return ( a + b ) * 0.5f;
}
int main( int argc, char **argv )
{
struct timeval t0, t1;
float *polys = malloc( COUNT * sizeof( float ) * N );
float *p = polys;
for ( int i = 0; i < COUNT * N; i++ )
scanf( "%f", p++ );
float xsum = 0; // So the code isn't optimized when we don't print the roots
p = polys;
gettimeofday( &t0, NULL );
for ( int i = 0; i < COUNT; i++, p += N )
{
memcpy( polynomial, p, N * sizeof( float ) );
float x = nsect( -100, 100, 1e-3 );
xsum += x;
#ifdef PRINT_ROOTS
fprintf( stderr, "%f\n", x );
#endif
}
gettimeofday( &t1, NULL );
fprintf( stderr, "xsum: %f\n", xsum );
printf( "%f ms\n", ( ( t1.tv_sec - t0.tv_sec ) * 1e6 + ( t1.tv_usec - t0.tv_usec ) ) * 1e-3 );
free( polys );
}
编辑:这是我用来编译代码的命令:clang -O3 -o nsect nsect.c -Wall -DCOUNT=5000000 -DNSECT=? -funroll-loops。
我在 i7-8700k 上运行一切。
我决定测试不同 N 值的算法性能。该测试包括测量为 5,000,000 个 5 次多项式中的每一个找到范围 (-100;100) 中的任何根所需的时间。多项式是随机生成的,并且具有从 -5 到 5 的实系数。多项式值使用以下公式计算:霍纳的方法。
这些是我为每个 N 运行代码 10 次得到的结果(x=N,y=time [ms]):
我对这里最坏情况性能的理解是,在主 while 循环中要完成的工作量与 N 成正比。主循环需要 logN(C) (其中 C > 1 是一个常数 - 初始搜索范围与要求的准确度之比)迭代到完全的。这产生以下等式:
该图看起来与我上面用来大致匹配数据的紫色曲线非常相似:
现在,我有一些(希望是正确的)结论和问题:
- 首先,我的方法是否有效?
- 我想出的函数真的描述了操作数和N之间的关系吗?
- 我认为这是最有趣的一个 - 我想知道是什么导致 N=2 与所有其他人之间存在如此显着的差异?对于我的所有测试数据,我始终得到这种模式。
此外:
- 该函数在 e≈2.718... 中具有最小值,它更接近于 3 而不是 2。此外,f(3) 成立.鉴于我提出的方程式是正确的,我认为这意味着三分法实际上可能比二分法更有效。这似乎有点违反直觉,但测量结果似乎承认了这一点。对吗?
- 如果是这样,与二等分相比,为什么三分法似乎如此不受欢迎?
谢谢
【问题讨论】:
-
这些根求解器本质上是在对许多随机函数进行平均并将初始间隔缩放到长度为 1 之后,列出以 N 为底的数字(它们收敛到的根)的数字。最有效的数字系统is the one of base 3。
-
二分算法在结构上非常简单,就像排序算法中的冒泡排序一样。对于三等分实施,您至少需要两个分支来选择下一个间隔。如果您非常关心二分法的性能,则可以通过使用函数值作为正割根等选择中点来获得更显着的改进,例如在 regula falsi 及其变体中,然后是 Dekker 方法和广泛使用的 Brent 方法。
-
@PeterCordes 我在源代码顶部的注释中指定了构建命令。它在那里并不真正可见-对此感到抱歉。我使用了
clang编译器和-O3优化。但是,我没有尝试使用-ffast-math并启用矢量化。我的 CPU 是 i7-8700k。 -
在内部循环中,您正在重新计算
fa == f(x[0])和可能的fb == f(x[NSECT])。这对二等分和三等分情况下的运行时间有很大影响,而在更高的部门中影响不大。 -
@LutzL 我没有意识到由此造成的差异有多大。我优化了代码,因此它重用了以前迭代中的 f(x) 值,并且 N=2 现在可以与所有其他值相媲美。感谢您指出这一点!
标签: c algorithm benchmarking numerical-methods