请解释缓存一致性
Please explain cache coherence
我最近了解了虚假共享,据我所知,这源于 CPU 试图在不同内核之间创建缓存一致性。
但是,下面的例子不是证明缓存一致性被破坏了吗?
下面的例子启动了几个增加全局变量 x 的线程,几个将 x 的值赋给 y 的线程,以及一个测试 y > x 的观察者。如果内核之间存在内存一致性,则永远不会发生 y>x 的情况,因为 y 仅在 x 增加后才增加。但是,根据 运行 这个程序的结果,这种情况确实会发生。我在 visual studio 64 和 86 上对其进行了测试,调试和发布的结果几乎相同。
那么,内存一致性是否只发生在不好的时候,而不会发生在好的时候? :)
请解释缓存一致性如何工作以及如何不工作。如果你能指导我找到一本解释这个主题的书,我将不胜感激。
编辑:我尽可能地添加了 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
虚假共享主要与性能有关,与连贯性或程序顺序无关。 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++ 和奇怪的属性声明不是。
我最近了解了虚假共享,据我所知,这源于 CPU 试图在不同内核之间创建缓存一致性。 但是,下面的例子不是证明缓存一致性被破坏了吗?
下面的例子启动了几个增加全局变量 x 的线程,几个将 x 的值赋给 y 的线程,以及一个测试 y > x 的观察者。如果内核之间存在内存一致性,则永远不会发生 y>x 的情况,因为 y 仅在 x 增加后才增加。但是,根据 运行 这个程序的结果,这种情况确实会发生。我在 visual studio 64 和 86 上对其进行了测试,调试和发布的结果几乎相同。
那么,内存一致性是否只发生在不好的时候,而不会发生在好的时候? :) 请解释缓存一致性如何工作以及如何不工作。如果你能指导我找到一本解释这个主题的书,我将不胜感激。
编辑:我尽可能地添加了 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
虚假共享主要与性能有关,与连贯性或程序顺序无关。 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++ 和奇怪的属性声明不是。