C++ 并发编程:并发代码性能分析

本文探讨了影响并发代码性能的关键因素,包括处理器个数、数据竞争、乒乓缓存、伪共享和过多的任务切换。同时,提供了设计高效并发数据结构的策略,如调整数据分布和减少线程间的数据交换。

一、相关基础知识

1. CPU高速缓存

2. MESI协议

二、影响并发代码性能的因素

1. 处理器个数(核心个数)

处理器个数是影响多线程应用的首要因素。 在某些情况下, 你对目标硬件会很熟悉, 并且针对硬件进行设计, 并在目标系统或副本上进行测量。 如果是这样, 那你很幸运; 不过, 要知道这些都是很奢侈的。 你可能在一个类似的平台上进行开发, 不过你所使用的平台与目标平台的差异很大。

为了扩展应用线程的数量, 与硬件所支持的并发线程数量一致, C++标准线程库提供了 std::thread::hardware_concurrency() 。 使用这个函数就能知道在给定硬件上可以扩展的线程数量了。需要谨慎使用 std::thread::hardware_concurrency() , 因为代码不会考虑有其他运行在系统上的线程(除非已经将系统信息进行共享)。 最坏的情况就是, 多线程同时调用 std::thread::hardware_concurrency() 函数来对线程数量进行扩展, 这样将导致庞大的超额认购(oversubscription)。 std::async() 就能避免这个问题, 因为标准库会对所有的调用进行适当的安排。 同样, 谨慎的使用线程池也可以避免这个问题。

2.数据竞争和乒乓缓存

当两个线程并发的在不同处理器上执行,并且对同一数据进行读取,通常不会出现问题;因为数据将会拷贝到每个线程的缓存中,并且可以让两个处理器同时进行处理。不过,当有线程对数据进行修改的时候,这个修改需要更新到其他核芯的缓存中去,就要耗费一定的时间。根据线程的操作性质,以及使用到的内存序,这样的修改可能会让第二个处理器停下来,等待硬件内存更新缓存中的数据。即便是精确的时间取决于硬件的物理结构,不过根据CPU指令,这是一个特别特别慢的操作,相当于执行成百上千个独立指令。
思考下面代码:

std::atomic<unsigned long> counter(0);
void processing_loop()
{
  while(counter.fetch_add(1,std::memory_order_relaxed)<100000000)
  {
    do_something();
  }
}

counter变量是全局的,所以任何线程都能调用processing_loop()去修改同一个变量。因此,当新增加的处理器时,counter变量必须要在缓存内做一份拷贝,再改变自己的值,或其他线程以发布的方式对缓存中的拷贝副本进行更新。即使用 std::memory_order_relaxed ,编译器不会为任何数据做同步操作,fetch_add是一个“读-改-写”操作,因此就要对最新的值进行检索。如果另一个线程在另一个处理器上执行同样的代码,counter的数据需要在两个处理器之间进行传递,那么这两个处理器的缓存中间就存有counter的最新值(当counter的值增加时)。如果do_something()足够短,或有很多处理器来对这段代码进行处理时,处理器将会互相等待;一个处理器准备更新这个值,另一个处理器正在修改这个值,所以该处理器就不得不等待第二个处理器更新完成,并且完成更新传递时,才能执行更新.
如果处理器经常需要互相等待,这种情况被称为 高竞争 ( high contention )。
如果处理器很少需要互相等待,那么这种情况就是 低竞争 ( low contention )。
在上面例子中,counter的数据将在每个缓存中传递若干次。这就叫做 乒乓缓存 ( cache ping-pong )。

兵乓缓存会对应用的性能有着重大的影响。当一个处理器因为等待缓存转移而停止运行时,这个处理器就不能做任何事情,所以对于整个应用来说,这就是一个坏消息。

3.伪共享

处理器缓存通常不会用来处理在单个存储位置,但其会用来处理称为 缓存行 ( cache lines )的内存块。内存块通常大小为32或64字节,实际大小需要由正在使用着的处理器模型来决定。因为硬件缓存进处理缓存行大小的内存块,较小的数据项就在同一内存行的相邻内存位置上。有时,这样的设定还是挺不错:当线程访问的一组数据是在同一数据行中,对于应用的性能来说就要好于向多个缓存行进行传播。不过,当在同一缓存行存储的是无关数据,且需要被不同线程访问,这就是伪共享,就会造成性能问题。
这个问题的解决办法就是对数据进行构造,让同一线程访问的数据项存在临近的内存中(就像是放在同一缓存行中),这样那些能被独立线程访问的数据将分布在相距很远的地方,并且可能是存储在不同的缓存行中。

4.过多的任务切换

这并不总是一件好事。如果您有过多的额外线程,则准备运行的线程将多于可用处理器,并且操作系统将不得不大量启动任务切换,以确保它们都能获得合理的时间。这可能会增加任务切换的开销,并使由于缺乏邻近性而导致的缓存问题更加复杂。或者在通过任务类型对任务进行划分的时候, 线程数量大于处理器数量, 这里对性能影响的主要来源是CPU的能力, 而非I/O。

三、设计并发数据结构的建议

  • 尝试调整数据在线程间的分布,让同一线程中的数据紧密联系在一起(让同一线程的数据更加紧凑)。
  • 尝试减少线程上所需的数据量。
  • 尝试让不同线程访问不同的存储位置,以避免伪共享。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值