好的,我没有找到任何权威来源,所以我想自己试一试。
#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_PRODUCER
alignas(128) std::atomic<unsigned> unshared;
#endif
alignas(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是内部循环的大小(对缓存很重要)。
- LOOPS是...好吧...
以下是生成的循环:
消费者线程
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
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)
400c97: 00 00 00
400c9a: 0f b6 05 5f 16 20 00 movzbl 0x20165f(%rip),%eax
400ca1: 84 c0 test %al,%al
400ca3: 74 eb je 400c90 <_ZL7producePv>
生产者线程,向共享内存
写入:
400c90: c7 05 66 17 20 00 00 movl $0x0,0x201766(%rip)
400c97: 00 00 00
400c9a: 0f b6 05 5f 16 20 00 movzbl 0x20165f(%rip),%eax
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
3 2 2.10G 1.80G
3 1 4.18G 3.24G
正如预期的那样,由于两个线程都是CPU占用程序并且都得到了公平份额,生产者烧掉的循环会使消费者变慢约一半。这只是CPU争用。
当生产者在第2个CPU上运行时,由于没有交互,消费者可以在另一个CPU上运行而不受到生产者影响。
当生产者在第1个CPU上运行时,我们可以看到超线程的工作效果。
有干扰的生产者的结果:
CONSUMER PRODUCER cycles for 1M cycles for 128k
3 3 4.26G 3.24G
3 2 22.1 G 19.2 G
3 1 36.9 G 37.1 G
lock
前缀,在C中只有一个dep.链,那么延迟可能无法被隐藏。我认为您在这方面非常有知识,并且您肯定可以自己测量延迟/吞吐量,所以我必须漏掉了一些东西才能完全理解这个问题。是什么呢? :) - Margaret Bloom