我正在使用Gauss-Seidel方法编写稀疏矩阵求解器。通过剖析,我已经确定我的程序的大约一半时间花在解算器中。性能关键部分如下:
size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
for (size_t y = 1; y < d_ny - 1; ++y) {
for (size_t x = 1; x < d_nx - 1; ++x) {
d_x[ic] = d_b[ic]
- d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
- d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
++ic; ++iw; ++ie; ++is; ++in;
}
ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}
所涉及的所有数组都是float
类型。实际上,它们不是数组,而是具有重载[]
运算符的对象,我认为它应该被优化掉,但定义如下:
inline float &operator[](size_t i) { return d_cells[i]; }
inline float const &operator[](size_t i) const { return d_cells[i]; }
对于d_nx = d_ny = 128
,这可以在Intel i7 920上以每秒3500次运行。这意味着内部循环体每秒运行3500 * 128 * 128 = 5700万次。由于只涉及一些简单的算法,因此对于2.66 GHz处理器来说,这个数字很低。
也许它不受CPU功率的限制,而是受内存带宽的限制?好吧,一个128 * 128 float
阵列吃65 kB,所以所有6个阵列都应该很容易适应CPU的L3缓存(8 MB)。假设寄存器中没有任何缓存,我在内部循环体中计算了15次内存访问。在64位系统上,这是每次迭代120个字节,因此5700万* 120字节= 6.8 GB / s。 L3缓存运行在2.66 GHz,因此它具有相同的数量级。我的猜测是记忆确实是瓶颈。
为了加快速度,我尝试了以下方法:
与g++ -O3
汇编。 (好吧,我从一开始就这样做。)
使用OpenMP pragma并行化4个核心。我必须改为Jacobi算法,以避免从同一个数组读取和写入。这需要我做两次迭代,导致大约相同速度的净结果。
摆弄循环体的实现细节,例如使用指针而不是索引。没效果。
加快这个人加速的最佳方法是什么?是否有助于重新组装内部主体(我必须先学习)?我应该在GPU上运行它(我知道该怎么做,但是这样麻烦)?还有其他好主意吗?
(N.B。我做取“否”作为答案,如:“它不能明显加快,因为......”)
更新:根据要求,这是一个完整的程序:
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;
size_t d_nx = 128, d_ny = 128;
float *d_x, *d_b, *d_w, *d_e, *d_s, *d_n;
void step() {
size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
for (size_t y = 1; y < d_ny - 1; ++y) {
for (size_t x = 1; x < d_nx - 1; ++x) {
d_x[ic] = d_b[ic]
- d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
- d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
++ic; ++iw; ++ie; ++is; ++in;
}
ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}
}
void solve(size_t iters) {
for (size_t i = 0; i < iters; ++i) {
step();
}
}
void clear(float *a) {
memset(a, 0, d_nx * d_ny * sizeof(float));
}
int main(int argc, char **argv) {
size_t n = d_nx * d_ny;
d_x = new float[n]; clear(d_x);
d_b = new float[n]; clear(d_b);
d_w = new float[n]; clear(d_w);
d_e = new float[n]; clear(d_e);
d_s = new float[n]; clear(d_s);
d_n = new float[n]; clear(d_n);
solve(atoi(argv[1]));
cout << d_x[0] << endl; // prevent the thing from being optimized away
}
我按如下方式编译并运行它:
$ g++ -o gstest -O3 gstest.cpp
$ time ./gstest 8000
0
real 0m1.052s
user 0m1.050s
sys 0m0.010s
(它每秒执行8000而不是3500次迭代,因为我的“真实”程序也做了很多其他的事情。但它具有代表性。)
更新2 :我被告知单元化值可能不具代表性,因为NaN和Inf值可能会减慢速度。现在清除示例代码中的内存。不过,这对我的执行速度没有任何影响。
答案 0 :(得分:5)
我想我已经设法优化它,这是一个代码,在VC ++中创建一个新项目,添加这个代码并简单地在“Release”下编译。
#include <iostream>
#include <cstdlib>
#include <cstring>
#define _WIN32_WINNT 0x0400
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <conio.h>
using namespace std;
size_t d_nx = 128, d_ny = 128;
float *d_x, *d_b, *d_w, *d_e, *d_s, *d_n;
void step_original() {
size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
for (size_t y = 1; y < d_ny - 1; ++y) {
for (size_t x = 1; x < d_nx - 1; ++x) {
d_x[ic] = d_b[ic]
- d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
- d_s[ic] * d_x[is] - d_n[ic] * d_x[in];
++ic; ++iw; ++ie; ++is; ++in;
}
ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}
}
void step_new() {
//size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
float
*d_b_ic,
*d_w_ic,
*d_e_ic,
*d_x_ic,
*d_x_iw,
*d_x_ie,
*d_x_is,
*d_x_in,
*d_n_ic,
*d_s_ic;
d_b_ic = d_b;
d_w_ic = d_w;
d_e_ic = d_e;
d_x_ic = d_x;
d_x_iw = d_x;
d_x_ie = d_x;
d_x_is = d_x;
d_x_in = d_x;
d_n_ic = d_n;
d_s_ic = d_s;
for (size_t y = 1; y < d_ny - 1; ++y)
{
for (size_t x = 1; x < d_nx - 1; ++x)
{
/*d_x[ic] = d_b[ic]
- d_w[ic] * d_x[iw] - d_e[ic] * d_x[ie]
- d_s[ic] * d_x[is] - d_n[ic] * d_x[in];*/
*d_x_ic = *d_b_ic
- *d_w_ic * *d_x_iw - *d_e_ic * *d_x_ie
- *d_s_ic * *d_x_is - *d_n_ic * *d_x_in;
//++ic; ++iw; ++ie; ++is; ++in;
d_b_ic++;
d_w_ic++;
d_e_ic++;
d_x_ic++;
d_x_iw++;
d_x_ie++;
d_x_is++;
d_x_in++;
d_n_ic++;
d_s_ic++;
}
//ic += 2; iw += 2; ie += 2; is += 2; in += 2;
d_b_ic += 2;
d_w_ic += 2;
d_e_ic += 2;
d_x_ic += 2;
d_x_iw += 2;
d_x_ie += 2;
d_x_is += 2;
d_x_in += 2;
d_n_ic += 2;
d_s_ic += 2;
}
}
void solve_original(size_t iters) {
for (size_t i = 0; i < iters; ++i) {
step_original();
}
}
void solve_new(size_t iters) {
for (size_t i = 0; i < iters; ++i) {
step_new();
}
}
void clear(float *a) {
memset(a, 0, d_nx * d_ny * sizeof(float));
}
int main(int argc, char **argv) {
size_t n = d_nx * d_ny;
d_x = new float[n]; clear(d_x);
d_b = new float[n]; clear(d_b);
d_w = new float[n]; clear(d_w);
d_e = new float[n]; clear(d_e);
d_s = new float[n]; clear(d_s);
d_n = new float[n]; clear(d_n);
if(argc < 3)
printf("app.exe (x)iters (o/n)algo\n");
bool bOriginalStep = (argv[2][0] == 'o');
size_t iters = atoi(argv[1]);
/*printf("Press any key to start!");
_getch();
printf(" Running speed test..\n");*/
__int64 freq, start, end, diff;
if(!::QueryPerformanceFrequency((LARGE_INTEGER*)&freq))
throw "Not supported!";
freq /= 1000000; // microseconds!
{
::QueryPerformanceCounter((LARGE_INTEGER*)&start);
if(bOriginalStep)
solve_original(iters);
else
solve_new(iters);
::QueryPerformanceCounter((LARGE_INTEGER*)&end);
diff = (end - start) / freq;
}
printf("Speed (%s)\t\t: %u\n", (bOriginalStep ? "original" : "new"), diff);
//_getch();
//cout << d_x[0] << endl; // prevent the thing from being optimized away
}
像这样运行:
app.exe 10000 o
app.exe 10000 n
“o”表示旧代码,您的。
“n”是我的,新的。
我的结果: 速度(原创):
1515028
1523171
1495988
速度(新):
966012
984110
1006045
改善约30%。
背后的逻辑: 您一直在使用索引计数器来访问/操作。 我用指针。 在VC ++的调试器中的某个计算代码行上运行断点,然后按F8。你会得到反汇编窗口。 您将看到生成的操作码(汇编代码)。
无论如何,看:
int * x = ...;
x [3] = 123;
这告诉PC将指针x放在寄存器(比如说EAX)。 添加它(3 * sizeof(int))。 只有这样,将值设置为123。
你可以理解,指针方法要好得多,因为我们削减了添加过程,实际上我们自己处理它,因此能够根据需要进行优化。
我希望这会有所帮助。
stackoverflow.com员工的旁注: 伟大的网站,我希望我早就听说过它!
答案 1 :(得分:5)
一些想法:
使用SIMD。您可以从每个阵列一次加载4个浮点数到SIMD寄存器(例如Intel上的SSE,PowerPC上的VMX)。这样做的缺点是某些d_x值将“陈旧”,因此您的收敛速度将受到影响(但不会像jacobi迭代那样糟糕);很难说加速是否会抵消它。
使用SOR。它很简单,不会增加太多计算,并且可以很好地提高收敛速度,即使是相对保守的放松值(比如说1.5)。
使用共轭梯度。如果这是流体模拟的投影步骤(即强制执行非压缩性),您应该能够应用CG并获得更好的收敛速度。一个好的预处理器可以提供更多帮助。
使用专门的解算器。如果线性系统来自Poisson equation,则可以使用基于FFT的方法比共轭梯度更好。
如果您可以解释更多关于您要解决的系统的内容,我可以就#3和#4提供更多建议。
答案 2 :(得分:3)
首先,这里似乎存在一个流水线问题。循环读取刚刚写入的d_x
中的值,但显然它必须等待该写入完成。只需重新排列计算的顺序,在等待时做一些有用的事情,使它几乎快两倍:
d_x[ic] = d_b[ic]
- d_e[ic] * d_x[ie]
- d_s[ic] * d_x[is] - d_n[ic] * d_x[in]
- d_w[ic] * d_x[iw] /* d_x[iw] has just been written to, process this last */;
Eamon Nerbonne是谁发现了这一点。很多人对他赞不绝口!我永远不会猜到。
答案 3 :(得分:2)
Poni的答案看起来对我来说是对的。
我只是想指出,在这类问题中,经常从内存局部性中获益。现在,b,w,e,s,n
数组都位于内存中的不同位置。如果你不能够解决L3缓存中的问题(主要是在L2中),那么这将是不好的,这种解决方案会有所帮助:
size_t d_nx = 128, d_ny = 128;
float *d_x;
struct D { float b,w,e,s,n; };
D *d;
void step() {
size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
for (size_t y = 1; y < d_ny - 1; ++y) {
for (size_t x = 1; x < d_nx - 1; ++x) {
d_x[ic] = d[ic].b
- d[ic].w * d_x[iw] - d[ic].e * d_x[ie]
- d[ic].s * d_x[is] - d[ic].n * d_x[in];
++ic; ++iw; ++ie; ++is; ++in;
}
ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}
}
void solve(size_t iters) { for (size_t i = 0; i < iters; ++i) step(); }
void clear(float *a) { memset(a, 0, d_nx * d_ny * sizeof(float)); }
int main(int argc, char **argv) {
size_t n = d_nx * d_ny;
d_x = new float[n]; clear(d_x);
d = new D[n]; memset(d,0,n * sizeof(D));
solve(atoi(argv[1]));
cout << d_x[0] << endl; // prevent the thing from being optimized away
}
例如,1280x1280的这个解决方案比Poni的解决方案略低于2x (在我的测试中13s vs 23s - 你的原始实现是22s),而在128x128则是30% 较慢(7s vs. 10s - 你原来是10s)。
(对于基本案例,迭代次数扩大到80000次,对于100x大型案例1280x1280,迭代次数扩大到800次。)
答案 4 :(得分:1)
我认为你对记忆是一个瓶颈是正确的。这是一个非常简单的循环,每次迭代只需要一些简单的算术。 ic,iw,ie,is,并且在索引中似乎位于矩阵的两侧,所以我猜测那里有一堆缓存未命中。
答案 5 :(得分:1)
我不是这方面的专家,但我已经看到了there are several academic papers关于改进Gauss-Seidel方法的缓存使用情况。
另一种可能的优化是使用红黑变体,其中点以棋盘式模式在两次扫描中更新。通过这种方式,扫描中的所有更新都是独立的,可以并行化。
答案 6 :(得分:0)
我建议加入一些预取语句,并研究“面向数据的设计”:
void step_original() {
size_t ic = d_ny + 1, iw = d_ny, ie = d_ny + 2, is = 1, in = 2 * d_ny + 1;
float dw_ic, dx_ic, db_ic, de_ic, dn_ic, ds_ic;
float dx_iw, dx_is, dx_ie, dx_in, de_ic, db_ic;
for (size_t y = 1; y < d_ny - 1; ++y) {
for (size_t x = 1; x < d_nx - 1; ++x) {
// Perform the prefetch
// Sorting these statements by array may increase speed;
// although sorting by index name may increase speed too.
db_ic = d_b[ic];
dw_ic = d_w[ic];
dx_iw = d_x[iw];
de_ic = d_e[ic];
dx_ie = d_x[ie];
ds_ic = d_s[ic];
dx_is = d_x[is];
dn_ic = d_n[ic];
dx_in = d_x[in];
// Calculate
d_x[ic] = db_ic
- dw_ic * dx_iw - de_ic * dx_ie
- ds_ic * dx_is - dn_ic * dx_in;
++ic; ++iw; ++ie; ++is; ++in;
}
ic += 2; iw += 2; ie += 2; is += 2; in += 2;
}
}
这与第二种方法不同,因为在执行计算之前将值复制到本地临时变量。