为什么我从具有更大阵列的 SIMD 内在函数中获得更大的相对加速比与标量?

Why do I get a larger relative speedup vs. scalar from SIMD intrinsics with larger arrays?

我想学习 SIMD 编程。现在我的代码中有一些有趣的时刻。

我只是想测量我的代码的工作时间。我尝试为具有特定大小的数组应用一些基本函数。

首先我尝试使用用 SIMD 指令编写的函数,然后我尝试使用通常的方法。我比较了这两个实现相同功能的时间。

我将性能定义为(没有 sse 的时间)/(使用 sse 的时间)。

但是当我的尺寸为 8 时,我的性能为 1.3,当我的尺寸 = 512 - 我的性能 = 3,如果我的尺寸 = 1000,性能 = 4,如果尺寸 = 4000 -> 性能 = 5 .

我不明白为什么我的性能会随着数组大小的增加而增加。

我的代码

void init(double* v, size_t size) {
    for (int i = 0; i < size; ++i) {
        v[i] = i / 10.0;
    }
}

void sub_func_sse(double* v, int start_idx) {
    __m256d vector = _mm256_loadu_pd(v + start_idx);
    __m256d base = _mm256_set_pd(2.0, 2.0, 2.0, 2.0);
    for (int i = 0; i < 128; ++i) {
        vector = _mm256_mul_pd(vector, base);
    }
    _mm256_storeu_pd(v + start_idx, vector);
}

void sub_func(double& item) {
    for (int k = 0; k < 128; ++k) {
        item *= 2.0;
    }
}

int main() {
    const size_t size = 8;
    double* v = new double[size];
    init(v, size);
    const int num_repeat = 2000;//I should repeat my measuraments 
                               //because I want to get average time - it is more clear information
    double total_time_sse = 0;
    for (int p = 0; p < num_repeat; ++p) {
        init(v, size);
        TimerHc t;
        t.restart();
        for (int i = 0; i < size; i += 8) {
            sub_func_sse(v, i);
        }
        total_time_sse += t.toc();
    }

    double total_time = 0;
    for (int p = 0; p < num_repeat; ++p) {
        init(v, size);
        TimerHc t;
        t.restart();
        for (int i = 0; i < size; ++i) {
            sub_func(v[i]);
        }
        total_time += t.toc();
    }
    std::cout << "time using sse = " << total_time_sse / num_repeat << std::endl <<
        "time without sse = " << total_time / num_repeat << std::endl;
    system("pause");
}

I defined performance like (time without sse) / (time using sse).

你衡量的是加速。

通过 Amdahl's law 模拟了您可以通过应用并行化获得的加速。它将那些可以(通过并行化或其他方式)变得更快的部分的节省与总加速比联系起来。 Amdahl 定律可能相当吓人,因为它基本上表明,使零件更快并不总能使您获得总加速。可实现加速的限制取决于可以并行化的工作负载的相对比例。

Gustavon's law 持不同观点。简而言之,它指出您只需增加工作负载即可有效利用并行化。总体上更多的工作负载通常对并行化和计算的非并行部分的开销影响较小,因此(根据 Amdahl 定律)导致更有效地使用并行性。

...从某种意义上说,这就是您在这里观察到的。数组越大,并行化的影响就越大。

PS:这只是一些手势,可以解释为什么您看到的效果并不太令人惊讶。幸运的是,还有另一个答案可以更详细地解决您的特定基准。

您可能是 CPU frequency scaling 的受害者;为了获得稳定的结果,您应该禁用 动态频率缩放 涡轮增压 ,或者至少在开始测量之前预热 CPU。

由于是先测SSE性能,然后再进行常规性能,一开始CPU频率较低,所以SSE性能显得更差。

话虽如此,您的方法还有一些其他问题:

  • high_frequency_clock::now() 调用的开销与正在测量的工作相比很高;将时间测量移动到 for (..num_repeat 循环之外,即为整个循环计时,而不是单独的迭代(然后可选择将测量时间除以迭代次数)。

  • 计算结果从未被使用;编译器可以自由地完全优化工作。确保 "use" 结果,例如通过打印它。

  • 将双精度数乘以 2.0 的效率非常低。事实上,非 SSE 版本被优化为 ADD(item *= 2.0 ==> vaddsd xmm0, xmm0, xmm0)。所以你手作的SSE版本输了

  • 优化编译器可能会自动矢量化您的非 SSE 代码。可以肯定的是,请始终检查生成的程序集。 Link to godbolt

  • 使用像 Google Benchmark 这样的基准测试框架;它将帮助您避免与代码基准测试相关的许多陷阱。