我正在尝试加速许多文件中的一大段代码,结果发现一个函数占用了大约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;
因此,我尝试根据其预设插入数字,但是未能找到任何可节省的时间。
pow
和exp
函数似乎每次都被调用,它们分别占总时间的40%和20%,因此,仅占总时间的10%。该函数中不是pow
或exp
的部分。寻找加快速度的方法可能是最好的,但是pow
中使用的指数中没有一个是-4
以外的整数,而且我不知道1/(x*x*x*x)
是否比{{1} }。
pow(x, -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。
然后在每种情况下我们要做两件事以增加价值。在这两种情况下,对于较小的情况,我们都使用多项式;对于较大的情况,我们都使用指数/幂方程。
作为猜测,这是因为多项式是小值指数的一个体面近似值-错误是可以接受的。您应该做的就是确认这一猜测-看一下基于“大”幂/指数的方程式的泰勒级数展开式,看它是否与多项式一致。或用数字检查。
如果是这种情况,则意味着该等式具有可接受的已知误差量。通常,exp
或pow
的更快版本具有已知的最大错误量;考虑使用那些。
如果不是这种情况,仍然可能会出现可接受的误差量,但是泰勒级数逼近可以为您提供“在代码中”可接受的误差量信息。
我下一步要做的是将这个等式的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_
版本,所以这可能不尽相同。无论如何,我都会检查您是否需要额外的精度。