这些常量的目的确实是为了获取高速缓存行的大小。提案本身的最佳阅读之处是:
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字节可能是正确的值。
警告:该测试将使用您计算机上的所有内核,并分配约256MB内存。不要忘记进行优化编译!
在我的机器上,输出为:
硬件并发:16
sizeof(naive_int):4
alignof(naive_int):4
sizeof(cache_int):64
alignof(cache_int):64
sizeof(坏对):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秒,无用的结果:6392272
正在运行good_pair测试。
平均时间:0.174936秒,无用结果:6666685
通过避免错误共享,我获得了〜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);
}
}