为什么在多个线程上运行时快速内存写入比在单个线程上运行时花费更多时间?

问题描述 投票:0回答:1

我有一个程序,它分配一些内存(2亿个整数),进行一些快速计算,然后将数据写入分配的内存。

在单线程上运行时,该过程大约需要 1 秒。当跨 10 个线程运行时,该过程大约需要 77 秒。线程之间没有并发性。每个线程分配自己的数据并写入。

我机器上的缓存行是 64 位对齐的。我将数据写入一个 16 整数数组(分配在堆栈上)。然后通过memcpy将该数组刷新到内存中。

我的假设是这样的:

跨CPU的聚合处理速度比内存的速度更快。因此,内存速度是这里的瓶颈。

我故意以 64 位数据 (tmp_arr) 写入内存,以便缓存行不应该在线程之间共享。但我怀疑缓存行可能会出现抖动,因为多个线程正在尝试快速写入内存(CPU 计算量较低)。当我一次将单个整数写入内存而不是通过 memcpy 写入 16 个整数的数组时,时间为 250 秒。因此,通过 memcpy 一次将 16 个整数(64 位)推送到内存,速度提高了 3 倍。但77秒的时间比单线程执行多了近77倍。因此,一定会发生一些颠簸。

另一个观察结果是,单线程执行显示高停滞周期后端 (30%),而在十个线程上运行时我们看到高停滞周期前端 (39%)。有人可以解释一下为什么单线程的后端停顿而我们看到多线程的前端停顿吗?

我还尝试使用 _mm_store_si128 (向量化指令),但这并没有提高速度。

以下是与硬件计数器一起拍摄的时间的屏幕截图。

One thread timingsten thread timingsmachine diagram

我在 Linux 上运行并使用 AMD Ryzen 9 3900XT 12 核处理器。我使用的是 DDR4 内存,运行速度:3600 MT/s

编写此代码是为了了解在处理大型数据集的多线程应用程序中可以实现多快的内存写入速度。

这是代码:

//Had tried vectorized instructions but they had no noticeable performance improvements
//__m128i tmp_arr ;
//__m128i b = _mm_set_epi32(1,2,3,4);
//_mm_store_si128( (__m128i*) (arr + (i*16)), tmp_arr);
void runLargeMemoryAlloOutside(benchmark::State & s) {
    long size = 200'000'000; //200 million integers
    //each array is about 0.8GB
    //setting a pragma to alignas(64).
    //giving this pragma or not didn't have any noticeable effect on performance.
    alignas(64) int * arr = new int [size];
    //will write to this temporary array on stack. and then will flush this to the above array via memcpy.
    int tmp_arr[16];
    for (auto _ : s) {
        for (long i = 0; i < size/16; i++) {
            for (int j =0; j < 16; j++) {
                //very small compute.
                int tmp = rand();
                tmp = log(tmp);
                //arr[i] = tmp;
                tmp_arr[j] = tmp;
            }
            memcpy (arr + (i*64), tmp_arr, sizeof(tmp_arr));
        }
    }
    delete [] arr;
}

//running on 10 threads via Google benchmark
BENCHMARK(runLargeMemoryAlloOutside)->Threads(10);

代码使用 gcc (c++ 20) 使用标志“-Ofast -O5”进行编译。

我不完全理解为什么在 10 个线程上运行的进程时间会增加到单线程执行的 77 倍。并希望有人能够对这里时间的增加有所启发。还有什么可以加快内存写入速度。

我期望多线程执行不会那么慢。

memory-management heap-memory cpu-architecture cpu-cache
1个回答
0
投票

int tmp = rand();
- 这就是你的问题。 GNU/Linux (glibc)
rand()
在程序范围内共享一个种子状态,并受锁保护。因此,所有线程都会争夺该锁,从而使整体吞吐量比单个线程差得多。

无竞争的锁不好(当持有锁的缓存行在执行锁定和解锁的核心上保持修改状态时),但锁的竞争会慢很多。 将结果存储到内存中与将其添加到每个线程临时内存中的运行速度大致相同,因为循环体比内存带宽慢得多,尤其是在所有线程争夺锁的情况下。

浮点log也很慢,但也许你正在尝试做一些足够慢的事情,以至于一个线程无法接近饱和内存带宽,但也许所有核心都接近饱和?

如果你想要随机数,像

xorshift*
这样的简单廉价的 PRNG 可能会很好。 (

https://en.wikipedia.org/wiki/Xorshift#xorshift*

)。或者后来的一些变体。如果您试图故意使代码变慢而不是例如,这应该足以阻止编译器自动向量化随机数生成。只需填充

arr[i] = i;
。如果您想要快速随机数,xorshift+很容易矢量化。

     alignas(64) int * arr = new int [size];


我认为只是对齐指针对象本身的存储,而不是分配的空间。 (
alignas()
确实以字节计算,而不是位,因此这对于缓存行的大小来说是正确的。您在问题文本中写了“位”,但16x 32位
int

是64字节。)

检查编译器生成的 asm,看看编译器做了什么,它是否实际上存储到本地缓冲区然后复制到大数组,或者是否优化掉了 
memcpy
。 (如果它复制,它可能会

内联

memcpy
,因为大小很小且固定。就像四个xmm副本,因为您没有使用
-march=native
-march=x86-64-v3
来让它使用AVX 32字节向量.)
    

© www.soinside.com 2019 - 2024. All rights reserved.