【问题标题】:Threading and vectorisation optimisations线程和矢量化优化
【发布时间】:2018-08-19 15:35:46
【问题描述】:

我对代码优化技术真的很陌生,我目前正在尝试优化一段代码的循环部分,这应该很容易。

for (int i = 0; i < N; i++)
{
    array[i] = 0.0f;
    array2[i] = 0.0f;
    array3[i] = 0.0f;
}

我尝试如下实现矢量化和线程化:

int i;
int loop_unroll = (int) (N/4)*4;

#pragma omp parallel for shared(array,array2,array3)
for(int i = 0; i < loop_unroll; i+=4)
{
    __m128 array_vector = _mm_load_ps(array+i);
    array_vector = _mm_set1_ps(0.0f);

    _mm_store_ps(array+i, array_vector);
    _mm_store_ps(array2+i, array_vector);
    _mm_store_ps(array3+i, array_vector);
}

for(;i<N;i++)
{
    array[i] = 0.0f;
    array2[i] = 0.0f;
    array3[i] = 0.0f;
}

不管输入大小 N 我运行它,“优化”版本总是需要更长的时间。 我认为这是由于与设置线程和寄存器相关的开销,但是对于在程序变得太慢而无法使用之前的最大 N,更快的代码仍然不能减轻开销。

这让我怀疑所使用的优化技术是否实施不正确?

【问题讨论】:

  • 为什么一定要同时设置3个数组?
  • 这三个数组用于稍后在程序中计算力。这是第一步,只需将数组清零,以便稍后在程序中使用它们。为什么同时执行它们会对性能产生任何影响?肯定把它分成 3 个单独的 for 循环会更没有效率吗?
  • 这段代码会破坏优化器所做的工作。它不会使用 for 循环。请务必查看 -O2 生成的机器码。
  • Somewhat related。我不知道。
  • @HansPassant 道歉,忘了提到优化器没有被用于编译代码,所有基于编译器的优化都被禁用了。但是,如果我要激活它,我将如何查看 -O2 输出呢?

标签: multithreading performance optimization openmp intrinsics


【解决方案1】:

Compiling + benchmarking with optimization disabled is your main problem。具有内在函数的未优化代码通常非常慢。 在禁用优化的情况下比较内在函数与标量是没有用的,gcc -O0 通常对内在函数的伤害更大。

一旦您停止在未优化的代码上浪费时间,您将希望让 gcc -O3 将标量循环优化为 3 个 memset 操作,而不是将 3 个存储流交错在一个循环中。

编译器(和优化的 libc memset 实现)擅长优化内存归零,并且可能比简单的手动矢量化做得更好。不过,这可能还不算太糟糕,特别是如果您的阵列在 L1d 缓存中还不是很热的话。 (如果是,那么在具有宽数据路径的 CPU 上,仅使用 128 位存储比使用 256 位或 512 位向量要慢得多。)


我不确定使用 OpenMP 进行多线程的最佳方法是什么,同时仍让编译器优化到 memset。让omp parallel for 并行化存储来自每个线程的 3 个流的代码可能并不可怕,只要每个线程都在处理每个数组中的连续块。特别是如果稍后更新相同数组的代码将以相同的方式分布,因此每个线程都在处理之前归零的数组的一部分,并且可能在 L1d 或 L2 缓存中仍然很热。

当然,如果你能避免它,作为另一个有一些有用计算的循环的一部分,即时执行归零。如果您的下一步将是a[i] += something,那么在第一次通过数组时将其优化为a[i] = something,这样您就不需要先将其归零。


请参阅Enhanced REP MOVSB for memcpy 了解大量 x86 内存性能详细信息,尤其是“延迟绑定平台”部分,该部分解释了为什么大型 Xeon 上的单线程内存带宽(至 L3/DRAM)更差与四核桌面相比,即使当您有足够的线程使四通道内存饱和时,来自多个线程的聚合带宽可能要高得多。

为了存储性能(为以后的工作忽略缓存局部性),我认为最好让每个线程处理(一大块)1 个​​数组;单个顺序流可能是最大化单线程存储带宽的最佳方式。但是缓存是关联的,并且 3 个流足够少,以至于在你写完一整行之前它通常不会导致冲突丢失。不过,Desktop Skylake 的 L2 缓存只有 4 路关联。

存储多个流会产生 DRAM 页面切换效果,每个线程 1 个流可能优于每个线程 3 个流。因此,如果您确实希望每个线程将 3 个单独数组的块归零,理想情况下,您希望每个线程将其第一个数组的整个块,然后是第二个数组的整个块,而不是交错 3 个数组。

【讨论】:

  • Intel cpus 因为woodcrest 至少可以有效地处理6 个前向流(如果所有流都对齐的话)。在几年前的测试中,我发现通过在一个循环中组合最多可以提高 4 个流
  • 如果您有反对优化的规则,simd 内在函数可能比纯 C 代码更好。否则,只有在使用非临时存储、需要对齐所有数据时,内在函数才有优势。英特尔 C++ 具有非临时性的 pragma,但还需要特定指定相互数据对齐。 Nontemporal 应该在 memset 操作中提供接近两倍的性能。自 Woodcrest 以来,英特尔 CPU 可在每个循环中高效处理多达 6 个存储流。在几年前的测试中,与拆分循环相比,我发现多达 4 个流的可衡量收益。
  • @tim18: 一些memset 实现使用超过一定大小阈值的 NT 存储,但是,当您知道数据是否会很快被重新读取时,选择是否使用它们是个好主意.请参阅stackoverflow.com/questions/43343231/…(已在问题中链接)了解有关现代 x86 (Skylake) 上的 NT 商店的更多信息,以及它们的工作原理。
  • @tim18:关于多个流的有趣点。是单线程,还是多线程情况下每个线程的 4 个流?我没有看到任何建议memset 应该通过将目的地划分为宿舍和交叉商店来实现。
  • @tim18:如果您针对-O0 进行优化而不是正常编写它,则内在函数只有希望变得更好。即通过在单个语句中做尽可能多的事情。像 _mm_store_ps(array+i, _mm_setzero_ps()) 可能没问题,但不太可能使用单独的 __m128 变量,每次都必须加载。 (虽然它应该在 L1d 缓存中命中......)
猜你喜欢
  • 2023-03-27
  • 2016-02-11
  • 2020-07-17
  • 2017-06-18
  • 1970-01-01
  • 2013-05-01
  • 2018-07-02
  • 1970-01-01
相关资源
最近更新 更多