提高了一个非常简单但用途广泛的函数(计算晶格中原子的邻居)的效率

improving the efficiency of a very simple, but much used function (counting neighbours of atoms in a lattice)

本文关键字:计算 函数 效率 原子的 邻居 一个 非常 简单      更新时间:2023-10-16

我在这里相对较新,但我想如果有人能帮忙,那一定是这里的人。

我们正在做一个大规模的原子晶格模拟程序,这个非常简单的函数被使用了很多次,大大减缓了这个过程。

它只是检查周期晶格中一个位置的8个邻居(我们在BCC中)的类型(称为晶格的结构的三维矢量的一部分,包括表示原子类型的整数t)。

我意识到可能不可能把它简化得更多,但如果有人有任何灵感,请告诉我,谢谢!

//calculates number of neighbours that aren't vacancies
int neighbour(int i, int j, int k)
{
//cout << "neighbour" << endl;
int n = 0;
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][(j+1) & (LATTICE_SIZE-1)][k].t)
{
    n++;
}
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][(j-1) & (LATTICE_SIZE-1)][k].t)
{
    n++;
}
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][j][k+1].t)
{
    n++;
}
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][j][k-1].t)
{
    n++;
}
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][(j+1) & (LATTICE_SIZE-1)][k].t)
{
    n++;
}
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][(j-1) & (LATTICE_SIZE-1)][k].t)
{
    n++;
}
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][j][k+1].t)
{
    n++;
}
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][j][k-1].t)
{
    n++;
}
return n;
}

首先,只有当lattice_SIZE是2的幂时,使用中的晶格包裹解决方案((i+1) & (LATTICE_SIZE-1))才能正常工作。例如如果LATTICE_SIZE == 100i == 99(i+1)&(LATTICE_SIZE-1) == 100 & 99 == 0x64 & 0x63 == 0x60 == 96,而期望值为0。

鉴于此,我建议您检查多维数组索引与编译器和平台的工作方式。在LATTICE_ SIZE等于2的幂的情况下,第n个索引的乘法可以有效地用左移代替,在一些架构上左移明显更快。VC++11自动进行此优化,然而,我不知道你的编译器是什么,也不能假设它也能做到这一点。

想到的另一个改进是尽量避免重新计算高阶索引的偏移量。如果我们将相同的组划分得更高,优化器可以帮助实现这一目标将索引排列在一起。我只通过对表达式进行排序就实现了这一点:

if (V != lattice[(i+1) & (LATTICE_SIZE-1)][(j+1) & (LATTICE_SIZE-1)][k  ].t)  n++;
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][j                       ][k+1].t)  n++;
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][j                       ][k-1].t)  n++;
if (V != lattice[(i+1) & (LATTICE_SIZE-1)][(j-1) & (LATTICE_SIZE-1)][k  ].t)  n++;
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][(j+1) & (LATTICE_SIZE-1)][k  ].t)  n++;
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][j                       ][k+1].t)  n++;
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][j                       ][k-1].t)  n++;
if (V != lattice[(i-1) & (LATTICE_SIZE-1)][(j-1) & (LATTICE_SIZE-1)][k  ].t)  n++;

我的优化器利用了这一点,最终的加速率只有4%。然而,对于您的系统来说,它可能会归结为不同的值。

此外,大部分优化实际上取决于函数的使用。例如,我写了一个简单的测试:

volatile int n = 0;
for ( int i = 0; i != LATTICE_SIZE; ++i )
    for ( int j = 0; j != LATTICE_SIZE; ++j )
        for ( int k = 0; k != LATTICE_SIZE; ++k )
            n += neighbour ( i, j, k );

我的测量显示,每个邻居()呼叫大约12纳秒。在那之后,我注意到邻居只接受了两架高级飞机的检查。我重构了函数,为优化器提供了更明确的提示:

int neighbour_in_plane ( elem_t l[LATTICE_SIZE][LATTICE_SIZE], int j, int k )
{
    int n = 0;
    if (V != l[(j-1) & (LATTICE_SIZE-1)][k  ].t)  n++;
    if (V != l[j                       ][k-1].t)  n++;
    if (V != l[j                       ][k+1].t)  n++;
    if (V != l[(j+1) & (LATTICE_SIZE-1)][k  ].t)  n++;
    return n;
}
//calculates number of neighbours that aren't vacancies
int neighbour(int i, int j, int k)
{
    return neighbour_in_plane ( lattice[(i-1) & (LATTICE_SIZE-1)], i, j ) +
           neighbour_in_plane ( lattice[(i+1) & (LATTICE_SIZE-1)], i, j );
}

令人惊讶的是,每次通话只有4 ns。我检查了编译器的输出,发现这一次它已经将两个函数内联到调用循环中,并生成了一个数字例如,它有效地将两个内部循环移动到neighbor_in_plane()函数中,从而避免了对CCD_ 5表达式。

最重要的是,你必须在你的代码+编译器+平台环境中使用这个函数,才能最大限度地提高它的速度

我假设LATTICE_SIZE是2的幂。另外,& (LATTICE_SIZE-1)不会按照你的意愿进行包装。此外,我注意到,"k"维度没有包装。这是故意的吗?

然后,在C++中,该代码的执行时间将在很大程度上取决于您的"晶格"是什么类型的"数组",以及比较V!=lattice[i][i][k].t是。通常,嵌套的std::vector或boost::multi_array可能比传统的"C"数组慢得多:lattice lattice[lattice_SIZE][lattice_IZE][lattice_SIZE]

如果你可以在所有三个维度上留下一个空的边界(基本上是一个空表面),那么这可能有助于提高效率,因为你可以用& (LATTICE_SIZE-1)省去所有的环绕。如果您有一个编译时常数LATTICE_SIZE,那么在没有这些包装的情况下,像LATTICE[i-1][j][k+1]这样的精确索引的计算要快得多,因为编译器可以确定各种数组访问之间的常量偏移。

最后,但同样重要的是,我建议您查看该函数生成的汇编程序输出(只需使用-S开关进行编译,然后查看生成的.S文件)。如果编译器将"If"转换为"n++"(转换为inc %reg)周围的条件跳跃,那么这就为进一步优化留下了空间,因为条件跳跃往往会被CPU预测错误,然后导致大量额外的时钟周期。如果使用了cmov,或者如果"If"的结果通过条件集指令(如setc或setg)转换为寄存器,则代码可能已经更接近最优。为了帮助编译器在英特尔x86上有效地使用setxx操作,您可以尝试将结果计数"n"转换为"无符号字符"。