В информатике ложное разделение кэша — это ухудшающий производительность шаблон использования, который может возникать в системах с распределёнными, когерентными кэшами размером с наименьший блок ресурсов, управляемый механизмом кэширования. Когда участник системы пытается периодически получить доступ к данным, которые не изменяются другой стороной, но эти данные разделяют блок кэша с данными, которые изменяются, протокол кэширования может заставить первого участника перезагрузить весь блок кэша, несмотря на отсутствие логической необходимости. Система кэширования не знает об активности внутри этого блока и заставляет первого участника нести накладные расходы системы кэширования, необходимые для истинного совместного доступа к ресурсу.

Кэши многопроцессорных ЦП Наиболее распространенное использование этого термина — в современных многопроцессорных кэшах ЦП, где память кэшируется строками размером, являющимся небольшой степенью двойки (например, 64 выровненных, смежных байта ). Если два процессора работают с независимыми данными в одной и той же области адреса памяти, хранящимися в одной строке, механизмы когерентности кэша в системе могут принудительно передавать всю строку по шине или соединять её при каждой записи данных, вызывая задержки в памяти в дополнение к нерациональному использованию пропускной способности системы. В некоторых случаях устранение ложного разделения может привести к повышению производительности на порядок. Ложное разделение является неотъемлемым артефактом протоколов автоматической синхронизации кэша и может также существовать в таких средах, как распределённые файловые системы или базы данных, но в настоящее время оно распространено только в кэшах ОЗУ.

Пример #include <atomic> #include <chrono> #include <iostream> #include <latch> #include <thread> #include <vector>

int main() { std::vector<std::jthread> threads; const int hc = std::jthread::hardware_concurrency(); constexpr int testLimit = 256; // для простоты предел количества элементов

for (int nThreads = 1; nThreads <= hc && nThreads <= testLimit; ++nThreads)

// точное измерение требует старта всех нитей одновременно std::latch sync(nThreads);

// свои локальные данные для каждой нити struct { std::atomic_char mightBeShared; } globaldata[testLimit];

// решение проблемы: занимать всю линию кэша // struct alignas(64) { std::atomic_char mightBeShared; } globaldata[testLimit];

// сумма времён исполнения всех нитей std::atomic_int64_t nsSum(0);

for (int t = 0; t != nThreads; ++t)

threads.emplace_back([&](int i)

sync.arrive_and_wait(); // старт синхронизации нитей на уровне ядра

auto start = std::chrono::high_resolution_clock::now();

for (std::size_t r = 10'000'000; r--;) globaldata[i].mightBeShared.fetch_add(1);

nsSum += std::chrono::duration_cast<std::chrono::nanoseconds>( std::chrono::high_resolution_clock::now() - start ).count();

}, t);

threads.clear(); // объединить все нити

std::cout << nThreads << ": " << static_cast<double>(nsSum / (1.0e7 * nThreads)) << std::endl;

}Этот код демонстрирует эффект ложного разделения кэша. Он создает всё большее количество потоков, начиная с одного и заканчивая числом физических потоков в системе. Каждый поток последовательно увеличивает на один байт значение, специфичное для этого потока (т.е. не разделяемое между ними). Чем выше уровень конкуренции между потоками, тем дольше длится каждое увеличение, несмотря на то, что каждый поток увеличивает только свою часть глобальных данных. Это результаты на системе Intel Core i7 12-го поколения с 6 производительными и 8 энергоэффективными ядрами, что дает 20 потоков:

Как видно, одновременный доступ этих потоков требует примерно в 50 раз больше вычислительного времени, чем при использовании методов снижения нагрузки, где оно увеличивается примерно вдвое.

Борьба с проблемой Существуют способы смягчения последствий ложного разделения. Например, ложное разделение в кэшах ЦП можно предотвратить путем перестановки переменных или добавления заполнения (неиспользуемых байтов) между переменными. Однако это может увеличить размер объектов, что приведет к увеличению использования памяти. Преобразование данных во время компиляции также может смягчить ложное разделение. Однако некоторые из этих преобразований не всегда допустимы. Например, в проекте стандарта языка программирования C++23 предписывается, что члены данных (data member, аналог полей записи) должны располагаться таким образом, чтобы последующие члены имели более высокие адреса.

Существуют инструменты для обнаружения ложного разделения. Также существуют системы, которые как обнаруживают, так и исправляют ложное разделение в исполняемых программах. Однако эти системы приводят к некоторым накладным расходам на выполнение.

Ссылки

Внешние ссылки

Простое объяснение ложного распределения ресурсов Блог C++ Today: Ложные сообщения снова в деле! Статья доктора Доббса: Как искоренить ложную информацию Будьте осторожны при попытке устранить ложное разделение ресурсов в Java. Категория:Компьютерная память