Метою цих констант справді є отримання розміру рядка кешу. Найкраще, щоб прочитати про їх обґрунтування, є в самій пропозиції:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0154r1.html
Для зручності читання я наведу тут фрагмент обґрунтування:
[...] деталізація пам'яті, яка не заважає (до першого порядку) [зазвичай] називається розміром кеш-рядка .
Використання розміру кеш-рядка поділяється на дві великі категорії:
- Уникнення руйнівних втручань (обміну помилками) між об’єктами з тимчасово роз’єднаними шаблонами доступу під час виконання з різних потоків.
- Сприяння конструктивному втручанню (спільне використання правди) між об'єктами, які мають часові локальні шаблони доступу під час виконання.
Найважливішою проблемою цієї корисної кількості реалізації є сумнівна переносимість методів, що використовуються в сучасній практиці для визначення її цінності, незважаючи на їхню поширеність та популярність як групи. [...]
Ми прагнемо внести скромний винахід для цієї справи, абстракції для цієї кількості, які можна консервативно визначити для заданих цілей реалізаціями:
- Розмір руйнівних перешкод : число, яке підходить як зсув між двома об’єктами, щоб, ймовірно, уникнути обміну помилками через різні схеми доступу під час виконання з різних потоків.
- Конструктивний розмір перешкод : число, яке підходить як обмеження для об’єднаного розміру місця пам’яті двох об’єктів та базового вирівнювання, щоб, ймовірно, сприяти спільному використанню правди.
В обох випадках ці значення наводяться на основі якості впровадження, лише як підказки, які можуть покращити ефективність. Це ідеальні портативні значення для використання з alignas()
ключовим словом, для яких на даний час майже не існує стандартних портативних використання.
"Як ці константи пов'язані з розміром рядка кешу L1?"
Теоретично, досить безпосередньо.
Припустимо, компілятор точно знає, на якій архітектурі ви будете працювати - тоді вони майже напевно точно дадуть вам розмір кеш-рядка L1. (Як зазначалося пізніше, це велике припущення.)
Для чого це варто, я майже завжди очікував би, що ці значення будуть однаковими. Я вважаю, що єдиною причиною, за якою вони оголошуються окремо, є повнота. (Тим не менш, можливо, компілятор хоче оцінити розмір рядка кешу L2 замість розміру рядка кешу L1 для конструктивних перешкод; я не знаю, чи справді це було б корисно.)
"Чи є хороший приклад, який демонструє випадки їх використання?"
Внизу цієї відповіді я додав довгу контрольну програму, яка демонструє неправдивий та правдивий обмін.
Він демонструє помилковий спільний доступ, виділяючи масив обгортки int: в одному випадку декілька елементів поміщаються в лінію кешування L1, а в іншому один елемент займає лінію кешу L1. У тісному циклі з масиву вибирається одиничний, нерухомий елемент, який неодноразово оновлюється.
Він демонструє правдивий обмін, виділяючи одну пару вкладок в обгортці: в одному випадку два вставки в парі не відповідають розміру кеш-лінії L1 разом, а в іншій вони підходять. У щільному циклі кожен елемент пари неодноразово оновлюється.
Зверніть увагу, що код доступу до об'єкта, що тестується, не змінюється; різниця лише в компонуванні та вирівнюванні самих об’єктів.
У мене немає компілятора C ++ 17 (припустимо, більшість людей на даний момент цього теж не роблять), тому я замінив ці константи на свої. Вам потрібно оновити ці значення, щоб бути точними на вашому апараті. Тим не менш, 64 байти - це, мабуть, правильне значення для типового сучасного настільного обладнання (на момент написання статті).
Попередження: під час тесту будуть використані всі ядра на ваших машинах та виділено ~ 256 МБ пам'яті. Не забудьте скомпілювати з оптимізаціями!
На моїй машині результат:
Паралельність обладнання: 16
sizeof (naive_int): 4
alignof (naive_int): 4
sizeof (cache_int): 64
alignof (cache_int): 64
sizeof (bad_pair): 72
alignof (bad_pair): 4
sizeof (good_pair): 8
alignof (good_pair): 4
Запуск тесту naive_int.
Середній час: 0,0873625 секунд, марний результат: 3291773
Запуск тесту cache_int.
Середній час: 0,024724 секунди, марний результат: 3286020
Запуск тесту bad_pair.
Середній час: 0,308667 секунд, марний результат: 6396272
Запуск тесту good_pair.
Середній час: 0,174936 секунди, марний результат: 6668457
Я отримую ~ 3,5-кратний пришвидшення, уникаючи обміну помилками, та ~ 1,7-кратний прискорення, забезпечуючи правдивий обмін.
"І те, і інше визначається статичним constexpr. Хіба це не проблема, якщо ви створюєте двійковий файл і виконуєте його на інших машинах з різними розмірами рядків кешу? Як він може захистити від обміну помилками в цьому сценарії, коли ви не впевнені, на якій машині ваш код буде бігати? "
Це справді буде проблемою. Ці константи не гарантують відображення будь-якого розміру рядка кеш-пам'яті на цільовій машині, зокрема, але призначені як найкраще наближення, яке компілятор може зібрати.
Це зазначено в пропозиції, а в додатку вони наводять приклад того, як деякі бібліотеки намагаються виявити розмір кеш-рядка під час компіляції на основі різних екологічних підказок та макросів. Ви будете гарантовані , що це значення , по крайней мере alignof(max_align_t)
, що є очевидним нижньою межею.
Іншими словами, це значення слід використовувати як резервний випадок; Ви можете визначити точне значення, якщо вам це відомо, наприклад:
constexpr std::size_t cache_line_size() {
#ifdef KNOWN_L1_CACHE_LINE_SIZE
return KNOWN_L1_CACHE_LINE_SIZE;
#else
return std::hardware_destructive_interference_size;
#endif
}
Під час компіляції, якщо ви хочете припустити розмір рядка кешу, просто визначте KNOWN_L1_CACHE_LINE_SIZE
.
Сподіваюся, це допомагає!
Тестова програма:
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <functional>
#include <future>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
constexpr std::size_t hardware_destructive_interference_size = 64;
constexpr std::size_t hardware_constructive_interference_size = 64;
constexpr unsigned kTimingTrialsToComputeAverage = 100;
constexpr unsigned kInnerLoopTrials = 1000000;
typedef unsigned useless_result_t;
typedef double elapsed_secs_t;
struct naive_int {
int value;
};
static_assert(alignof(naive_int) < hardware_destructive_interference_size, "");
struct cache_int {
alignas(hardware_destructive_interference_size) int value;
};
static_assert(alignof(cache_int) == hardware_destructive_interference_size, "");
struct bad_pair {
int first;
char padding[hardware_constructive_interference_size];
int second;
};
static_assert(sizeof(bad_pair) > hardware_constructive_interference_size, "");
struct good_pair {
int first;
int second;
};
static_assert(sizeof(good_pair) <= hardware_constructive_interference_size, "");
template <typename T, typename Latch>
useless_result_t sample_array_threadfunc(
Latch& latch,
unsigned thread_index,
T& vec) {
std::random_device rd;
std::mt19937 mt{ rd() };
std::uniform_int_distribution<int> dist{ 0, 4096 };
auto& element = vec[vec.size() / 2 + thread_index];
latch.count_down_and_wait();
for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) {
element.value = dist(mt);
}
return static_cast<useless_result_t>(element.value);
}
template <typename T, typename Latch>
useless_result_t sample_pair_threadfunc(
Latch& latch,
unsigned thread_index,
T& pair) {
std::random_device rd;
std::mt19937 mt{ rd() };
std::uniform_int_distribution<int> dist{ 0, 4096 };
latch.count_down_and_wait();
for (unsigned trial = 0; trial != kInnerLoopTrials; ++trial) {
pair.first = dist(mt);
pair.second = dist(mt);
}
return static_cast<useless_result_t>(pair.first) +
static_cast<useless_result_t>(pair.second);
}
class threadlatch {
public:
explicit threadlatch(const std::size_t count) :
count_{ count }
{}
void count_down_and_wait() {
std::unique_lock<std::mutex> lock{ mutex_ };
if (--count_ == 0) {
cv_.notify_all();
}
else {
cv_.wait(lock, [&] { return count_ == 0; });
}
}
private:
std::mutex mutex_;
std::condition_variable cv_;
std::size_t count_;
};
std::tuple<useless_result_t, elapsed_secs_t> run_threads(
const std::function<useless_result_t(threadlatch&, unsigned)>& func,
const unsigned num_threads) {
threadlatch latch{ num_threads + 1 };
std::vector<std::future<useless_result_t>> futures;
std::vector<std::thread> threads;
for (unsigned thread_index = 0; thread_index != num_threads; ++thread_index) {
std::packaged_task<useless_result_t()> task{
std::bind(func, std::ref(latch), thread_index)
};
futures.push_back(task.get_future());
threads.push_back(std::thread(std::move(task)));
}
const auto starttime = std::chrono::high_resolution_clock::now();
latch.count_down_and_wait();
for (auto& thread : threads) {
thread.join();
}
const auto endtime = std::chrono::high_resolution_clock::now();
const auto elapsed = std::chrono::duration_cast<
std::chrono::duration<double>>(
endtime - starttime
).count();
useless_result_t result = 0;
for (auto& future : futures) {
result += future.get();
}
return std::make_tuple(result, elapsed);
}
void run_tests(
const std::function<useless_result_t(threadlatch&, unsigned)>& func,
const unsigned num_threads) {
useless_result_t final_result = 0;
double avgtime = 0.0;
for (unsigned trial = 0; trial != kTimingTrialsToComputeAverage; ++trial) {
const auto result_and_elapsed = run_threads(func, num_threads);
const auto result = std::get<useless_result_t>(result_and_elapsed);
const auto elapsed = std::get<elapsed_secs_t>(result_and_elapsed);
final_result += result;
avgtime = (avgtime * trial + elapsed) / (trial + 1);
}
std::cout
<< "Average time: " << avgtime
<< " seconds, useless result: " << final_result
<< std::endl;
}
int main() {
const auto cores = std::thread::hardware_concurrency();
std::cout << "Hardware concurrency: " << cores << std::endl;
std::cout << "sizeof(naive_int): " << sizeof(naive_int) << std::endl;
std::cout << "alignof(naive_int): " << alignof(naive_int) << std::endl;
std::cout << "sizeof(cache_int): " << sizeof(cache_int) << std::endl;
std::cout << "alignof(cache_int): " << alignof(cache_int) << std::endl;
std::cout << "sizeof(bad_pair): " << sizeof(bad_pair) << std::endl;
std::cout << "alignof(bad_pair): " << alignof(bad_pair) << std::endl;
std::cout << "sizeof(good_pair): " << sizeof(good_pair) << std::endl;
std::cout << "alignof(good_pair): " << alignof(good_pair) << std::endl;
{
std::cout << "Running naive_int test." << std::endl;
std::vector<naive_int> vec;
vec.resize((1u << 28) / sizeof(naive_int));
run_tests([&](threadlatch& latch, unsigned thread_index) {
return sample_array_threadfunc(latch, thread_index, vec);
}, cores);
}
{
std::cout << "Running cache_int test." << std::endl;
std::vector<cache_int> vec;
vec.resize((1u << 28) / sizeof(cache_int));
run_tests([&](threadlatch& latch, unsigned thread_index) {
return sample_array_threadfunc(latch, thread_index, vec);
}, cores);
}
{
std::cout << "Running bad_pair test." << std::endl;
bad_pair p;
run_tests([&](threadlatch& latch, unsigned thread_index) {
return sample_pair_threadfunc(latch, thread_index, p);
}, cores);
}
{
std::cout << "Running good_pair test." << std::endl;
good_pair p;
run_tests([&](threadlatch& latch, unsigned thread_index) {
return sample_pair_threadfunc(latch, thread_index, p);
}, cores);
}
}