为了账号安全,请及时绑定邮箱和手机立即绑定

超级兄弟姐妹与非超级兄弟姐妹之间的生产者/消费者共享内存位置的延迟和吞吐量成本是多少?

/ 猿问

超级兄弟姐妹与非超级兄弟姐妹之间的生产者/消费者共享内存位置的延迟和吞吐量成本是多少?

元芳怎么了 2019-09-26 11:19:43

超级兄弟姐妹与非超级兄弟姐妹之间的生产者/消费者共享内存位置的延迟和吞吐量成本是多少?

单个进程中的两个不同线程可以通过读取和/或写入共享一个公共内存位置。

通常,这种(有意的)共享是使用原子操作通过lockx86上的前缀来实现的,lock前缀本身本身具有众所周知的成本(即无竞争成本),并且在实际共享高速缓存行时具有额外的一致性成本(正确或错误共享)。

在这里,我很感兴趣,产生的消费成本,其中单个线程P写入到存储位置,而另一个线程`C从内存读取的位置,无论是使用普通的读取和写入。

在最近的x86内核上,在同一套接字的不同内核上执行此操作,以及在同一物理内核上的同级超线程上执行操作时,这种操作的延迟和吞吐量是多少?

在标题中,我使用的术语“超兄弟”是指在同一核心的两个逻辑线程上运行的两个线程,而内核间的兄弟是指在不同物理核心上运行的两个线程的更常见的情况。


查看完整描述

3 回答

?
慕斯王

好的,我找不到任何权威来源,所以我想自己去尝试一下。

#include <pthread.h>#include <sched.h>#include <atomic>#include <cstdint>#include <iostream>alignas(128) static uint64_t data[SIZE];alignas(128) static std::atomic<unsigned> shared;#ifdef EMPTY_PRODUCERalignas(128) std::atomic<unsigned> unshared;#endifalignas(128) static std::atomic<bool> stop_producer;alignas(128) static std::atomic<uint64_t> elapsed;static inline uint64_t rdtsc(){
    unsigned int l, h;
    __asm__ __volatile__ (
        "rdtsc"
        : "=a" (l), "=d" (h)
    );
    return ((uint64_t)h << 32) | l;}static void * consume(void *){
    uint64_t    value = 0;
    uint64_t    start = rdtsc();
    for (unsigned n = 0; n < LOOPS; ++n) {
        for (unsigned idx = 0; idx < SIZE; ++idx) {
            value += data[idx] + shared.load(std::memory_order_relaxed);
        }
    }
    elapsed = rdtsc() - start;
    return reinterpret_cast<void*>(value);}static void * produce(void *){
    do {#ifdef EMPTY_PRODUCER
        unshared.store(0, std::memory_order_relaxed);#else
        shared.store(0, std::memory_order_relaxed);#enfid
    } while (!stop_producer);
    return nullptr;}int main(){
    pthread_t consumerId, producerId;
    pthread_attr_t consumerAttrs, producerAttrs;
    cpu_set_t cpuset;
    for (unsigned idx = 0; idx < SIZE; ++idx) { data[idx] = 1; }
    shared = 0;
    stop_producer = false;
    pthread_attr_init(&consumerAttrs);
    CPU_ZERO(&cpuset);
    CPU_SET(CONSUMER_CPU, &cpuset);
    pthread_attr_setaffinity_np(&consumerAttrs, sizeof(cpuset), &cpuset);
    pthread_attr_init(&producerAttrs);
    CPU_ZERO(&cpuset);
    CPU_SET(PRODUCER_CPU, &cpuset);
    pthread_attr_setaffinity_np(&producerAttrs, sizeof(cpuset), &cpuset);
    pthread_create(&consumerId, &consumerAttrs, consume, NULL);
    pthread_create(&producerId, &producerAttrs, produce, NULL);
    pthread_attr_destroy(&consumerAttrs);
    pthread_attr_destroy(&producerAttrs);
    pthread_join(consumerId, NULL);
    stop_producer = true;
    pthread_join(producerId, NULL);
    std::cout <<"Elapsed cycles: " <<elapsed <<std::endl;
    return 0;}

使用以下命令进行编译,替换定义:

gcc -std=c++11 -DCONSUMER_CPU=3 -DPRODUCER_CPU=0 -DSIZE=131072 -DLOOPS=8000 timing.cxx -lstdc++ -lpthread -O2 -o timing

哪里:

  • CONSUMER_CPU是要在其上运行使用者线程的cpu的编号。

  • PRODUCER_CPU是要在其上运行生产者线程的cpu的编号。

  • SIZE是内部循环的大小(缓存的注意事项)

  • 循环是...

这是生成的循环:

使用者线程

  400cc8:       ba 80 24 60 00          mov    $0x602480,%edx
  400ccd:       0f 1f 00                nopl   (%rax)
  400cd0:       8b 05 2a 17 20 00       mov    0x20172a(%rip),%eax        # 602400 <shared>
  400cd6:       48 83 c2 08             add    $0x8,%rdx
  400cda:       48 03 42 f8             add    -0x8(%rdx),%rax
  400cde:       48 01 c1                add    %rax,%rcx
  400ce1:       48 81 fa 80 24 70 00    cmp    $0x702480,%rdx
  400ce8:       75 e6                   jne    400cd0 <_ZL7consumePv+0x20>
  400cea:       83 ee 01                sub    $0x1,%esi
  400ced:       75 d9                   jne    400cc8 <_ZL7consumePv+0x18>

生产者线程,带有空循环(不写入shared):

  400c90:       c7 05 e6 16 20 00 00    movl   $0x0,0x2016e6(%rip)        # 602380 <unshared>
  400c97:       00 00 00 
  400c9a:       0f b6 05 5f 16 20 00    movzbl 0x20165f(%rip),%eax        # 602300 <stop_producer>
  400ca1:       84 c0                   test   %al,%al
  400ca3:       74 eb                   je     400c90 <_ZL7producePv>

生产者线程,写入shared

  400c90:       c7 05 66 17 20 00 00    movl   $0x0,0x201766(%rip)        # 602400 <shared>
  400c97:       00 00 00 
  400c9a:       0f b6 05 5f 16 20 00    movzbl 0x20165f(%rip),%eax        # 602300 <stop_producer>
  400ca1:       84 c0                   test   %al,%al
  400ca3:       74 eb                   je     400c90 <_ZL7producePv>

该程序计算使用者核心上消耗的CPU周期数,以完成整个循环。我们将第一个生产者(不执行任何操作,只消耗CPU周期)与第二个生产者(通过重复写入来破坏使用者)进行比较shared

我的系统有一个i5-4210U。即2个核心,每个核心2个线程。它们被内核公开为Core#1 → cpu0, cpu2 Core#2 → cpu1, cpu3

结果完全没有启动生产者:

CONSUMER    PRODUCER     cycles for 1M      cycles for 128k
    3          n/a           2.11G              1.80G

空生产者的结果。适用于1G操作(1000 * 1M或8000 * 128k)。

CONSUMER    PRODUCER     cycles for 1M      cycles for 128k
    3           3            3.20G              3.26G       # mono
    3           2            2.10G              1.80G       # other core
    3           1            4.18G              3.24G       # same core, HT

不出所料,由于这两个线程都是cpu猪,并且都占有相当的份额,因此生产者的燃烧周期使消费者的速度降低了大约一半。这只是cpu争用。

由于生产者在cpu#2上,因为没有交互,所以消费者运行时不会受到在另一个cpu上运行的生产者的影响。

通过使用生产者在cpu#1上,我们可以看到超线程在起作用。

破坏性生产者的结果:

CONSUMER    PRODUCER     cycles for 1M      cycles for 128k
    3           3            4.26G              3.24G       # mono
    3           2           22.1 G             19.2 G       # other core
    3           1           36.9 G             37.1 G       # same core, HT
  • 当我们将两个线程安排在同一内核的同一线程上时,不会产生影响。再次期望,因为生产者写操作保持本地状态,因此不会产生同步开销。

  • 我无法真正解释为什么我的超线程性能要比两个内核差得多。欢迎咨询。


查看完整回答
反对 回复 2019-09-26
?
慕粉4167745

这种分析非常合理(实际上,由内存排序引起的大量“机器清除”事件的存在在很大程度上证实了粗略的笔触。但是存储缓冲区又如何呢?在相同的核心示例中,存储区进入存储缓冲区) ,可能是“一段时间”,这会稍微改变分析的范围。另请参阅上述彼得的评论:“跨SMT-HW线程监听(存储)到达负载缓冲区。”似乎在相同核心情况下的存储需要监听负载。缓冲区(内核中的微型一致性协议),但尚不清楚何时发生这种情况

查看完整回答
反对 回复 2019-09-26

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信