【问题标题】:How to optimize the following common loop?如何优化以下公共循环?
【发布时间】:2015-12-24 14:20:36
【问题描述】:

我有代码

#include <iostream>
#include <vector>
#include <ctime>
using namespace std;

void foo(int n, double* a, double* b, double *c, double*d, double* e, double* f, double* g)
{
    for (int i = 0; i < n; ++i)
        a[i] = b[i] * a[i] + c[i] * (d[i] + e[i] + f[i] + g[i]);
}

int main()
{
    int m = 1001001;
    vector<double> a(m), b(m), c(m), d(m), f(m);

    clock_t start = std::clock();

    for (int i = 0; i < 1000; ++i)
        foo(1000000, &a[0], &b[0], &c[0], &d[0], &d[1], &f[0], &f[1000] );

    double duration = (std::clock() - start) / (double)CLOCKS_PER_SEC;
    cout << "Finished in " << duration << " seconds [CPU Clock] " << endl;
}

你能给我一个可行的例子来优化它以获得更好的性能吗?任何编译器都可以,例如 Intel c++ 编译器和 Visual c++ 编译器。请推荐一个性能良好的 CPU 来完成这项工作。

【问题讨论】:

  • 这个问题听起来很笼统,但我会给出一个具体的建议:您是否验证过代码是矢量化的?
  • 你可以试试 openmp #pragma omp parallel foren.wikipedia.org/wiki/OpenMP
  • 我使用visual c++,它是矢量化的。不确定我是否可以获得比编译器的自动矢量化更好的性能。
  • 取决于自动矢量化器在做什么,你能显示反汇编吗?
  • 可以改进您的代码的一件事是,如果您没有创建多个双精度类型的向量,而是创建一个具有包含双精度值的结构的向量。原因是对于一个循环迭代,CPU 总是需要获取五个不同缓存行中的值。此外,我会避免将相同的数组但不同的偏移量(d[0] vs d[1])传递给函数。这使优化器和读者更容易弄清楚发生了什么。

标签: c++ loops optimization vectorization multicore


【解决方案1】:

有问题的代码是无用的。它使用未初始化的变量进行大量计算,然后忽略结果。编译器在找出这类事情并删除所有代码方面变得越来越聪明。因此,如果这样的代码根本不需要任何时间,请不要感到惊讶。

在 C 中,您可以将指针声明为“const double* restrict”,除了 a 将是 double* restrict,告诉编译器除了第一个指针之外的所有指针都指向在运行期间不会被修改的数据环形;这允许编译器向量化。不幸的是,这不是 C++ 功能。

如果这是你真正的问题,你只需交换内循环和外循环,并像这样删除循环不变量:

void foo(int iter, int n, double* a, double* b, double *c, double*d, double* e, double* f, double* g)
{
    for (int i = 0; i < n; ++i) {
        double xa = a [i];
        double xb = b [i];
        double xr = c[i] * (d[i] + e[i] + f[i] + g[i]);

        for (int j = 0; j < iter; ++j)
            xa = xb * xa + xr;

        a [i] = xa;
    }
}

您可能会并行执行四次迭代以避免延迟。

但在现实生活中,您会观察到,在每次调用中,您读取的数据大约为 40MB,远远超出任何缓存。因此,您受到 RAM 速度的限制。通常的解决方案是将工作分成更小的部分,例如一次 500 个元素,因此所有内容都适合 L1 缓存,然后对相同的数据执行 1000 次操作。

【讨论】:

  • 实际上编译器不会优化循环。限制没有区别。问题是内存受限。重新排列数据以使块适合缓存将是一个合理的优化。
  • Richard,您是否检查了所有版本的所有 x86 编译器以做出给定的声明?作为旁注,此代码将受益于 FMA(AVX2,从 Haswell CPU 开始),尤其是在应用缓存阻塞(在给定答案中描述)之后。
【解决方案2】:

关于apple clang,我试过了:

  • 在参数上使用__restict__ 以使编译器相信没有别名。

结果:没有变化

  • 将计算分布在foo()中的8个线程上

结果:计算时间增加从 ~3 秒到 ~18 秒!

  • 使用#pragma omp parallel for

结果:编译器忽略了我并坚持原来的解决方案。约 3 秒。

  • 设置命令行选项-march=native 让cpu 的全部魅力闪耀

结果:不同的汇编器输出(应用了矢量化),但运行时间在 ~3s 时仍保持不变

初步结论:

此问题受内存访问限制,而不是 CPU 限制。

【讨论】:

  • 我发现我使用的是 DDR3 1600 RAM。如果我使用更好的 RAM,会有帮助吗?
【解决方案3】:

您可以尝试将向量预取到缓存行中,然后以 8 个为一组对它们进行操作(每个缓存行都适合 8 个双精度)。

确保在对 x[i] 到 x[i+7] 进行操作时,您正在预取 x[i+8] 到 x[i+15]。

这可能无济于事,因为您使用的加法和乘法速度太快,以至于您的 RAM 可能无论如何都跟不上。

【讨论】:

    【解决方案4】:

    我认为你应该使用多线程。更改 foo 以获取 fromIndex、toIndex,而不是 n 并在线程上分配向量。

    void foo(int fromIndex, int toIndex, double* a, double* b, double *c, double*d, double* e, double* f, double* g)
    {
        for (int i = fromIndex; i < toIndex; ++i)
            a[i] = b[i] * a[i] + c[i] * (d[i] + e[i] + f[i] + g[i]);
    }
    

    【讨论】:

    • 这没有考虑到不同索引处的值之间的(不幸的是被混淆了)依赖关系。例如,如果你反转循环计数器,你会得到不同的结果。
    • 另外,我试过了。它只会减慢算法的速度。
    • @UlrichEckhardt 我看不到 foo 函数中不同索引处的值之间的任何依赖关系!
    • 看看foo() 是如何被调用的,你在同一个数组中得到不同的偏移量。例如,foo() 中的de 都引用main() 中的d
    • @UlrichEckhardt 只有 'a' 发生变化,并且 'a[i]' 和 'a[i-x]' 之间没有依赖关系
    猜你喜欢
    • 2020-02-18
    • 2013-05-15
    • 2011-06-15
    • 1970-01-01
    • 2022-11-03
    • 2015-01-13
    • 1970-01-01
    • 2015-06-09
    • 2011-04-28
    相关资源
    最近更新 更多