性能优化 - 伪共享

背景

上一篇关于局部性的文章,讲了因为CPU缓存、Cache Line、局部性而导致的性能差异。我们接着分析因为缓存、Cache Line和缓存一致性,在多线程并发编程中所带来的另一个问题:伪共享。

伪共享,False Sharing,没找到中文的标准翻译。参考一下英文版的[wiki](False sharing - Wikipedia):

In computer science, false sharing is a performance-degrading usage pattern that can arise in systems with distributed, coherent caches at the size of the smallest resource block managed by the caching mechanism. When a system participant attempts to periodically access data that is not being altered by another party, but that data shares a cache block with data that is being altered, the caching protocol may force the first participant to reload the whole cache block despite a lack of logical necessity. The caching system is unaware of activity within this block and forces the first participant to bear the caching system overhead required by true shared access of a resource.

By far the most common usage of this term is in modern multiprocessor CPU caches, where memory is cached in lines of some small power of two word size (e.g., 64 aligned, contiguous bytes). If two processors operate on independent data in the same memory address region storable in a single line, the cache coherency mechanisms in the system may force the whole line across the bus or interconnect with every data write, forcing memory stalls in addition to wasting system bandwidth. In some cases, the elimination of false sharing can result in order-of-magnitude performance improvements. False sharing is an inherent artifact of automatically synchronized cache protocols and can also exist in environments such as distributed file systems or databases, but current prevalence is limited to RAM caches.

简单地说,CPU每次会读取一个Cache Line的数据(64字节)进入缓存。如果两个线程分别运行在Core0、Core1,这两个Core读取了同一块64字节的内存进入各自的L1缓存。Core0去修改前4个字节的内容;Core1去修改接下去的4个字节的内容。这两个看似不冲突的操作,因为CPU的[缓存一致性](MESI协议 - 维基百科,自由的百科全书),会带来对性能的冲击。

实验

我们可以通过两个简单的代码来进行对比。两个代码都是一样的目的,创建两个线程,分别对同一个结构体内的不同数据进行操作。线程A操作数据a,线程B操作数据b,看起来河水不犯井水。

第一个C文件 - False_sharing_hit.c:

// False_sharing_hit.c

#include <stdio.h>
#include <pthread.h>

#define Loops 1000000000

struct
{
    int data_thread_a;
    int data_thread_b;
} data;

void* thread_a_routine()
{
    for (int i = 0; i < Loops; i++)
    {
        data.data_thread_a = 1;
    }
}

void* thread_b_routine()
{
    for (int i = 0; i < Loops; i++)
    {
        data.data_thread_b = 2;
    }
}

int main(int argc, char *argv[])
{
    pthread_t tid_a, tid_b;
    pthread_create(&tid_a, NULL, (void*)thread_a_routine, NULL);
    pthread_create(&tid_b, NULL, (void*)thread_b_routine, NULL);

    return 0;
}

第二个C文件 - False_sharing_avoid.c:

// False_sharing_avoid.c

#include <stdio.h>
#include <pthread.h>

#define Loops 1000000000
#define CacheLine 64

struct
{
    int data_thread_a;
    // Add this to avoid false sharing
    char padding[CacheLine];
    int data_thread_b;
} data;


void* thread_a_routine()
{
    for (int i = 0; i < Loops; i++)
    {
        data.data_thread_a = 1;
    }
}

void* thread_b_routine()
{
    for (int i = 0; i < Loops; i++)
    {
        data.data_thread_b = 2;
    }
}

int main(int argc, char *argv[])
{
    pthread_t tid_a, tid_b;
    pthread_create(&tid_a, NULL, (void*)thread_a_routine, NULL);
    pthread_create(&tid_b, NULL, (void*)thread_b_routine, NULL);

    return 0;
}

分别编译这两个代码:

$ gcc False_sharing_hit.c -o False_sharing_hit
$ gcc False_sharing_avoid.c -o False_sharing_avoid

接着分别执行编译出来的程序:

$ time ./False_sharing_hit

real    0m0.006s
user    0m0.000s
sys    0m0.005s
$ time ./False_sharing_avoid

real    0m0.002s
user    0m0.002s
sys     0m0.000s

可以发现,第二个程序只花了第一个程序1/3的时间。

结论

第一个代码触发了伪共享的问题。线程A和线程B分别运行在两个Core上,假设Core0运行线程A,Core1运行线程B。data_thread_a和data_thread_b紧紧挨着,会一起被读入两个Core的缓存。当Core0上的线程A修改了data_thread_a的数值之后,Core0的Cache Line变成了dirty状态。需要写回内存后,Core1上的线程B才能继续修改该Cache Line。反之亦然,当Core1的线程B修改完data_thread_b之后,Cache Line又变成dirty状态,需要写回内存后,Core0上的线程A才能修改data_thread_a。所以,为了保证缓存一致性,不管哪个线程发生了修改操作,都会触发缓存回写内存的操作。
像不像A、B两人住在一起,A阳性了,B作为密接被关起来;等到A、B都放出来后,B又阳性了,A作为密接又被关起来了?子子孙孙无穷尽也……

而第二个代码data_thread_a和data_thread_b之间加入了补齐的元素,使得data_thread_a和data_thread_b不会进入同一个Cache Line。每个Core上的Cache Line修改不需要立即写回内存。从而带来了3倍的性能收益。
承接上面的比喻,就是A、B不住在一起,不是时空伴随者,所以A阳性了,B不用被关……

所以在程序开发中,需要理解代码背后发生了什么,才能写出高效的代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容