我正在移植一个复杂的性能导向应用程序,以便在新的双插槽计算机上运行。在移植过程中,我遇到了一些性能异常,经过多次试验后,我发现新机器上的内存带宽似乎比我预期的要慢得多。
该机器采用Supermicro X11DGQ主板配备 2 X英特尔至强金牌 6148处理器和 6 x 32 GB DDR4-2133 RAM(共 192 GB)。系统运行 Ubuntu 16.04.4,内核为 4.13。
我编写了一个简单的内存测试实用程序,它反复运行并计时memcpy
以确定平均持续时间和速率:
#include <algorithm>
#include <chrono>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <unistd.h>
const uint64_t MB_SCALER = 1024 * 1024L;
// g++ -std=c++11 -O3 -march=native -o mem_test mem_test.cc
int main(int argc, char** argv)
{
uint64_t buffer_size = 64 * MB_SCALER;
uint32_t num_loops = 100;
std::cout << "Memory Tester\n" << std::endl;
if (argc < 2)
{
std::cout << "Using default values.\n" << std::endl;
}
// Parse buffer size
if (argc >= 2)
{
buffer_size = std::strtoul(argv[1], nullptr, 10) * MB_SCALER;
}
// Parse num loops
if (argc >= 3)
{
num_loops = std::strtoul(argv[2], nullptr, 10);
}
std::cout << " Num loops: " << num_loops << std::endl;
std::cout << " Buffer size: " << (buffer_size / MB_SCALER) << " MB"
<< std::endl;
// Allocate buffers
char* buffer1 = nullptr;
posix_memalign((void**)&buffer1, getpagesize(), buffer_size);
std::memset(buffer1, 0x5A, buffer_size);
char* buffer2 = nullptr;
posix_memalign((void**)&buffer2, getpagesize(), buffer_size);
std::memset(buffer2, 0xC3, buffer_size);
// Loop and copy memory, measuring duration each time
double average_duration = 0;
for (uint32_t loop_idx = 0; loop_idx < num_loops; ++loop_idx)
{
auto iter_start = std::chrono::system_clock::now();
std::memcpy(buffer2, buffer1, buffer_size);
auto iter_end = std::chrono::system_clock::now();
// Calculate and accumulate duration
auto diff = iter_end - iter_start;
auto duration = std::chrono::duration<double, std::milli>(diff).count();
average_duration += duration;
}
// Calculate and display average duration
average_duration /= num_loops;
std::cout << " Duration: " << std::setprecision(4) << std::fixed
<< average_duration << " ms" << std::endl;
// Calculate and display rate
double rate = (buffer_size / MB_SCALER) / (average_duration / 1000);
std::cout << " Rate: " << std::setprecision(2) << std::fixed
<< rate << " MB/s" << std::endl;
std::free(buffer1);
std::free(buffer2);
}
然后我使用 64 MB 缓冲区大小(明显大于 L3 缓存大小)在 10,000 个循环中编译并运行了该实用程序。
双插座配置:
$ ./mem_test 64 10000
Memory Tester
Num loops: 10000
Buffer size: 64 MB
Duration: 17.9141 ms
Rate: 3572.61 MB/s
单插座配置:
(同一硬件,但物理上移除了一个处理器)
#./mem_test 64 10000
Memory Tester
Num loops: 10000
Buffer size: 64 MB
Duration: 11.2055 ms
Rate: 5711.46 MB/s
使用 numactl 的双套接字:
在同事的要求下,我尝试运行相同的实用程序,用于将numactl
内存访问本地化到仅第一个 numa 节点。
$ numactl -m 0 -N 0 ./mem_test 64 10000
Memory Tester
Num loops: 10000
Buffer size: 64 MB
Duration: 18.3539 ms
Rate: 3486.99 MB/s
结果
5711.43 / 3572.61 = 1.59867
对两种配置进行完全相同的测试表明,单插槽配置速度快约 60%。
我发现这个问题这有点类似,但更加详细。来自其中一条评论:“填充第二个套接字会迫使即使本地 L3 未命中也会监听远程 CPU...”。
我理解 L3 监听的概念,但与单插槽情况相比,开销对我来说似乎仍然非常高。我看到的行为是预期的吗?有人能更清楚地说明发生了什么,以及我可以做些什么吗?