请解释缓存一致性

时间:2018-11-18 08:23:09

标签: multithreading visual-c++ multiprocessing race-condition

我最近了解了虚假共享,据我理解,虚假共享源于CPU在不同内核之间创建缓存一致性的尝试。 但是,以下示例是否不证明违反了缓存一致性?

下面的示例启动几个增加全局变量x的线程,以及几个将x的值赋给y的线程,以及一个观察者是否测试y> x。如果内核之间存在内存一致性,则条件y> x永远不会发生,因为y仅在x增加之后才增加。但是,根据运行该程序的结果,确实会发生这种情况。我在Visual Studio 64和86上进行了测试,调试和发布的结果几乎相同。

那么,内存一致性仅在情况不好时才会发生,而在情况良好时才不会发生吗? :) 请说明缓存一致性如何工作以及如何不工作。如果您可以引导我读一本解释该主题的书,我将不胜感激。

edit:我尽可能地添加了mfence,但仍然没有内存一致性(大概是由于过时的缓存)。 另外,我知道程序有数据争用,这就是重点。我的问题是:如果cpu保持高速缓存一致性(为什么不保持高速缓存一致性,那么什么是虚假共享以及如何发生?),为什么会引起数据争夺。谢谢。

#include <intrin.h>
#include <windows.h>

#include <iostream>
#include <thread>
#include <atomic>
#include <list>
#include <chrono>
#include <ratio>

#define N 1000000

#define SEPARATE_CACHE_LINES 0
#define USE_ATOMIC 0

#pragma pack(1)
struct  
{
    __declspec (align(64)) volatile long x;
#if SEPARATE_CACHE_LINES
    __declspec (align(64))
#endif
        volatile long y;
} data;

volatile long &g_x = data.x;
volatile long &g_y = data.y;

int g_observed;
std::atomic<bool> g_start;

void Observer()
{
    while (!g_start);
    for (int i = 0;i < N;++i)
    {
        _mm_mfence();
        long y = g_y;
        _mm_mfence();
        long x = g_x;
        _mm_mfence();
        if (y > x)
        {
            ++g_observed;
        }
    }
}

void XIncreaser()
{
    while (!g_start);
    for (int i = 0;i < N;++i)
    {
#if USE_ATOMIC
        InterlockedAdd(&g_x,1);
#else
        _mm_mfence();
        int x = g_x+1;
        _mm_mfence();
        g_x = x;
        _mm_mfence();
#endif
    }
}

void YAssigner()
{
    while (!g_start);
    for (int i = 0;i < N;++i)
    {
#if USE_ATOMIC
        long x = g_x;
        InterlockedExchange(&g_y, x);
#else
        _mm_mfence();
        int x = g_x;
        _mm_mfence();
        g_y = x;
        _mm_mfence();
#endif
    }
}

int main()
{
    using namespace std::chrono;

    g_x = 0;
    g_y = 0;
    g_observed = 0;
    g_start = false;

    const int NAssigners = 4;
    const int NIncreasers = 4;

    std::list<std::thread> threads;

    for (int i = 0;i < NAssigners;++i)
    {
        threads.emplace_back(YAssigner);
    }
    for (int i = 0;i < NIncreasers;++i)
    {
        threads.emplace_back(XIncreaser);
    }
    threads.emplace_back(Observer);

    auto tic = high_resolution_clock::now();
    g_start = true;

    for (std::thread& t : threads)
    {
        t.join();
    }

    auto toc = high_resolution_clock::now();

    std::cout << "x = " << g_x << " y = " << g_y << " number of times y > x = " << g_observed << std::endl;
    std::cout << "&x = " << (int*)&g_x << " &y = " << (int*)&g_y << std::endl;
    std::chrono::duration<double> t = toc - tic;
    std::cout << "time elapsed = " << t.count() << std::endl;
    std::cout << "USE_ATOMIC = " << USE_ATOMIC << " SEPARATE_CACHE_LINES = " << SEPARATE_CACHE_LINES << std::endl;

    return 0;
}

示例输出:

x = 1583672 y = 1583672 number of times y > x = 254
&x = 00007FF62BE95800 &y = 00007FF62BE95804
time elapsed = 0.187785
USE_ATOMIC = 0 SEPARATE_CACHE_LINES = 0

1 个答案:

答案 0 :(得分:1)

虚假共享主要与性能有关,与一致性或程序顺序无关。 cpu缓存的粒度通常为16、32、64,...字节。这意味着,如果两个独立的数据项在内存中靠在一起,它们将彼此经历缓存操作。具体来说,如果&a%CACHE_LINE_SIZE ==&b%CACHE_LINE_SIZE,则它们将共享一个缓存行。

例如,如果cpu0和1争夺a,而cpu 2和3争夺b,则包含a&b的缓存行将在4个缓存中的每一个之间跳动。这是错误共享的结果,并且会导致性能大幅下降。

发生错误共享是因为高速缓存中的一致性算法要求存在一致的内存视图。检查它的一个好方法是将两个原子计数器放在以一个或两个k隔开的结构中:

struct a {
      long    a;
      long    pad[1024];
      long    b;
};

并找到一个不错的小机器语言函数来进行原子增量。然后切掉以a为增量的NCPU / 2线程和以b为增量的NCPU / 2线程,直到它们达到一个大数目。 然后重复,注释掉焊盘阵列。比较时间。

当您尝试了解机器的详细信息时,您的朋友会变得更加清晰和精确;不是C ++和奇怪的属性声明。