OpenMp之false sharing

关于false sharing的文章,网上一大堆了,不过觉得都不太系统,那么下面着重系统说明一下。

先看看外国佬下的定义:

In symmetric multiprocessor (SMP) systems, each processor has a local cache. The memory system must guarantee cache coherence. False sharing occurs when threads on different processors modify variables that reside on the same cache line. This invalidates the cache line and forces an update, which hurts performance。

在多核系统上,每一个处理器都有自己的缓存。计算机体系结构中,必须要保证内存数据的一致性,当某个处理器对属于的它自己的缓存的变量执行更新操作时,糟糕的是那个变量所在的块也被其他的核放在了缓存里面,那么就会发生 false sharing,听起来比较难懂。

1.首先,什么是cache line?

CPU处理指令时,由于“Locality of Reference”原因,需要决定哪些数据需要加载到CPU的缓存中,以及如何预加载。

因为不同的处理器有不同的规范,导致这部分工作具有不确定性。在加载的过程中,涉及到一个非常关键的术语:cache line。

cache line是能被cache处理的内存chunks,chunk的大小即为cache line size,典型的大小为32,64及128 bytes. cache能处理的内存大小除以cache line size即为cache line。

1) L1 Cache(一级缓存)是CPU第一层高速缓存,分为数据缓存和指令缓存。内置的L1高速缓存的容量和结构对CPU的性能影响较大,不过高速缓冲存储器均由静态RAM组成,结构较复杂,在CPU管芯面积不能太大的情况下,L1级高速缓存的容量不可能做得太大。

2) L2  Cache由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存储器,即二级缓存。工作主频比较灵活,可与CPU同 频,也可不同。CPU在读取数据时,先在L1中寻找,再从L2寻找,然后是内存,在后是外存储器。所以L2对系统的影响也不容忽视。

3) L3 Cache 现在的都是内置的。而它的实际作用即是,L3缓存的应用可以进一步降低内存延迟,同时提升大数据量计算时处理器的性能。降低内存延迟和提升大数据量计算能 力对游戏都很有帮助。而在服务器领域增加L3缓存在性能方面仍然有显著的提升。比方具有较大L3缓存的配置利用物理内存会更有效,故它比较慢的磁盘I/O 子系统可以处理更多的数据请求。具有较大L3缓存的处理器提供更有效的文件系统缓存行为及较短消息和处理器队列长度。

增加多级缓存的好处就是提高命中率,三级缓存的机器总体的命中率大约为95%,也就是大约有5%的数据从内存中读取,这样就大大提高了cpu的使用率。

2.cpu上cache的策略

cache entry (cache条目)
包含如下部分
1) cache line : 从主存一次copy的数据大小)
2) tag : 标记cache line对应的主存的地址
3) falg : 标记当前cache line是否invalid, 如果是数据cache, 还有是否dirty

cpu访问主存的规律
1) cpu从来都不直接访问主存, 都是通过cache间接访问主存
2) 每次需要访问主存时, 遍历一遍全部cache line, 查找主存的地址是否在某个cache line中.
3) 如果cache中没有找到, 则分配一个新的cache entry, 把主存的内存copy到cache line中, 再从cache line中读取.

cache中包含的cache entry条目有限, 所以, 必须有合适的cache淘汰策略
一般使用的是LRU策略.
将一些主存区域标记为non-cacheble, 可以提高cache命中率, 降低没用的cache

回写策略
cache中的数据更新后,需要回写到主存, 回写的时机有多种
1) 每次更新都回写. write-through cache
2) 更新后不回写,标记为dirty, 仅当cache entry被evict时才回写
3) 更新后, 把cache entry送如回写队列, 待队列收集到多个entry时批量回写.

cache一致性问题
有两种情况可能导致cache中的数据过期
1) DMA, 有其他设备直接更新主存的数据
2) SMP, 同一个cache line存在多个CPU各自的cache中. 其中一个CPU对其进行了更新.

3. false sharing是怎么产生的呢?

拿上面那个图为例子,core1拥有包含X和Y的数据块,core1会将其标记为“Exclusive”,就是专用的意思,当core2加载了相同的数据
块后(这点一点也不奇怪,因为操作系统的各个核之间的缓存调度是独立的),core1会将相同的块标记为“shared”,那么core2里面的在加载的
时候就会被标记“shared”。如果core1要对X进行修改(如果core2也要对X进行修改,那么会发生冲突,需要原子操作进行隔离,否则会发生错
误),core1就对XY数据块标记为“Modified”,并发送“Invalid”通知其他拥有相同数据块的处理器。如果此时core2要使用XY数
据块,那么被core1得知之后,core1就把它自己Cache里面的XY数据块回写到内存中,并将core1
cache里面的XY数据块重新标记为“shared”,而core2
cache里的XY数据块是“Invalid”,也就会产生一个miss,需要重新加载XY数据块,加载完成后将其标记为“shared”。

4. 怎么防止false sharing呢?

http://software.intel.com/sites/default/files/m/d/4/1/d/8/3-4-MemMgt_-_Avoiding_and_Identifying_False_Sharing_Among_Threads.pdf 中列出了三个方法。

1)
字节对齐。因为缓存遵循”Locality of
Reference“,所以只要避免多个处理器Cache里面的数据块尽量不要”shared“就行了。如果上述的例子core1只有X数据
块,core2只有Y数据块,那么就不会存在false sharing。在windows的程序中使用__declspec(align(64))
加在变量核结构体之前,就能把变量或者结构体扩展成64个字节的数据,如果Cache的一个数据块是64byte的话,就只会加载一个变量,那么就不会发
生false sharing了,不过会造成一定的资源浪费。

__declspec (align(64)) int thread1_global_variable;
__declspec (align(64)) int thread2_global_variable

linux中可以使用__attribute__((aligned(64)))。两者的用法存在差异,具体怎么用下面会给一个linux的例子。

2)结构体填充。类似于上面的方法,不过是自己手动去填充数据块。下面是一个将结构体填充成64byte的例子:

struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v; // Frequent read/write access variable
unsigned long start;
unsigned long end;
// expand to 64 bytes
// (4 unsigned long variables + 12 padding)*4 = 64
int padding[12];
}

3) 将数据线程私有化。也就是把可能会产生false sharing的数据块对每个线程copy一份,并重新命名,作为每个线程私有的东西,并在线程的最后一步同步到主线程中去。下面是一个例子:

struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v; //Frequent read/write access variable
unsigned long start;
unsigned long end;
};
void threadFunc(void *parameter)
{
  ThreadParams *p = (ThreadParams*) parameter;
  // local copy for read/write access variable
  unsigned long local_v = p->v;
  for(local_v = p->start; local_v < p->end; local_v++)
  {
   // Functional computation
  }
  p->v = local_v;  // Update shared data structure only once
}

下面给出一个false sharing的例子,以及按照上面改进的方法:

#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

#define THREAD_NUM 4
int test(int i,int n,int* data);
int main(){

clock_t start,finish;
int n=40000000;

int sum=0;
start=clock();
for(int i=0;i<n;i++)
{
 sum+=2;
 sum-=1;
}

finish=clock();
printf("Serial computation\n");
printf("time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

printf("Parallel computation\n");
start=clock();
int sumarray[THREAD_NUM] ={0};
printf("sumarray bytes=%d\n",(int)(THREAD_NUM*sizeof(int)));
#pragma omp parallel num_threads(THREAD_NUM)
{
 int nth=omp_get_num_threads();
 int me=omp_get_thread_num();
 clock_t t1,t2;

 t1=clock();
 for(int i=me;i<n;i+=nth)
 {
   sumarray[me]+=2;
   sumarray[me]-=1;
 }
 t2=clock();
 printf("time:%lf\n",(double)(t2-t1)/CLOCKS_PER_SEC);
}

finish=clock();
sum=0;
for(int i=0;i<THREAD_NUM;i++)
sum+=sumarray[i];
printf("Total time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

return 0;
}

运行结果:

Serial computation
time:0.201041,sum=40000000
Parallel computation
sumarray bytes=16
time:0.875622
time:0.904432
time:0.933348
time:0.939942
Total time:0.940346,sum=40000000

效果真的惨不忍睹啊!因为存储结果的sumarray的字节只有16字节,所以每一个处理器下的cache line都会存储这个数据块,所以造成了false sharing,并行的效果比串行的要糟糕好多。

使用填充的方式进行改进,首先每一个数组中的int是4个字节,扩充成64字节,就要有60个字节的无用区域,很好办,把数组长度乘以16就行了,下面是改进的代码:

#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

#define THREAD_NUM 4
#define EXPAND 16
int test(int i,int n,int* data);
int main(){

clock_t start,finish;
int n=40000000;

int sum=0;
start=clock();
for(int i=0;i<n;i++)
{
 sum+=2;
 sum-=1;
}

finish=clock();
printf("Serial computation\n");
printf("time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

printf("Parallel computation\n");
start=clock();
int sumarray[THREAD_NUM*EXPAND] ={0};
printf("sumarray bytes=%d\n",(int)(EXPAND*THREAD_NUM*sizeof(int)));
#pragma omp parallel num_threads(THREAD_NUM)
{
 int nth=omp_get_num_threads();
 int me=omp_get_thread_num();
 clock_t t1,t2;

 t1=clock();
 for(int i=me;i<n;i+=nth)
 {
   sumarray[me*EXPAND]+=2;
   sumarray[me*EXPAND]-=1;
 }
 t2=clock();
 printf("time:%lf\n",(double)(t2-t1)/CLOCKS_PER_SEC);
}

finish=clock();
sum=0;
for(int i=0;i<THREAD_NUM*EXPAND;i+=EXPAND)
sum+=sumarray[i];
printf("Total time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

return 0;
}

运行结果:

Serial computation
time:0.203828,sum=40000000
Parallel computation
sumarray bytes=256
time:0.158469
time:0.168066
time:0.173716
time:0.173987
Total time:0.184376,sum=40000000

另外运行过程中,有时还是会出现很糟糕的情况,这个要看实际的Cache更新方法。

对字节进行扩展的方法:

#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

#define THREAD_NUM 4

struct A{
 int i;
}__attribute__((aligned(64)));

int test(int i,int n,int* data);
int main(){

clock_t start,finish;
int n=40000000;

A sum={0};
start=clock();
for(int i=0;i<n;i++)
{
 sum.i+=2;
 sum.i-=1;
}

finish=clock();
printf("Serial computation\n");
printf("time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum.i);

printf("Parallel computation\n");
start=clock();
A sumarray[THREAD_NUM] ={0};
printf("sumarray bytes=%d\n",(int)(THREAD_NUM*sizeof(A)));
#pragma omp parallel num_threads(THREAD_NUM)
{
 int nth=omp_get_num_threads();
 int me=omp_get_thread_num();
 clock_t t1,t2;

 t1=clock();
 for(int i=me;i<n;i+=nth)
 {
   sumarray[me].i+=2;
   sumarray[me].i-=1;
 }
 t2=clock();
 printf("time:%lf\n",(double)(t2-t1)/CLOCKS_PER_SEC);
}

finish=clock();

int sum2=0;
for(int i=0;i<THREAD_NUM;i++)
sum2+=sumarray[i].i;

printf("Total time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum2);

return 0;
}

运行结果:

Serial computation
time:0.216687,sum=40000000
Parallel computation
sumarray bytes=256
time:0.164040
time:0.195033
time:0.200804
time:0.201481
Total time:0.204139,sum=40000000

再者就是私有化线程变量,例子如下:

#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<omp.h>

#define THREAD_NUM 4

int main(){

clock_t start,finish;
int n=40000000;

int sum={0};
start=clock();
for(int i=0;i<n;i++)
{
 sum+=2;
 sum-=1;
}

finish=clock();
printf("Serial computation\n");
printf("time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

printf("Parallel computation\n");
start=clock();
sum=0;
#pragma omp parallel num_threads(THREAD_NUM)
{
 int nth=omp_get_num_threads();
 int me=omp_get_thread_num();
 clock_t t1,t2;
 int mysum=0;
 t1=clock();
 for(int i=me;i<n;i+=nth)
 {
   mysum+=2;
   mysum-=1;
 }
 t2=clock();
 printf("time:%lf\n",(double)(t2-t1)/CLOCKS_PER_SEC);
 #pragma omp atomic
 sum+=mysum;
}

finish=clock();
printf("Total time:%lf,sum=%d\n",(double)(finish-start)/CLOCKS_PER_SEC,sum);

return 0;
}

#pragma omp atomic 是为了防止冲突而调用的omp的命令。

输出结果:

Serial computation
time:0.206015,sum=40000000
Parallel computation
time:0.152098
time:0.160297
time:0.161239
time:0.169079
Total time:0.174484,sum=40000000
时间: 2024-11-16 08:17:47

OpenMp之false sharing的相关文章

从缓存行出发理解volatile变量、伪共享False sharing、disruptor

volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证了新值能马上同步到主内存,以及每次使用前能够马上从内存刷新,即一个线程改动了某个变量的值,其他线程读取的话肯定能看到新的值. 普通变量: 写命中:当处理器将操作数写回到一个内存缓存的区域时.它首先会检查这个缓存的内存地址是否在缓存行中,假设不存在一个有效的缓存行,则处理器将这个操作数写回到缓存,而不

伪共享(false sharing),并发编程无声的性能杀手

在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor,它被誉为“最快的消息框架”,其 LMAX 架构能够在一个线程里每秒处理 6百万 订单!在讲到 Disruptor 为什么这么快时,接触到了一个概念——伪共享( false sharing ),其中提到:缓存行上的写竞争是运行在 SMP 系统中并行线程实现可伸缩性最重要的限制因素.由于从代码中很难看

False &#39;Sharing Violation&#39; Xcopy error message

今天想要将QC的新工具自动拷贝到p4 用户机器上使用,为了避免每次通知大家升级啊!!! 于是,我在程序里调用了bat文件,执行拷贝操作,想在默默的情况下替换更新新版本工具,结果我测试发现没能成功更新版本,于是去看log,发现拷贝exe文件的时候报错:Sharing Violation 网上查了很多资料,说的最多的是.拷贝权限问题~~~ 例如:  B -->A.  通常是A处目标不可写,或者B处文件不可读. 查了文件后发现不存在该问题. 当我手动执行bat文件,拷贝顺利进行~~~ 如此诡异~~~

Java8使用@sun.misc.Contended避免伪共享(False Sharing)

伪共享(False Sharing) Java8中用sun.misc.Contended避免伪共享(false sharing) Java8使用@sun.misc.Contended避免伪共享 原文地址:https://www.cnblogs.com/gotodsp/p/8831182.html

杂谈 什么是伪共享(false sharing)?

问题 (1)什么是 CPU 缓存行? (2)什么是内存屏障? (3)什么是伪共享? (4)如何避免伪共享? CPU缓存架构 CPU 是计算机的心脏,所有运算和程序最终都要由它来执行. 主内存(RAM)是数据存放的地方,CPU 和主内存之间有好几级缓存,因为即使直接访问主内存也是非常慢的. 如果对一块数据做相同的运算多次,那么在执行运算的时候把它加载到离 CPU 很近的地方就有意义了,比如一个循环计数,你不想每次循环都跑到主内存去取这个数据来增长它吧. 越靠近 CPU 的缓存越快也越小. 所以 L

OpenMp并行提升时间为什么不是线性的?

最近在研究OpenMp,写了一段代码,如下: #include<time.h> #include<stdio.h> #include<stdlib.h> #include<omp.h> #define THREAD_NUM 8 int main() { clock_t start,finish; int n=80000000; int sum; start=clock(); for(int i=0;i<n;i++) { sum+=2; sum-=1;

转:最近5年133个Java面试问题列表

最近5年133个Java面试问题列表 Java 面试随着时间的改变而改变.在过去的日子里,当你知道 String 和 StringBuilder 的区别就能让你直接进入第二轮面试,但是现在问题变得越来越高级,面试官问的问题也更深入. 在我初入职场的时候,类似于 Vector 与 Array 的区别.HashMap 与 Hashtable 的区别是最流行的问题,只需要记住它们,就能在面试中获得更好的机会,但这种情形已经不复存在.如今,你将会被问到许多 Java 程序员都没有看过的领域,如 NIO,

volatile语义

volatile在Java内存模型(JMM)中,保证共享变量对所有线程可见,但不保证原子性.volatile语义是同步,通过共享变量的方式,完成线程间的通信. 为什么需要volatile Java内存模型中抽象.简化了计算机物理设备,分成工作内存和主内存,线程有各自的工作内存,却共享主内存.如果要把Java内存模型与物理设备映射起来的话,L1,L2 Cache可以视为工作内存,而L3 Cache视为主内存.线程执行指令时,会优先选择距离 CPU 较近的位置的工作内存中使用,而不会从读写速度较慢的

面试题_1_to_16_多线程、并发及线程的基础问题

多线程.并发及线程的基础问题 1)Java 中能创建 volatile 数组吗?能,Java 中可以创建 volatile 类型数组,不过只是一个指向数组的引用,而不是整个数组.我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了. 2)volatile 能使得一个非原子操作变成原子操作吗?一个典型的例子是在类中有一个 long 类型的成员变量.如果你知道该成员变量会被多个线程访问,如计数器