关于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[];
}

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=

效果真的惨不忍睹啊!因为存储结果的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=

另外运行过程中,有时还是会出现很糟糕的情况,这个要看实际的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=

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

#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=

OpenMp之false sharing的更多相关文章

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

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

  2. 由一道淘宝面试题到False sharing问题

    今天在看淘宝之前的一道面试题目,内容是 在高性能服务器的代码中经常会看到类似这样的代码: typedef union { erts_smp_rwmtx_t rwmtx; byte cache_line ...

  3. 并发性能的隐形杀手之伪共享(false sharing)

    在并发编程过程中,我们大部分的焦点都放在如何控制共享变量的访问控制上(代码层面),但是很少人会关注系统硬件及 JVM 底层相关的影响因素.前段时间学习了一个牛X的高性能异步处理框架 Disruptor ...

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

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

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

    volatilekeyword 当变量被某个线程A改动值之后.其他线程比方B若读取此变量的话,立马能够看到原来线程A改动后的值 注:普通变量与volatile变量的差别是volatile的特殊规则保证 ...

  6. 伪共享(False Sharing)和缓存行(Cache Line)

    转载:https://www.jianshu.com/p/a9b1d32403ea https://www.toutiao.com/a6644375612146319886/ 前言 在上篇介绍Long ...

  7. Linux -- 在多线程程序中避免False Sharing

    1.什么是false sharing 在对称多处理器(SMP)系统中,每个处理器均有属于自己的本地高速缓存区. 如图,CPU0和CPU1有各自的本地高速缓存区(cache).线程0和线程1会用到不同的 ...

  8. 并发刺客(False Sharing)——并发程序的隐藏杀手

    并发刺客(False Sharing)--并发程序的隐藏杀手 前言 前段时间在各种社交平台"雪糕刺客"这个词比较火,简单的来说就是雪糕的价格非常高!其实在并发程序当中也有一个刺客, ...

  9. False 'Sharing Violation' Xcopy error message

    今天想要将QC的新工具自动拷贝到p4 用户机器上使用,为了避免每次通知大家升级啊!!! 于是,我在程序里调用了bat文件,执行拷贝操作,想在默默的情况下替换更新新版本工具,结果我测试发现没能成功更新版 ...

随机推荐

  1. 设计模式之工厂模式(Factory)

    设计模式的工厂模式一共有三种:简单工厂模式,工厂模式,抽象工厂模式 简单工厂模式原理:只有一个工厂类,通过传参的形式确定所创建的产品对象种类 代码如下: #include <stdio.h> ...

  2. 【BZOJ】【2084】【POI2010】Antisymmetry

    Manacher算法 啊……Manacher修改一下就好啦~蛮水的…… Manacher原本是找首尾相同的子串,即回文串,我们这里是要找对应位置不同的“反回文串”(反对称?233) 长度为奇数的肯定不 ...

  3. java如何操作注册表(Preferences类)(在windows的注册表中保存、读取)

    我们经常需要将我们的程序运行中的一些信息(比如在选项对话框中的设置)记录下来,以做便再次运行的时候不用再重写填写这些数据.这对改善软件的人机可用性方面是很有用的.比如:数据库监控.日志工具,JDBMo ...

  4. Slim - 超轻量级PHP Restful API构建框架

    下载源码包: http://www.slimframework.com/ 基于Slim的Restful API Sample: <?php require '/darjuan/Slim/Slim ...

  5. [nowCoder] 二进制中1的个数

    题目描述 输入一个整数,输出该数二进制表示中1的个数.其中负数用补码表示.     class Solution { public: int NumberOf1(int n) { ; while(n) ...

  6. HTML5中表单验证的8种方法(转)

    在深人探讨表单验证之前,让我们先思考一下表单验证的真实含义.就其核心而言,表单验证是一套系统,它为终端用户检测无效的控件数据并标记这些错误.换言之,表单验证就是在表单提交服务器前对其进行一系列的检查并 ...

  7. 封装dll遇到的奇葩错误:error LNK2005: _DllMain@12 已经在 DLLMain.obj 中定义

    在定义一个dll工程的时候,一添加MFC的头文件就会报出这个 错误:error LNK2005: _DllMain@12 已经在 DLLMain.obj 中定义  既蛋疼又蛋疼!! 然后逛论坛,查资料 ...

  8. Asp.net与Flex交互测试记录

    一.利用asp.net为Flex提供数据服务,flex为前端表现. 二.flex通过三种方式四种代码实现来取数据.分别为     HttpService. WebService. RemoteObje ...

  9. 一系列JavaScript的基础工具

    在我们的bootcamp训练营中,学员们介绍了一些工具和库来扩展他们代码的能力.Kalina,目前我们JavaScript学员中的一员,列举了这些工具,想和其它爱好代码的小伙伴一起分享. 点击看大图 ...

  10. 10个免费开源的JS音乐播放器插件

    点这里 音乐播放器在网页设计中有时候会用到,比如一些时尚类.音乐或影视类等项目,但这些 网页播放器 插件比较少见,所以这里为大家整理一个集合,也许会有用到的时候. 下面整理的播放器有些是支持自适应的, ...