7

谷歌大神并没有向我解释一些循环优化问题。所以,很遗憾我没有足够的 Google-fu,我求助于 StackOverflow。

我正在优化用于求解特定微分方程组的 C 程序。在寻找数值解的过程中,我调用一个建立线性方程组的函数,然后调用一个函数来解决它。

求解函数最初在访问定义线性系统的数组对角线上的元素时存在瓶颈。因此,我包含了一个在系统初始化期间设置的一维数组,该数组保存沿数组对角线的值。

为了好玩,我继续使用初始化对角元素的代码,测量它所花费的时间并尝试不断改进代码。我尝试的版本导致了几个问题:

注意:我将我尝试过的所有版本都放在一个函数中,并分析了这个函数以查看时间花在了哪里。我将报告一个版本的执行时间占函数总时间的百分比。该功能被评估了数百万次。数字越小越好。

代码中使用的数据的相关声明:

/* quick definitions of the relevant variables, data is a struct */

static const int sp_diag_ind[98] = {2,12,23,76,120,129,137,142,.../* long list */};

double *spJ = &(data->spJ[0]);
/* data has double spJ[908] that represents a sparse matrix stored in triplet
*  form, I grab the pointer because I've found it to be more 
*  efficient than referencing data->spJ[x] each time I need it
*/

int iter,jter;
double *diag_data = NV_DATA_S(data->J_diag);
/* data->J_diag has a content field that has an array double diag_data[150]
*  NV_DATA_S is a macro to return the pointer to the relevant data
*/

我用于初始化 diag_data的原始循环。时间是评估的 16.1%(见注)。

/* try 1 */
for (iter = 0; iter<3; iter++) {
    diag_data[iter] = 0; 
}
jter = 0;
for (iter = 3; iter<101; iter++) { // unaligned loop start
    diag_data[iter] = spJ[sp_diag_ind[jter]];
    jter++; // heavy line for loop
}

for (iter = 101; iter<150; iter++) {
    diag_data[iter] = 0; 
}

总而言之,我们抓取指向对角线的指针,将一些组件设置为零(根据我使用的算法,这不是可选的),然后抓取驻留在以稀疏形式表示的“数组”的对角线上的值通过 spJ。由于 spJ 是 150x150 数组(大部分为零)的 908 个非零的一维数组,我们必须使用查找来查找 spJ 中对角线元素的位置。此查找由 98 元素数组 sp_diag_ind 定义。

我试图删除 jter 的使用,因为它显示为不可自由增加。我第二次尝试的中间循环:

for (iter = 0; iter<98; iter++) { // unaligned loop start
    diag_data[iter+3] = spJ[sp_diag_ind[iter]];
}

这使情况有所改善。此版本的时间为 15.6%。但是当我查看 Shark 对此代码的分析(Mac 上 XCode 附带的工具)时,它警告我这是一个未对齐的循环。

第三次改进的尝试是删除“归零”循环并使用 memset 将 diag_data 归零:

memset(diag_data, '\0', sizeof(diag_data));

for (iter = 0; iter<98; iter++) { // unaligned loop start
    diag_data[iter+3] = spJ[sp_diag_ind[iter]];
}

时间为 14.9%。不确定什么是未对齐的循环,我继续摆弄。我发现了一个改进的第四个实现,使用指针在 diag_data 和 spJ[crazy index] 之间进行对齐偏移:

realtype * diag_mask = &diag_data[3];
for (iter = 0; iter<98; iter++) { // unaligned loop start
    diag_mask[iter] = spJ[sp_diag_ind[iter]];
}

使用 diag_mask 可以稍微提高速度。它以 13.1% 的比例出现。

编辑:原来这部分比我最初想象的更愚蠢。iter 的使用是未定义的。支持@caf 和@rlibby 来捕捉它

最后,我尝试了一些我认为很愚蠢的方法:

memset(diag_data, '\0', sizeof(diag_data));

for (iter = 0; iter<98;) {
    diag_mask[iter] = spJ[sp_diag_ind[iter++]];
}

时间为 10.9%。此外,当我查看带注释的源代码时,Shark 不会发出未对齐循环警告。 结束愚蠢的部分

所以,我的问题:

  1. 什么是未对齐的循环?
  2. 为什么第五个实现是一致的,而第四个不是?
  3. 是否有一个对齐的循环负责在我的第四个和第五个实现之间提高执行速度,或者是否将增量步骤嵌入到 sp_diag_ind 的值的查找中?
  4. 你看到我可以做的任何其他改进吗?

谢谢您的帮助。

- 安德鲁

4

3 回答 3

2

未对齐循环是第一条指令不在特定边界(16 或 32 的倍数)上开始的循环。应该有一个编译器标志来对齐循环;它可能会或可能不会有助于性能。没有标志的循环是否对齐取决于它之前的指令,因此它是不可预测的。您可以尝试的另一种优化是将 、 和 as 标记diag_maskspJsp_diag_indC99restrict功能)。这表明它们没有别名,并且可能有助于编译器更好地优化循环。不过,98 的计数可能太小而看不到任何效果。

于 2011-02-26T03:19:59.337 回答
1

你看到我可以做的任何其他改进吗?

您正在从大约 11% 的时间使用的东西中调整日光。剩下的 89% 中没有什么可以优化的吗?

于 2011-02-26T04:16:16.480 回答
1

您的第五个版本不正确 - 它具有未定义的行为,因为它修改iter和引用其值,其目的不是计算新值,而没有中间序列点。

您是否尝试在计算时存储对角线的实际,而不是它们的索引?然后你可以直接将它们复制到(或者,甚至更好,直接使用对角线向量)。spJsp_diag_ind[]diag_data


C 标准的相关部分是 §6.5 表达式:

'2。在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的评估修改一次。此外,应仅读取先验值以确定要存储的值。

这适用于iter表达式中的对象。违反“应”约束是未定义的行为。

gcc(使用 4.4.5 版测试)甚至会警告您的表达:

x.c:16: warning: operation on ‘iter’ may be undefined
于 2011-02-26T03:43:04.797 回答