Я недавно узнал о ложном совместном использовании, которое в моем понимании связано с попыткой ЦП создать согласованность кэша между различными ядрами.Однако разве следующий пример не демонстрирует, что нарушена когерентность кэша?
В приведенном ниже примере запускаются несколько потоков, которые увеличивают глобальную переменную x, и несколько потоков, которые присваивают значение x для y, и наблюдатель, которыйпроверяет, если у> х.Условие y> x никогда не должно выполняться, если между ядрами имеется согласованность памяти, поскольку y увеличивается только после увеличения x.Тем не менее, это условие происходит по результатам выполнения этой программы.Я тестировал его на Visual Studio как на 64, так и на 86, как отладка, так и выпуск с почти одинаковыми результатами.
Итак, согласованность памяти происходит только тогда, когда она плоха, и никогда, когда она хороша?:) Пожалуйста, объясните, как работает согласованность кэша и как он не работает.Если вы можете привести меня к книге, в которой объясняется тема, я буду вам благодарен.
edit: я добавил защиту везде, где это возможно, но при этом отсутствует согласованность памяти (предположительно из-за устаревшего кэша).Кроме того, я знаю, что в программе есть гонка данных, вот и весь смысл.Мой вопрос: почему существует гонка данных, если процессор поддерживает согласованность кэша (если он не поддерживал согласованность кэша, то что такое ложное совместное использование и как это происходит?).Спасибо.
#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