- html - 出于某种原因,IE8 对我的 Sass 文件中继承的 html5 CSS 不友好?
- JMeter 在响应断言中使用 span 标签的问题
- html - 在 :hover and :active? 上具有不同效果的 CSS 动画
- html - 相对于居中的 html 内容固定的 CSS 重复背景?
我已经实现了一个简单的线性探测 HashMap ,其中包含一个结构内存布局数组。该结构包含键、值和指示条目是否有效的标志。默认情况下,此结构由编译器填充,因为键和值是 64 位整数,但条目仅占用 8 个 bool 值。因此,我也尝试以未对齐访问为代价打包结构。由于更高的内存密度,我希望从打包/未对齐版本中获得更好的性能(我们不会在传输填充字节时浪费带宽)。
在 Intel Xeon Gold 5220S CPU(单线程、gcc 11.2、-O3 和 -march=native)上对该 HashMap 进行基准测试时,我发现填充版本和未对齐版本之间没有性能差异。但是,在 AMD EPYC 7742 CPU(相同设置)上,我发现未对齐和填充之间存在性能差异。下图描述了 HashMap 负载因子 25% 和 50% 的结果,x 轴上的不同成功查询率 (0,25,50,75,100):如您所见,在 Intel 上,灰色和蓝色(圆形和方形)线几乎重叠,struct packing 的好处很小。然而,在 AMD 上,表示未对齐/打包结构的线始终更高,即我们有更多的吞吐量。
为了对此进行调查,我尝试构建了一个较小的微基准测试。在这个微基准测试中,我们执行类似的基准测试,但没有 HashMap 查找逻辑(即,我们只是在数组中选择随机索引并在那里前进一点)。请在此处找到基准:
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <random>
#include <vector>
void ClobberMemory() { std::atomic_signal_fence(std::memory_order_acq_rel); }
template <typename T>
void doNotOptimize(T const& val) {
asm volatile("" : : "r,m"(val) : "memory");
}
struct PaddedStruct {
uint64_t key;
uint64_t value;
bool is_valid;
PaddedStruct() { reset(); }
void reset() {
key = uint64_t{};
value = uint64_t{};
is_valid = 0;
}
};
struct PackedStruct {
uint64_t key;
uint64_t value;
uint8_t is_valid;
PackedStruct() { reset(); }
void reset() {
key = uint64_t{};
value = uint64_t{};
is_valid = 0;
}
} __attribute__((__packed__));
int main() {
const uint64_t size = 134217728;
uint16_t repetitions = 0;
uint16_t advancement = 0;
std::cin >> repetitions;
std::cout << "Got " << repetitions << std::endl;
std::cin >> advancement;
std::cout << "Got " << advancement << std::endl;
std::cout << "Initializing." << std::endl;
std::vector<PaddedStruct> padded(size);
std::vector<PackedStruct> unaligned(size);
std::vector<uint64_t> queries(size);
// Initialize the structs with random values + prefault
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<uint64_t> dist{0, 0xDEADBEEF};
std::uniform_int_distribution<uint64_t> dist2{0, size - advancement - 1};
for (uint64_t i = 0; i < padded.size(); ++i) {
padded[i].key = dist(gen);
padded[i].value = dist(gen);
padded[i].is_valid = 1;
}
for (uint64_t i = 0; i < unaligned.size(); ++i) {
unaligned[i].key = padded[i].key;
unaligned[i].value = padded[i].value;
unaligned[i].is_valid = 1;
}
for (uint64_t i = 0; i < unaligned.size(); ++i) {
queries[i] = dist2(gen);
}
std::cout << "Running benchmark." << std::endl;
ClobberMemory();
auto start_padded = std::chrono::high_resolution_clock::now();
PaddedStruct* padded_ptr = nullptr;
uint64_t sum = 0;
for (uint16_t j = 0; j < repetitions; j++) {
for (const uint64_t& query : queries) {
for (uint16_t i = 0; i < advancement; i++) {
padded_ptr = &padded[query + i];
if (padded_ptr->is_valid) [[likely]] {
sum += padded_ptr->value;
}
}
doNotOptimize(sum);
}
}
ClobberMemory();
auto end_padded = std::chrono::high_resolution_clock::now();
uint64_t padded_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_padded - start_padded).count());
std::cout << "Padded Runtime (ms): " << padded_runtime << " (sum = " << sum << ")" << std::endl; // print sum to avoid that it gets optimized out
ClobberMemory();
auto start_unaligned = std::chrono::high_resolution_clock::now();
uint64_t sum2 = 0;
PackedStruct* packed_ptr = nullptr;
for (uint16_t j = 0; j < repetitions; j++) {
for (const uint64_t& query : queries) {
for (uint16_t i = 0; i < advancement; i++) {
packed_ptr = &unaligned[query + i];
if (packed_ptr->is_valid) [[likely]] {
sum2 += packed_ptr->value;
}
}
doNotOptimize(sum2);
}
}
ClobberMemory();
auto end_unaligned = std::chrono::high_resolution_clock::now();
uint64_t unaligned_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_unaligned - start_unaligned).count());
std::cout << "Unaligned Runtime (ms): " << unaligned_runtime << " (sum = " << sum2 << ")" << std::endl;
}
在运行基准测试时,我选择重复 = 3 和进步 = 5,即在编译和运行它之后,你必须输入 3(并按换行符)然后输入 5 并按回车/换行符。我更新了源代码以 (a) 避免编译器展开循环,因为重复/推进是硬编码的,并且 (b) 切换到指向该向量的指针,因为它更类似于 HashMap 正在做的事情。
在 Intel CPU 上,我得到:
Padded Runtime (ms): 13204Unaligned Runtime (ms): 12185
在 AMD CPU 上,我得到:
Padded Runtime (ms): 28432Unaligned Runtime (ms): 22926
因此,虽然在此微基准测试中,Intel 仍从未对齐访问中受益,但对于 AMD CPU,绝对和相对改进都更高。我无法解释这一点。一般来说,根据我从相关 SO 线程中了解到的情况,单个成员的未对齐访问与对齐访问一样昂贵,只要它保持在单个缓存行 (1) 内。同样在 (1) 中,给出了对 (2) 的引用,它声称缓存提取宽度 可以不同于缓存行大小。然而,除了 Linus Torvalds 的邮件,我找不到任何其他关于处理器中缓存提取宽度的文档,尤其是我的具体两个 CPU,无法确定这是否与此有关。
有人知道为什么 AMD CPU 从结构包装中获益更多吗?如果是关于减少内存带宽消耗,我应该能够看到对两个 CPU 的影响。如果带宽使用情况相似,我不明白是什么导致了这里的差异。
非常感谢。
(1) 相关 SO 线程:How can I accurately benchmark unaligned access speed on x86_64?
(2) https://www.realworldtech.com/forum/?threadid=168200&curpostid=168779
最佳答案
Intel Xeon Gold 5220S(以及所有其他 Skylake/CascadeLake Xeon 处理器)上的 L1 数据缓存提取宽度在每个负载每个周期最多 64 个自然对齐的字节。
对于不跨越高速缓存行边界的大小和对齐的任意组合,核心每个周期可以执行两次加载。我没有在 SKX/CLX 处理器上测试所有组合,但在 Haswell/Broadwell 上,每当负载越过高速缓存行边界时,吞吐量就会减少到每个周期一个负载,我会假设 SKX/CLX 是相似的。这可以被视为必要的功能而不是“惩罚”——线路拆分负载可能需要使用两个端口来加载一对相邻线路,然后将线路的请求部分组合到目标寄存器的有效负载中。
跨页边界的加载有更大的性能损失,但要衡量它,您必须非常小心地理解和控制两个页面的页表条目的位置:DTLB、STLB、在缓存中或在主内存。我的记忆是,最常见的情况是相当快的——部分原因是“下一页预取器”非常擅长在一系列加载到达第一页结束之前将下一页的 PTE 条目预加载到 TLB 中。页。唯一慢得令人痛苦的情况是跨越页面边界的存储,英特尔编译器非常努力地避免这种情况。
我没有详细查看示例代码,但如果我正在执行此分析,我会小心固定处理器频率,测量指令和周期数,并计算每次更新的平均指令数和周期数. (我通常将核心频率设置为标称 (TSC) 频率只是为了使数字更易于处理。)对于自然对齐的情况,查看汇编代码并估计循环计数应该非常容易是。如果测量结果与该案例的观察结果相似,那么您可以开始查看未对齐访问的开销,以更可靠地了解基线。
硬件性能计数器对于这种情况也很有值(value),尤其是 DTLB_LOAD_MISSES 事件和 L1D.REPLACEMENT 事件。只需几个高延迟 TLB 未命中或 L1D 未命中事件即可使平均值发生偏差。
关于x86-64 - Intel x86 与 AMD x86 CPU 上的未对齐访问性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72475623/
我想知道在谈到 CPU 使用率和 CPU 利用率时,术语是否存在科学差异。我觉得这两个词都被用作同义词。它们都描述了 CPU 时间和 CPU 容量之间的关系。 Wikipedia称之为 CPU 使用率
我研究了一些关于处理器和 Tomasulo 算法的指令重新排序的内容。 为了更深入地了解这个主题,我想知道是否有任何方法可以(获取跟踪)查看为给定程序完成的实际动态重新排序? 我想给出一个输入程序并查
我有一台配备 2 个 Intel Xeon CPU E5-2620 (Sandy Bridge) 和 10Gbps 82599 NIC(2 个端口)的服务器,用于高性能计算。从 PCI 关联性中,我看
您能详细解释一下“用户 CPU 时间”和“系统 CPU 时间”吗?我读了很多,但我不太理解。 最佳答案 区别在于时间花在用户空间还是内核空间。用户 CPU 时间是处理器运行程序代码(或库中的代码)所花
我想知道如何识别 CPU 是否与 ARM v5 指令集兼容。 假设 ARM v7 指令与 ARM v5 兼容是否正确? 最佳答案 您可以阅读 CPUID base register获得PARTNO。然
我目前在具有多个六核 CPU 的服务器上使用 C 多线程。我想将我的一些线程的亲和性设置为单个 CPU 的各个核心。我使用过 pthread_setaffinity_np() 和 sched_seta
1) 独占时间是在方法中花费的时间2) 包含时间是在方法中花费的时间加上在任何被调用函数中花费的时间3)我们称调用方法为“ parent ”,称方法为“子”。引用链接:Click here 这里的问题
关闭。这个问题需要多问focused 。目前不接受答案。 想要改进此问题吗?更新问题,使其仅关注一个问题 editing this post . 已关闭 5 年前。 Improve this ques
好的,所以编译器可以出于性能原因自由地重新排序代码片段。让我们假设一些代码片段,在没有应用优化的情况下直接翻译成机器代码,看起来像这样: machine_instruction_1 machine_i
我在 zabbix 中有以下默认图表,但我不知道如何解释这些值。谁能解释一下? 最佳答案 操作系统是一件非常忙碌的事情,尤其是当你让它做某事时(即使你没有做)。当我们看到一个活跃的企业环境时,总会发生
换句话说,L1、L2、L3 等缓存是否总是反射(reflect) CPU的字节序 ? 或者总是将数据存储在某些 的缓存中更有意义吗?特定字节序 ? 有没有总体设计决策 ? 最佳答案 大多数现代缓存不会
我想知道当前的 cpus 是否避免在其中至少一个为零时将两个数字相乘。谢谢 最佳答案 这取决于 CPU 和(在某些情况下)操作数的类型。 较旧/较简单的 CPU 通常使用如下乘法算法: integer
我有一个 CUDA 应用程序,它在一台计算机(配备 GTX 275)上运行良好,而在另一台配备 GeForce 8400 的计算机上运行速度慢了大约 100 倍。我怀疑有某种回退使代码实际上在 CPU
例如,对于 8 位 CPU,堆栈大小预计为 8 位宽,16 位 CPU 与 16 位堆栈宽度,以及 32 位、64 位 CPU,等等。是否适用于所有架构? 最佳答案 CPU 具有数据总线和地址总线。它
实现 SIMD 是否需要多核 CPU? 在阅读有关 SIMD 的维基百科时,我发现了以下短语“多处理元素”。那么这句话和“多核CPU”有什么区别呢? 最佳答案 不,每个内核通常都可以执行指令集中的大多
我遗漏了一些基本的东西。 CPU 流水线:在基本层面上,为什么指令需要不同数量的时钟周期才能完成,为什么有些指令在多级 CPU 中只需要 1 个周期? 除了明显的“不同的指令需要不同的工作量才能完成”
超线程 CPU 是实现并行还是仅实现并发(上下文切换)? 我的猜测是没有并行性,只有通过上下文切换的并发性。 最佳答案 单个物理 CPU 具有超线程的核心显示为 两个逻辑 CPU 到操作系统。 CPU
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
背景是这样的:下周我们的办公室将有一天因为维护而没有暖气。预计室外温度在 7 至 12 摄氏度之间,因此可能会变冷。可移植电取暖器数量太少,无法满足所有人的需求。 但是,在我大约 6-8 平方米的办公
我开发了一个应用程序,该应用程序在我的开发箱上的三个容器中运行,该开发箱具有带超线程的四核,这意味着系统和 docker 使用 8 个核心。 容器的 CPU 分配由 docker-compose 完成
我是一名优秀的程序员,十分优秀!