找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2914

积分

0

好友

420

主题
发表于 5 小时前 | 查看: 1| 回复: 0

相比于《不可忽视的cacheline问题》中提到的导致程序错误的 Bug,本文要讨论的是一个更常见但隐蔽性更强的性能陷阱——伪共享。这类问题虽然不会让程序出错,但会显著拖慢执行速度,在多线程程序中尤其值得警惕。

什么是伪共享?

简单来说,在 MESI 和 MOESI 这类缓存一致性协议下,如果多个 CPU 核心反复访问位于同一 Cacheline 中的不同数据,协议就会在总线上频繁地对这条 Cacheline 进行无效化操作,并要求各个核心的缓存进行监听同步。在 CPU 的视角来看,这种操作是低效的,它无谓地消耗了内存带宽,浪费了宝贵的系统资源。

测试代码与分析

以下是一段用于演示伪共享问题的 C++ 测试代码,它清晰地展示了问题是如何产生的。

#include <iostream>
#include <thread>
#include <new>
#include <atomic>
#include <chrono>
#include <latch>
#include <vector>

using namespace std;
using namespace chrono;

#if defined(__cpp_lib_hardware_interference_size)
// default cacheline size from runtime
constexpr size_t CL_SIZE = hardware_constructive_interference_size;
#else
// most common cacheline size otherwise
constexpr size_t CL_SIZE = 64;
#endif

int main()
{
    vector<jthread> threads;
    int hc = jthread::hardware_concurrency();
        hc = hc <= CL_SIZE ? hc : CL_SIZE;
    for (int nThreads = 1; nThreads <= hc; ++nThreads)
    {
        // synchronize beginning of threads coarse on kernel level
        latch coarseSync(nThreads);
        // fine synch via atomic in userspace
        atomic_uint fineSync(nThreads);
        // as much chars as would fit into a cacheline
        struct alignas(CL_SIZE) { char shareds[CL_SIZE]; } cacheLine;
        // sum of all threads execution times
        atomic_int64_t nsSum(0);
        for (int t = 0; t != nThreads; ++t)
            threads.emplace_back(
                [&](char volatile &c)
                {
                    coarseSync.arrive_and_wait(); // synch beginning of thread execution on kernel-level
                    if (fineSync.fetch_sub(1, memory_order::relaxed) != 1) // fine-synch on user-level
                        while (fineSync.load(memory_order::relaxed));
                    auto start = high_resolution_clock::now();
                    for (size_t r = 10‘000’000; r--;)
                        c = c + 1;
                    nsSum += duration_cast<nanoseconds>(high_resolution_clock::now() - start).count();
                }, ref(cacheLine.shareds[t]));
        threads.resize(0); // join all threads
        cout << nThreads << ": " << (int)(nsSum / (1.0e7 * nThreads) + 0.5) << endl;
    }
}

代码的核心思路很清晰:

  1. 构造一个大小正好为一个 Cacheline 的数组 char shareds[CL_SIZE];,并确保其内存对齐。
  2. 在每次循环中,创建数量递增的线程。每个线程会操作该数组中不同的 char 元素(即ref(cacheLine.shareds[t])),进行千万次自增操作。
  3. 由于这些不同的 char 元素在物理内存上位于同一个 Cacheline 内,当多个线程并发修改时,就会触发上文所述的伪共享问题。
for (int nThreads = 1; nThreads <= hc; ++nThreads)

通过不断增加并发线程数,我们就能直观地看到伪共享带来的负面性能影响。

测试结果:伪共享的代价

编译并运行上述代码:

g++ -std=c++20 falsesharing.cpp -o falsesharing
# ./falsesharing
1: 3
2: 7
3: 12
4: 13
5: 14
6: 19
7: 21
8: 29
9: 30
10: 34
11: 37
12: 39
13: 41
14: 47
15: 49
16: 54

结果非常触目惊心!随着线程数从 1 增加到 16,完成相同计算量的耗时增长了 18 倍(从 3 个单位时间增加到 54 个单位时间)。这正是伪共享导致多线程性能不增反降的典型表现。

如何解决伪共享?

官方或常见的解决方案主要有两种:

  1. 数据布局调整:重新排序数据结构中的变量,或者在变量之间添加无用的填充字节,使得可能被不同线程频繁访问的变量落在不同的 Cacheline 中。
  2. 编译时优化:利用编译器提供的特性或指令进行数据转换,以减少伪共享。

对于这个示例,最直接有效的方法就是确保每个线程操作的数据位于独立的 Cacheline 中。修改思路很简单:不再让所有线程共享同一个 cacheLine 数组,而是为每个线程分配独立的、对齐到 Cacheline 大小的数据。

修复后的程序运行结果如下:

# ./fixfalsesharing
1: 3
2: 3
3: 3
4: 3
5: 3
6: 2
7: 4
8: 3
9: 3
10: 3
11: 3
12: 3
13: 3
14: 3
15: 3
16: 3

可以看到,消除了伪共享后,无论线程数如何增加,每个线程的执行时间基本保持稳定,这才是多线程并发应有的理想性能表现。这提醒我们,在开发程序时,尤其在高并发场景下,对数据布局保持敏感至关重要。

总结

在编写多线程或并发程序时,相比于那些会导致程序崩溃的 Cacheline 对齐 Bug,伪共享是一个更容易被忽视、但实际影响可能更大的性能杀手。它不会让程序出错,却会悄悄吞噬掉你的 CPU 性能。幸运的是,只要在编程时具备“避免不同线程竞争同一 Cacheline”的意识,通常可以通过简单的数据隔离或填充来有效规避。

希望这个在C++环境下的具体例子,能帮助你更深刻地理解伪共享的原理与危害。如果你对这类底层性能优化话题感兴趣,欢迎来云栈社区交流讨论,这里聚集了许多热衷于技术深度探索的开发者。

参考

https://en.wikipedia.org/wiki/False_sharing




上一篇:X推荐算法开源解析:基于Grok架构的7步推荐流程揭秘
下一篇:AI时代产品经理生存指南:正在被淘汰的4类思维模式
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-26 18:42 , Processed in 0.636570 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表