寻找加快功能的方法

时间:2019-07-09 19:33:26

标签: c++ performance

我正在尝试加速许多文件中的一大段代码,结果发现一个函数占用了大约70%的时间。这是因为此函数被称为477+百万次。

指针数组par只能是两个预设之一。

par[0] = 0.057;
par[1] = 2.87;
par[2] = -3.;
par[3] = -0.03;
par[4] = -3.05;
par[5] = -3.5; 

OR

par[0] = 0.043;
par[1] = 2.92;
par[2] = -3.21;
par[3]= -0.065;
par[4] = -3.00;
par[5] = -2.65;

因此,我尝试根据其预设插入数字,但是未能找到任何可节省的时间。

powexp函数似乎每次都被调用,它们分别占总时间的40%和20%,因此,仅占总时间的10%。该函数中不是powexp的部分。寻找加快速度的方法可能是最好的,但是pow中使用的指数中没有一个是-4以外的整数,而且我不知道1/(x*x*x*x)是否比{{1} }。

pow(x, -4)

4 个答案:

答案 0 :(得分:1)

我首先将其重写为更容易理解:

#include <math.h> 

double Param_RE_Tterm_approx(double Tterm, double const* par) {
  double value = 0.;

  if (Tterm > 0.) {
    // time after Che angle peak

    if ( fabs(Tterm/ par[0]) >= 1.e-2) {
      value += -1./(par[0])*exp(-1.*Tterm/(par[0]));
    } else {
      value += -1./par[0]*(1. - Tterm/par[0] + Tterm*Tterm/(par[0]*par[0]*2.) - Tterm*Tterm*Tterm/(par[0]*par[0]*par[0]*6.) );
    }

    if ( fabs(Tterm* par[1]) >= 1.e-2) {
      value += par[2]* par[1]*pow( 1.+par[1]*Tterm, par[2]-1. );
    } else {
      value += par[2]*par[1]*( 1.+(par[2]-1.)*par[1]*Tterm + (par[2]-1.)*(par[2]-1.-1.)/2.*par[1]*par[1]*Tterm*Tterm + (par[2]-1.)*(par[2]-1.-1.)*(par[2]-1.-2.)/6.*par[1]*par[1]*par[1]*Tterm*Tterm*Tterm );
    }

  } else {
    // time before Che angle peak

    if ( fabs(Tterm/ par[3]) >= 1.e-2 ) {
      value += -1./ par[3] *exp(-1.*Tterm/ par[3]);
    } else {
       value += -1./par[3]*(1. - Tterm/par[3] + Tterm*Tterm/(par[3]*par[3]*2.) - Tterm*Tterm*Tterm/(par[3]*par[3]*par[3]*6.) );
    }

    if ( fabs(Tterm* par[4]) >= 1.e-2 ) {
      value += par[5]* par[4] *pow( 1.+ par[4]*Tterm, par[5]-1. );

    } else {
       value += par[5]*par[4]*( 1.+(par[5]-1.)*par[4]*Tterm + (par[5]-1.)*(par[5]-1.-1.)/2.*par[4]*par[4]*Tterm*Tterm + (par[5]-1.)*(par[5]-1.-1.)*(par[5]-1.-2.)/6.*par[4]*par[4]*par[4]*Tterm*Tterm*Tterm );
    }
  }

  return value * 1.e9;
}

然后我们可以查看其结构。

有两个主要分支-Tterm负(之前)和正(之后)。这些对应于在par数组中使用0,1,2或3,4,5。

然后在每种情况下我们要做两件事以增加价值。在这两种情况下,对于较小的情况,我们都使用多项式;对于较大的情况,我们都使用指数/幂方程。

作为猜测,这是因为多项式是小值指数的一个体面近似值-错误是可以接受的。您应该做的就是确认这一猜测-看一下基于“大”幂/指数的方程式的泰勒级数展开式,看它是否与多项式一致。或用数字检查。

如果是这种情况,则意味着该等式具有可接受的已知误差量。通常,exppow更快版本具有已知的最大错误量;考虑使用那些。

如果不是这种情况,仍然可能会出现可接受的误差量,但是泰勒级数逼近可以为您提供“在代码中”可接受的误差量信息。

我下一步要做的是将这个等式的8个部分拆开。有一个正/负,每个分支中的第一个value+=和第二个<?php namespace App\Http\Middleware; use App\Helpers\Helper; use App\Helpers\LogHelper; use Carbon\Carbon; use Closure; use DateTime; use Exception; use Illuminate\Http\Response; class ApiAfter { public function handle($request, Closure $next) { return $next($request); } public function terminate($request, $response) { // move everything in handle function to this // logging the results of the request $response = $this->fixFiveHundred($request, $response); // I do some other stuff here return $response; } private function fixFiveHundred($request, $response) { if ($response->status() !== 500) return $response; try { if (!empty($response->original['message']) && $response->original['message'] === "Server Error") { if (!empty($response->exception)) { $newMessage = $response->exception->getMessage(); return response($newMessage, 500); // this is the line of code I'm having trouble with } } } catch(Exception $e) { return $response; } return $response; } } ,然后是多项式/指数形式。

我正在猜测一个事实,exp占用了pow的时间的1/3,这是因为您在函数中有3次调用pow到1次调用exp,但是您可能会发现一些有趣的东西,例如“时间实际上是在Tterm>0。情况下”或您有什么。

现在检查呼叫站点。您正在传递此函数的Tterm中是否存在模式?即,您倾向于按大致排序的顺序传递Tterms吗?如果是这样,您可以测试调用该函数的 outside 哪个函数,然后分批进行。

简单地分批执行,并进行优化编译并内联函数主体,可能会产生令人惊讶的差异;编译器在向量化工作方面越来越好。

如果这不起作用,则可以开始将内容整理掉。在现代计算机上,您可以拥有4至60个线程来独立解决此问题,并且该问题看起来您将获得近乎线性的加速。一个基本的线程库(如TBB)将很适合此类任务。

下一步,如果要获取大量数据,并且需要进行大量处理,则可以将其填充到GPU上并在那里进行求解。可悲的是,GPU RAM通讯很小,因此仅在GPU上执行此函数的数学运算并使用RAM来回读写就不会给您带来很多性能。但是,如果在GPU上进行的工作不止于此,那是值得的。

答案 1 :(得分:0)

  

此功能中不是pow或exp的部分仅使用总时间的10%。

如果函数性能瓶颈是exp(),pow()执行,请考虑在计算中使用向量指令。所有现代处理器都至少支持SSE2指令集,因此这种方法肯定可以使速度至少提高约2倍,因为您的计算可以很容易地矢量化。

我建议您使用this c ++向量化库,该库包含所有标准数学函数(例如exp和pow),并允许以OOP风格编写代码,而无需使用汇编语言。我使用了几次,它必须能完美解决您的问题。

如果您有GPU,还应该考虑尝试cuda框架,因为同样,您的问题可以完美地向量化。而且,如果此功能调用477+百万次,GPU会从根本上消除您的问题...

答案 2 :(得分:0)

(部分优化:)

最长的表达式具有

  • 常用子表达式
  • 多项式评估了昂贵的方法。

预定义这些(可能将它们添加到par []):

a = par[5]*par[4];
b =   (par[5]-1.);
c = b*(par[5]-2.)/2.;
d = c*(par[5]-3.)/3.;

例如,最长的表达式变为:

e = par[4]*Tterm;
value += a*(((d*e + c)*e + b)*e + 1.);

并简化其余部分。

如果表达式是曲线拟合的近似值,为什么不使用

value += -1./(*par)*exp(-1.*Tterm/(*par));

您还应该询问是否需要全部477M迭代。

答案 3 :(得分:0)

如果您想探索批处理/更多优化机会,以融合到依赖于这些值的计算中,请尝试使用Halide

我在这里用Halide重写了您的程序:

#include <Halide.h>
using namespace Halide;

class ParamReTtermApproxOpt : public Generator<ParamReTtermApproxOpt>
{
public:
    Input<Buffer<float>> tterm{"tterm", 1};
    Input<Buffer<float>> par{"par", 1};
    Input<int> ncpu{"ncpu"};

    Output<Buffer<float>> output{"output", 1};

    Var x;
    Func par_inv;

    void generate() {
        // precompute 1 / par[x]
        par_inv(x) = fast_inverse(par(x));

        // after che peak
        Expr after_che_peak = tterm(x) > 0;

        Expr first_term = -par_inv(0) * fast_exp(-tterm(x) * par_inv(0));
        Expr second_term = par(2) * par(1) * fast_pow(1 + par(1) * tterm(x), par(2) - 1);

        // before che peak

        Expr third_term = -par_inv(3) * fast_exp(-tterm(x) * par_inv(3));
        Expr fourth_term = par(5) * par(4) * fast_pow(1 + par(4) * tterm(x), par(5) - 1);

        // final value

        output(x) = 1.e9f * select(after_che_peak, first_term + second_term,
                                                   third_term + fourth_term);
    }

    void schedule() {
        par_inv.bound(x, 0, 6);
        par_inv.compute_root();

        Var xo, xi;
        // break x into two loops, one for ncpu tasks
        output.split(x, xo, xi, output.extent() / ncpu)
        // mark the task loop parallel
              .parallel(xo)
        // vectorize each thread's computation for 8-wide vector lanes
              .vectorize(xi, 8);

        output.print_loop_nest();
    }
};

HALIDE_REGISTER_GENERATOR(ParamReTtermApproxOpt, param_re_tterm_approx_opt)

我可以在Surface Book上用一秒钟多的时间运行477,000,000次迭代(ncpu = 4)。批处理在这里非常重要,因为它可以实现矢量化。

请注意,使用双精度算术编写的等效程序比浮点算法慢(20倍)。尽管Halide不提供双打的fast_版本,所以这可能不尽相同。无论如何,我都会检查您是否需要额外的精度。