【问题标题】:False sharing and pthreads虚假共享和 pthreads
【发布时间】:2012-01-10 00:03:28
【问题描述】:

我有以下任务来演示虚假共享并编写了一个简单的程序:

#include <sys/times.h>
#include <time.h>
#include <stdio.h> 
#include <pthread.h> 

long long int tmsBegin1,tmsEnd1,tmsBegin2,tmsEnd2,tmsBegin3,tmsEnd3;

int array[100];

void *heavy_loop(void *param) { 
  int   index = *((int*)param);
  int   i;
  for (i = 0; i < 100000000; i++)
    array[index]+=3;
} 

int main(int argc, char *argv[]) { 
  int       first_elem  = 0;
  int       bad_elem    = 1;
  int       good_elem   = 32;
  long long time1;
  long long time2;
  long long time3;
  pthread_t     thread_1;
  pthread_t     thread_2;

  tmsBegin3 = clock();
  heavy_loop((void*)&first_elem);
  heavy_loop((void*)&bad_elem);
  tmsEnd3 = clock();

  tmsBegin1 = clock();
  pthread_create(&thread_1, NULL, heavy_loop, (void*)&first_elem);
  pthread_create(&thread_2, NULL, heavy_loop, (void*)&bad_elem);
  pthread_join(thread_1, NULL);
  pthread_join(thread_2, NULL);
  tmsEnd1 = clock(); 

  tmsBegin2 = clock();
  pthread_create(&thread_1, NULL, heavy_loop, (void*)&first_elem);
  pthread_create(&thread_2, NULL, heavy_loop, (void*)&good_elem);
  pthread_join(thread_1, NULL);
  pthread_join(thread_2, NULL);
  tmsEnd2 = clock();

  printf("%d %d %d\n", array[first_elem],array[bad_elem],array[good_elem]);
  time1 = (tmsEnd1-tmsBegin1)*1000/CLOCKS_PER_SEC;
  time2 = (tmsEnd2-tmsBegin2)*1000/CLOCKS_PER_SEC;
  time3 = (tmsEnd3-tmsBegin3)*1000/CLOCKS_PER_SEC;
  printf("%lld ms\n", time1);
  printf("%lld ms\n", time2);
  printf("%lld ms\n", time3);

  return 0; 
} 

当我看到结果时我感到非常惊讶(我在 i5-430M 处理器上运行它)。

  • 虚假共享时,为 1020 毫秒。
  • 如果没有虚假共享,则为 710 毫秒,仅快 30% 而不是 300%(在某些网站上写过它会快于 300-400%)。
  • 不使用 pthread 时,为 580 毫秒。

请告诉我我的错误或解释它发生的原因。

【问题讨论】:

    标签: c pthreads false-sharing


    【解决方案1】:

    错误共享是多个具有不同缓存的内核访问同一物理内存区域的结果(尽管不是相同的地址——那将是真正的共享)。

    要了解虚假共享,您需要了解缓存。在大多数处理器中,每个内核都有自己的 L1 缓存,用于保存最近访问的数据。缓存按“行”组织,这些“行”是对齐的数据块,长度通常为 32 或 64 个字节(取决于您的处理器)。当您从不在缓存中的地址读取时,整行将从主内存(或 L2 缓存)读取到 L1。当您写入缓存中的地址时,包含该地址的行被标记为“脏”。

    这就是共享方面的用武之地。如果多个内核从同一行读取,它们每个都可以在 L1 中拥有该行的副本。但是,如果一个副本被标记为脏,它会使其他缓存中的行无效。如果这没有发生,那么在一个内核上进行的写入可能直到很久以后才会对其他内核可见。所以下次另一个核心去读取那行时,缓存会丢失,它必须再次获取该行。

    False 当内核在同一行上读取和写入不同地址时,就会发生共享。尽管它们不共享数据,但缓存的行为就像它们一样,因为它们非常接近。

    这种效果在很大程度上取决于处理器的架构。如果你有一个单核处理器,你根本看不到效果,因为没有共享。如果您的缓存行更长,您会在“坏”和“好”情况下看到效果,因为它们仍然靠得很近。如果您的内核不共享 L2 缓存(我猜他们确实如此),您可能会看到 300-400% 的差异,因为它们必须在缓存未命中时一直运行到主内存。

    您可能还想知道,每个线程都可以读写(+= 而不是 =),这一点很重要。一些处理器有write-through 缓存,这意味着如果内核写入不在缓存中的地址,它不会丢失并从内存中获取行。将此与 回写 缓存进行对比,后者在写入时会丢失。

    【讨论】:

    • 我认为 array[0] 和 array[1] 应该在一个缓存行中。他们非常接近,不是吗?
    • @AlexeyMatveev:第 31 和第 32 也非常接近。但是您假设它们属于不同的缓存行。事实是,它们可能在也可能不在同一缓存行上。如果 1 到 5(以及适合 1 之前的所有内容)进入一个缓存行,而 6 到 37 进入另一个缓存行怎么办?
    • @AlexeyMatveev:好的,这就是我要改进你的测试的方法。去掉clock ()(这是一个粗略的近似),用基于硬件的高精度定时器代替。如果你碰巧在 Linux 上运行,你可以使用 clock_gettime 和 CLOCK_MONOTONIC_RAW 标志(例如,参见 bitbucket.org/Yocto/yocto/src/9cec50caf923/include/yocto/… 和 bitbucket.org/Yocto/yocto/src/9cec50caf923/src/stopwatch.cpp)......
    • 禁用 CPU 节流并提出一些在单线程模式下将产生至少 1.5 秒的操作(否则节能将在最初的几秒内搞砸你的基准测试)。然后确保编译器不会在不需要的地方应用优化。例如,我会说“i”必须是 volatile 以避免展开,并且数组也是如此,否则编译器可能会决定完全丢弃您的循环。最后,您必须测量“heavy_loop”中的时间以排除线程管理开销。
    • @AlexeyMatveev:没有测试我不能肯定地说。事实上,启动线程会占用大量 CPU 周期,而且 join () 是一个阻塞调用,阻塞 + 唤醒代价高昂。事实上,您当前的测试可能只是测量,没有错误共享。但可能还有其他意外,例如当内核决定将您从一个 CPU 转移到另一个 CPU 时,因为新线程(请参阅进程/线程关联),这不会发生在单个线程中。
    【解决方案2】:

    C语言中clock()函数的简要说明: 它为您提供从开始到结束经过的 CPU 时钟数。所以当你运行两个并行线程时,CPU周期数将是CPU1的时钟周期+CPU2的时钟周期。

    我认为你想要的是一个真正的计时器。用于此用途

    clock_gettime()

    你应该得到预期的输出。

    我使用 clock_gettime() 运行了您的代码,结果如下:

    • 虚假共享 874.587381 毫秒
    • 没有虚假共享 331.844278 毫秒
    • 顺序计算 604.160276 毫秒

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-07
      • 2014-03-14
      • 1970-01-01
      • 2015-05-25
      • 2023-03-23
      • 2012-06-17
      • 1970-01-01
      • 2015-07-01
      相关资源
      最近更新 更多