是否可以优化此集成代码,使其运行更快?


9
double trap(double func(double), double b, double a, double N) {
  double j;
  double s;
  double h = (b-a)/(N-1.0); //Width of trapezia

  double func1 = func(a);
  double func2;

  for (s=0,j=a;j<b;j+=h){
    func2 = func(j+h);
    s = s + 0.5*(func1+func2)*h;
    func1 = func2;
  }

  return s;
}

上面是我的C ++代码,用于func()极限之间的一维数字积分(使用扩展的梯形规则)[a,b] 使用 N1 斜方肌。

我实际上是在进行3D集成,其中此代码被递归调用。我一起工作N=50 给我不错的结果

除了减少 N此外,有人能建议如何优化上面的代码,使其运行更快吗?或者甚至可以建议一种更快的集成方法?


5
这与问题并没有真正的关系,但是我建议选择更好的变量名。像trapezoidal_integration代替trapsumrunning_total代替s(也可以使用+=代替s = s +),trapezoid_width或者dx代替h(或不使用,取决于梯形规则的首选表示法),然后更改func1func2反映它们是值而不是函数的事实。例如func1-> previous_valuefunc2-> current_value或类似的东西。
David Z

Answers:


5

从数学上讲,您的表达等效于:

I=h(12f1+f2+f3+...+fn1+12fn)+O((ba)3fn2)

因此,您可以实现它。如前所述,时间可能是函数评估所决定的,因此,要获得相同的精度,可以使用需要较少函数评估的更好的积分方法。

在现代,高斯正交不只是玩具。只有有用的,如果你需要非常少的评价。如果您想要一些易于实现的方法,则可以使用Simpson规则,但是如果没有充分的理由,我不会超出阶数。1/N3

如果函数的曲率变化很大,则可以使用自适应步长例程,该函数会在函数平坦时选择较大的步长,而在曲率较高时选择较小的更精确的步长。


在离开并回到问题上之后,我决定实施辛普森规则。但是我可以检查一下,实际上,复合Simpson规则中的误差是否与1 /(N ^ 4)成正比(而不是您在答案中所暗示的1 /(N ^ 3))?
user2970116 2014年

1
您有和公式。第一个使用的系数和第二。1/N31/N45/12,13/12,1,1...1,1,13/12,15/121/3,4/3,2/3,4/3...
Davidmh,2014年

9

函数的评估可能是此计算中最耗时的部分。如果真是这样,那么您应该专注于提高func()的速度,而不是尝试加快集成例程本身的速度。

根据func()的属性,还可能通过使用更复杂的积分公式来获得更精确的积分评估,而函数评估更少。


1
确实。如果您的函数是平滑的,那么如果仅使用5个时间间隔的Gauss-4求积规则,则通常可以得到少于50个函数的求值。
Wolfgang Bangerth,2014年

7

可能?是。有用?不会。我将在此处列出的优化不太可能对运行时产生百分之几的差异。一个好的编译器可能已经为您完成了这些工作。

无论如何,请查看您的内部循环:

    for (s=0,j=a;j<b;j+=h){
        func2 = func(j+h);
        s = s + 0.5*(func1+func2)*h;
        func1 = func2;
    }

在每个循环迭代中,您都可以执行三个可以带到外面的数学运算:加法j + h,乘以0.5和乘以h。您可以通过在处开始迭代器变量来解决第一个问题a + h,而通过排除乘法来解决其他问题:

    for (s=0, j=a+h; j<=b; j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

尽管我会指出这样做,但是由于浮点舍入错误,有可能错过循环的最后一次迭代。(这也是您原始实现中的问题。)要解决此问题,请使用unsigned intsize_t计数器:

    size_t n;
    for (s=0, n=0, j=a+h; n<N; n++, j+=h){
        func2 = func(j);
        s += func1+func2;
        func1 = func2;
    }
    s *= 0.5 * h;

正如Brian的回答所言,您的时间最好花在优化功能评估上func。如果此方法的准确性足够,我怀疑您会发现相同的方法更快N。(尽管您可以运行一些测试来查看,例如Runge-Kutta是否可以使您降低N得足够多,从而使整体集成花费更少的时间而又不牺牲准确性。)


4

我建议进行一些更改以改进计算:

  • 为了提高性能和准确性,请使用std::fma(),它执行融合的乘法加法运算
  • 为了提高性能,请将每个梯形的面积乘以0.5-最后可以做一次。
  • 避免重复添加h,这可能会累积舍入误差。

另外,为清楚起见,我将进行一些更改:

  • 给函数起一个更具描述性的名称。
  • 交换函数签名中的a和的顺序b
  • 重命名Nnhdxjx2saccumulator
  • 更改nint
  • 在更严格的范围内声明变量。
#include <cmath>

double trapezoidal_integration(double func(double), double a, double b, int n) {
    double dx = (b - a) / (n - 1);   // Width of trapezoids

    double func_x1 = func(a);
    double accumulator = 0;

    for (int i = 1; i <= n; i++) {
        double x2 = a + i * dx;      // Avoid repeated floating-point addition
        double func_x2 = func(x2);
        accumulator = std::fma(func_x1 + func_x2, dx, accumulator); // Fused multiply-add
        func_x1 = func_x2;
    }

    return 0.5 * accumulator;
}

3

如果您的函数是多项式,可能是由某些函数(例如高斯函数)加权的,则可以直接使用古巴公式在3d中进行精确积分(例如,http//people.sc.fsu.edu/~jburkardt/c_src/ stroud / stroud.html)或稀疏网格(例如http://tasmanian.ornl.gov/)。这些方法仅指定一组点和权重即可乘以函数值,因此它们非常快。如果您的函数足够平滑,可以用多项式近似,那么这些方法仍然可以提供很好的答案。这些公式专用于要集成的函数的类型,因此可能需要进行一些挖掘才能找到合适的函数。


3

当您尝试通过数值计算积分时,您尝试以最小的努力获得所需的精度,或者尝试以固定的努力获得最高的精度。您似乎在问如何使一种特定算法的代码尽可能快地运行。

这可能会给您带来一些好处,但是却很少。有许多更有效的数值积分方法。Google的“辛普森法则”,“ Runge-Kutta”和“ Fehlberg”。通过评估函数的某些值并巧妙地将这些值的倍数相加,它们的工作方式都非常相似,在相同数量的函数求值时产生的误差较小,而在较小数量的求值时产生的误差相同。


3

进行积分的方法很多,其中梯形法则是最简单的方法。

如果您对所集成的实际功能一无所知,则可以更好地利用它。想法是在可接受的误差水平内将网格点的数量最小化。

例如,梯形对连续点进行线性拟合。您可以进行二次拟合,如果曲线平滑,则可以更好地拟合,这可以使您使用更粗糙的网格。

轨道仿真有时是使用圆锥曲线完成的,因为轨道非常类似于圆锥截面。

在我的工作中,我们正在集成近似钟形曲线的形状,因此按此建模是有效的(自适应高斯正交被视为这项工作的“黄金标准”)。


1

因此,正如其他答案所指出的那样,这在很大程度上取决于您的函数有多昂贵。仅当瓶颈确实存在时,优化陷阱代码才值得。如果不是很明显,则应通过对代码进行性能分析来进行检查(英特尔V-tune,Valgrind或Visual Studio之类的工具可以执行此操作)。

但是,我建议一种完全不同的方法:蒙特卡洛积分。在这里,您可以通过在随机点上对函数进行采样并添加结果来简单地近似积分。请参见 PDF除了对细节的维基页面。

这对于高维数据非常有效,通常比一维积分中使用的正交方法要好得多。

简单的情况很容易实现(请参阅pdf),只是要注意c ++ 98中的标准随机函数在性能和质量上都非常差。在c ++ 11中,可以在中使用Mersenne Twister。

如果您的功能在某些区域变化很大,而在其他区域变化很小,请考虑使用分层抽样。我建议使用 GNU科学库,而不要自己编写。


1
我实际上是在进行3D集成,其中此代码被递归调用。

“递归”是关键。您要么遍历一个大型数据集并多次考虑大量数据,要么实际上是通过(逐个?)函数自己生成数据集。

递归评估的集成将非常昂贵,并且随着递归能力的增加而变得不精确。

创建一个用于插值数据集的模型,并进行分段符号集成。由于大量数据随后被分解为基本函数的系数,因此深度递归的复杂度呈多项式增长(通常是较低的幂),而不是呈指数增长。这样您就可以得到“精确的”结果(您仍然需要找出好的评估方案来获得合理的数值性能,但是要获得比梯形积分更好的效果仍然相当可行)。

如果您查看梯形规则的错误估计,您会发现它们与所涉及函数的某些导数有关,并且如果以递归方式进行集成/定义,则这些函数将不会具有行为良好的导数。

如果您唯一的工具是锤子,那么每个问题都像钉子。尽管您几乎未触及描述中的问题,但我怀疑以递归方式应用梯形规则是不匹配的:您会同时出现不精确性和计算需求。


1

原始代码在每N个点评估该函数,然后将这些值相加,然后将总和乘以步长。唯一的技巧是在开头和结尾的值都加上权重1/2,而内部的所有点均以全权重添加。实际上,它们也增加了重量1/2但是两次。而不是将它们添加两次,而应将它们全权添加一次。乘以循环外的步长来排除乘法。确实,可以采取所有措施来加快速度。

    double trap(double func(double), double b, double a, double N){
double j, s;
double h = (b-a)/(N-1.0); //Width of trapezia

double s = 0;
j = a;
for(i=1; i<N-1; i++){
  j += h;
  s += func(j);
}
s += (func(a)+func(b))/2;

return s*h;
}

1
请说明您的更改和代码。代码块对大多数人来说是毫无用处的。
Godric Seer 2014年

同意;请解释您的答案。
Geoff Oxberry 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.