SMP架构多线程程序的一种性能衰退现象—False Sharing

简介:

很久没更新博客了,虽然说一直都在做事情也没虚度,但是内心多少还是有些愧疚的。忙碌好久了,这个周末写篇文章放松下。

言归正传,这次我们来聊一聊多核CPU运行多线程程序时,可能会产生的一种性能衰退现象——False Sharing. 貌似很高大上?No No No,我相信看完这篇文章之后你会完全理解False Sharing,并且能够在设计和编写多线程程序的时候意识到并完美解决这个问题。

OK,我们开始吧。

首先,False Sharing的产生需要几个特定条件:CPU具有多个核心,其上运行着的同一个程序的多个线程分别运行在不同的核心上,而且这些线程在修改同一个cache行的数据。说到这里你可能已经明白了,就是多核心修改同一cache行引起的。没错,因为现代CPU的每个核心都有自己的私有cache块,False Sharing产生的原因就是因为某个核心的线程修改了自己私有cache某行的数据,导致另一个核心的私有cache中映射到同样内存位置的cache行也被标记上脏位而被迫逐出,又一次从内存更新的缘故(保证cache一致性)。

如果两个核心运行的线程“此起彼伏”的修改邻近内存的数据,就会相互导致对方的私有cache中映射到该内存位置的cache行被频繁的更新。这样的话,cache的效果根本就没有体现出来。原理见下图:

如果你对cache有疑问的话,可以看看我另一篇博文:《浅析x86架构中cache的组织结构》,为了便于描述,我也会把那篇文章的一些图片贴过来说明的。

接下来我们详细解释并且尝试用实验来证明这个现象。

先贴一张CPU核心和cache的关系图(图片来自Intel Core系列处理器的白皮书):

交代一下实验环境:Fedora 18 i686,内核3.11.10,CPU是Intel(R) Core(TM) i3-2310M CPU @ 2.10GHz. CPU L1d cache行的大小是64字节,如下图:

那么,触发False Sharing现象的条件已经很明确了。最简单的方法是创建两个线程,让它们同时去频繁的访问两个邻近的变量就可以了。那会不会正巧它们分布在不同的cache行呢?一般情况下代码只定义两个全局变量,因为编译器考虑到对齐放置,一般是会在一起的。 代码很简单:


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

int num_0;
int num_1;

void *thread_0(void *args)
{
        int i = 100000000;
        while (i--) {
                num_0++;
        }
}

void *thread_1(void *args)
{
        int i = 100000000;
        while (i--) {
                num_1++;
        }
}

int main(int argc, char *argv[])
{
        pthread_t thread[2];

        pthread_create(&thread[0], NULL, thread_0, NULL);
        pthread_create(&thread[1], NULL, thread_1, NULL);

        pthread_join(thread[0], NULL);
        pthread_join(thread[1], NULL);

        return EXIT_SUCCESS;
}
 

编译执行,用time命令计时,输出如下:

作为对比,两个线程并行执行改为轮流执行,代码如下修改:


 pthread_create(&thread[0], NULL, thread_0, NULL);
pthread_join(thread[0], NULL);

pthread_create(&thread[1], NULL, thread_1, NULL);
pthread_join(thread[1], NULL);
 

同样的编译执行,结果如下:

结果大跌眼镜吧,并行反而更慢了!False Sharing现象也许对小程序影响不大,但是对高性能服务器和并发度很大的程序来说需要倍加小心,没有锁也不见得没有性能陷阱。

解释清楚了,那怎么解决呢?其实很简单,用__declspec (align(n))指定内存对齐方式就好了,我这里的cache行是64字节,那就64字节对齐好了。变量定义代码修改如下:


 int num_0 __attribute__ ((aligned(64)));
int num_1 __attribute__ ((aligned(64)));
 

这样就可以要求编译器把这两个变量按照64字节对齐,就不会在同一个cache行了。然后把线程创建代码改回去,编译执行,结果如图:

这才是并行的效率。

所以在设计多线程的程序时,当目标机器是SMP架构的时候,一定要留神这个问题。其实解决方法很好记,就是邻近的全局变量如果被多个线程频繁访问,一定要记得保持距离。

目录
相关文章
|
17天前
|
存储 缓存 Cloud Native
MPP架构数据仓库使用问题之ADB PG云原生版本的扩缩容性能怎么样
MPP架构数据仓库使用问题之ADB PG云原生版本的扩缩容性能怎么样
MPP架构数据仓库使用问题之ADB PG云原生版本的扩缩容性能怎么样
|
2月前
|
调度 数据库 uml
高级系统架构设计师问题之线程状态变化如何解决
高级系统架构设计师问题之线程状态变化如何解决
|
10天前
|
缓存 安全 Java
如何利用Go语言提升微服务架构的性能
在当今的软件开发中,微服务架构逐渐成为主流选择,它通过将应用程序拆分为多个小服务来提升灵活性和可维护性。然而,如何确保这些微服务高效且稳定地运行是一个关键问题。Go语言,以其高效的并发处理能力和简洁的语法,成为解决这一问题的理想工具。本文将探讨如何通过Go语言优化微服务架构的性能,包括高效的并发编程、内存管理技巧以及如何利用Go生态系统中的工具来提升服务的响应速度和资源利用率。
|
12天前
|
Rust 并行计算 安全
揭秘Rust并发奇技!线程与消息传递背后的秘密,让程序性能飙升的终极奥义!
【8月更文挑战第31天】Rust 以其安全性和高性能著称,其并发模型在现代软件开发中至关重要。通过 `std::thread` 模块,Rust 支持高效的线程管理和数据共享,同时确保内存和线程安全。本文探讨 Rust 的线程与消息传递机制,并通过示例代码展示其应用。例如,使用 `Mutex` 实现线程同步,通过通道(channel)实现线程间安全通信。Rust 的并发模型结合了线程和消息传递的优势,确保了高效且安全的并行执行,适用于高性能和高并发场景。
26 0
|
29天前
|
存储 算法 前端开发
JVM架构与主要组件:了解Java程序的运行环境
JVM的架构设计非常精妙,它确保了Java程序的跨平台性和高效执行。通过了解JVM的各个组件,我们可以更好地理解Java程序的运行机制,这对于编写高效且稳定的Java应用程序至关重要。
34 3
|
11天前
|
开发框架 Android开发 iOS开发
跨平台开发的双重奏:Xamarin在不同规模项目中的实战表现与成功故事解析
【8月更文挑战第31天】在移动应用开发领域,选择合适的开发框架至关重要。Xamarin作为一款基于.NET的跨平台解决方案,凭借其独特的代码共享和快速迭代能力,赢得了广泛青睐。本文通过两个案例对比展示Xamarin的优势:一是初创公司利用Xamarin.Forms快速开发出适用于Android和iOS的应用;二是大型企业借助Xamarin实现高性能的原生应用体验及稳定的后端支持。无论是资源有限的小型企业还是需求复杂的大公司,Xamarin均能提供高效灵活的解决方案,彰显其在跨平台开发领域的强大实力。
19 0
|
15天前
|
消息中间件 缓存 Java
如何优化大型Java后端系统的性能:从代码到架构
当面对大型Java后端系统时,性能优化不仅仅是简单地提高代码效率或硬件资源的投入,而是涉及到多层次的技术策略。本篇文章将从代码层面的优化到系统架构的调整,详细探讨如何通过多种方式来提升Java后端系统的性能。通过对常见问题的深入分析和实际案例的分享,我们将探索有效的性能优化策略,帮助开发者构建更高效、更可靠的后端系统。
|
1月前
|
缓存 前端开发 算法
Fiber 架构如何提高性能和响应性的
【8月更文挑战第6天】Fiber 架构如何提高性能和响应性的
31 1
|
20天前
|
Java 调度