memset 与绑定到每个物理内核的线程并行

Posted

技术标签:

【中文标题】memset 与绑定到每个物理内核的线程并行【英文标题】:memset in parallel with threads bound to each physical core 【发布时间】:2014-10-18 10:52:39 【问题描述】:

我一直在 In an OpenMP parallel code, would there be any benefit for memset to be run in parallel? 测试代码,我发现了一些意想不到的东西。

我的系统是单插槽 Xeon E5-1620,它是具有 4 个物理内核和 8 个超线程的 Ivy Bridge 处理器。我正在使用 Ubuntu 14.04 LTS、Linux 内核 3.13、GCC 4.9.0 和 EGLIBC 2.19。我用gcc -fopenmp -O3 mem.c编译

当我在链接中运行代码时,它默认为八个线程并给出

Touch:   11830.448 MB/s
Rewrite: 18133.428 MB/s

但是,当我绑定线程并将线程数设置为这样的物理内核数时

export OMP_NUM_THREADS=4 
export OMP_PROC_BIND=true

我明白了

Touch:   22167.854 MB/s
Rewrite: 18291.134 MB/s

点击率翻倍!绑定后运行几次总是比重写快。我不明白这一点。 绑定线程并设置物理核数后为什么touch比rewrite快?为什么触摸率翻了一番?

这是我使用的代码,未经修改从 Hristo Iliev 答案中获取。

#include <stdio.h>
#include <string.h>
#include <omp.h>

void zero(char *buf, size_t size)

    size_t my_start, my_size;

    if (omp_in_parallel())
    
        int id = omp_get_thread_num();
        int num = omp_get_num_threads();

        my_start = (id*size)/num;
        my_size = ((id+1)*size)/num - my_start;
    
    else
    
        my_start = 0;
        my_size = size;
    

    memset(buf + my_start, 0, my_size);


int main (void)

    char *buf;
    size_t size = 1L << 31; // 2 GiB
    double tmr;

    buf = malloc(size);

    // Touch
    tmr = -omp_get_wtime();
    #pragma omp parallel
    
        zero(buf, size);
    
    tmr += omp_get_wtime();
    printf("Touch:   %.3f MB/s\n", size/(1.e+6*tmr));

    // Rewrite
    tmr = -omp_get_wtime();
    #pragma omp parallel
    
        zero(buf, size);
    
    tmr += omp_get_wtime();
    printf("Rewrite: %.3f MB/s\n", size/(1.e+6*tmr));

    free(buf);

    return 0;

编辑: 没有胎面绑定,但在这里使用四个线程是运行八次的结果。

Touch:   14723.115 MB/s, Rewrite: 16382.292 MB/s
Touch:   14433.322 MB/s, Rewrite: 16475.091 MB/s 
Touch:   14354.741 MB/s, Rewrite: 16451.255 MB/s  
Touch:   21681.973 MB/s, Rewrite: 18212.101 MB/s 
Touch:   21004.233 MB/s, Rewrite: 17819.072 MB/s 
Touch:   20889.179 MB/s, Rewrite: 18111.317 MB/s 
Touch:   14528.656 MB/s, Rewrite: 16495.861 MB/s
Touch:   20958.696 MB/s, Rewrite: 18153.072 MB/s

编辑:

我在另外两个系统上测试了这段代码,但我无法在它们上重现问题

i5-4250U (Haswell) - 2 个物理核心,4 个超线程

4 threads unbound
    Touch:   5959.721 MB/s, Rewrite: 9524.160 MB/s
2 threads bound to each physical core
    Touch:   7263.175 MB/s, Rewrite: 9246.911 MB/s

四个插槽 E7- 4850 - 10 个物理内核,每个插槽 20 个超线程

80 threads unbound
    Touch:   10177.932 MB/s, Rewrite: 25883.520 MB/s
40 threads bound
    Touch:   10254.678 MB/s, Rewrite: 30665.935 MB/s

这表明将线程绑定到物理内核确实改善了触摸和重写,但在这两个系统上触摸比重写慢。

我还测试了 memset 的三种不同变体:my_memsetmy_memset_streamA_memset。 函数my_memsetmy_memset_stream 定义如下。函数A_memset来自Agner Fog的asmlib。

my_memset 结果:

Touch:   22463.186 MB/s
Rewrite: 18797.297 MB/s

我认为这表明问题不在 EGLIBC 的 memset 函数中。

A_memset 结果:

Touch:   18235.732 MB/s
Rewrite: 44848.717 MB/s

my_memset_stream:

Touch:   18678.841 MB/s
Rewrite: 44627.270 MB/s

查看 asmlib 的源代码,我看到在编写大块内存时使用了非临时存储。这就是为什么my_memset_stream 的带宽与 Agner Fog 的 asmlib 大致相同。 maximum throughput of this system is 51.2 GB/s。因此,这表明 A_memsetmy_memset_stream 获得了大约 85% 的最大吞吐量。

void my_memset(int *s, int c, size_t n) 
    int i;
    for(i=0; i<n/4; i++) 
        s[i] = c;
    


void my_memset_stream(int *s, int c, size_t n) 
    int i;
    __m128i v = _mm_set1_epi32(c);

    for(i=0; i<n/4; i+=4) 
        _mm_stream_si128((__m128i*)&s[i], v);
    

【问题讨论】:

没有OMP_PROC_BIND的4个线程呢? @HristoIliev,我在答案的末尾添加了八次运行,没有线程绑定但有四个线程。 @HristoIliev,当线程绑定在大约 22 GB/s 的触摸和 18 GB/s 的重写时,它是稳定的。但是当线程未绑定时它是不稳定的(正如您在我的问题的编辑中看到的那样)。 我很困惑。鉴于线程组是在第一个并行区域中创建的,这绝对没有意义。它可能与omp_get_wtime()(最近的libgomp 版本中的CLOCK_MONOTONIC)使用的定时器源有关。尝试通过 LIKWID 或类似的分析工具运行它,看看它报告的内存速度或尝试以不同的方式测量时间。 同意,除了线程创建,内存页面在第一次触摸时初始化。没有理由让相同数据上相同线程上的相同代码执行得更慢。除了一些 Turbo Boost 效果?否则它看起来像一个错误 【参考方案1】:

从您的数字中可以看出,您的 4 个绑定线程在 2 个物理内核上运行,而不是预期的 4 个物理内核。你能证实这一点吗?它可以解释触摸次数翻倍的原因。在您的系统上使用超线程时,我不确定如何将线程强制到物理核心。 我尝试将此作为问题添加,但“声誉”不足

【讨论】:

带有 Intel 处理器的 Linux 的默认拓扑(据我目前所见)是分散的。这意味着在我的情况下,前四个逻辑是物理内核,接下来的四个是超线程。我可以使用GOMP_CPU_AFFINITY 进行设置,因此 GOMP_CPU_AFFINITY="0 1 2 3" 应该是物理内核或“4 6 7 8”。如果我想在两个内核上运行四个线程,我可以执行“0 4 1 5”。如果我这样做,我会得到像“触摸:17219.149 MB/s 重写:17595.210 MB/s”这样的速率......让我开始一个新的评论...... 我已经编写了自己的绑定工具,它从 CPUID 中读取每个线程的 apicid,然后将线程绑定到偶数值。我遇到同样的问题。如果我做`cat /proc/cpuinfo | grep "initial apicid" 它返回 0 2 4 6 1 3 5 7。奇数值是超线程,因此表明前四个逻辑处理器是物理内核。 所以我可以使用OMP_PROC_BIND=true 绑定到物理内核,也可以使用 GOMP_CPU_AFFINITY="0 1 2 3"。但是,在 Windows 上,它使用紧凑的拓扑。所以我必须执行 GOMP_CPU_AFFINITY="0 4 6 8" 才能绑定到 Windows 上的每个物理内核。但由于 MSVC 不支持这一点,我自己通过读取 CPUID 来完成,所以我的代码可以在 Linux 和 Windows 上运行。顺便说一句,我没有看到使用 MSVC 在 Windows 上的重写加倍问题。但是在 Windows 上使用 MSVC 的 memset 实现测量的带宽无论如何都不是很好。 可以肯定的是,我只是禁用了 Bios 的超线程。我仍然遇到同样的问题。 这与物理内核上的线程放置无关,只要两个并行区域相同即可。让初始触摸比连续写入已经映射的页面更快是没有意义的。仅当部分(或全部)内存在两次测量之间的某处被交换或 TLB 未命中非常昂贵(即,将 PTE 加载到 TLB 应该比创建 PTE 更昂贵)时,才会发生这种情况。

以上是关于memset 与绑定到每个物理内核的线程并行的主要内容,如果未能解决你的问题,请参考以下文章

线程休眠时的线程与内核

第09章上 内核线程

Linux上如何查看物理CPU个数,核数,线程数

多处理:仅使用物理内核?

如何将 ActivePivot 实例绑定到物理内核

每个核心的最佳线程数