我们最近购买了一些新的服务器,并且正在经历糟糕的memcpy性能。与我们的笔记本电脑相比,服务器上的memcpy性能要慢3倍。
服务器规范
编辑:我也在另一台规格略高的服务器上进行测试,并看到与上述服务器相同的结果
服务器2规格
笔记本电脑规格
操作系统
$ cat /etc/redhat-release
Scientific Linux release 6.5 (Carbon)
$ uname -a
Linux r113 2.6.32-431.1.2.el6.x86_64 #1 SMP Thu Dec 12 13:59:19 CST 2013 x86_64 x86_64 x86_64 GNU/Linux
编译器(在所有系统上)
$ gcc --version
gcc (GCC) 4.6.1
根据@stefan的建议,使用gcc 4.8.2进行测试。编译器之间没有性能差异。
测试代码 下面的测试代码是一个罐装测试,用于复制我在生产代码中看到的问题。我知道这个基准是简单的,但它能够利用和识别我们的问题。代码在它们之间创建两个1GB缓冲区和memcpys,为memcpy调用计时。您可以使用以下命令在命令行上指定备用缓冲区大小:./ big_memcpy_test [SIZE_BYTES]
#include <chrono>
#include <cstring>
#include <iostream>
#include <cstdint>
class Timer
{
public:
Timer()
: mStart(),
mStop()
{
update();
}
void update()
{
mStart = std::chrono::high_resolution_clock::now();
mStop = mStart;
}
double elapsedMs()
{
mStop = std::chrono::high_resolution_clock::now();
std::chrono::milliseconds elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(mStop - mStart);
return elapsed_ms.count();
}
private:
std::chrono::high_resolution_clock::time_point mStart;
std::chrono::high_resolution_clock::time_point mStop;
};
std::string formatBytes(std::uint64_t bytes)
{
static const int num_suffix = 5;
static const char* suffix[num_suffix] = { "B", "KB", "MB", "GB", "TB" };
double dbl_s_byte = bytes;
int i = 0;
for (; (int)(bytes / 1024.) > 0 && i < num_suffix;
++i, bytes /= 1024.)
{
dbl_s_byte = bytes / 1024.0;
}
const int buf_len = 64;
char buf[buf_len];
// use snprintf so there is no buffer overrun
int res = snprintf(buf, buf_len,"%0.2f%s", dbl_s_byte, suffix[i]);
// snprintf returns number of characters that would have been written if n had
// been sufficiently large, not counting the terminating null character.
// if an encoding error occurs, a negative number is returned.
if (res >= 0)
{
return std::string(buf);
}
return std::string();
}
void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
memmove(pDest, pSource, sizeBytes);
}
int main(int argc, char* argv[])
{
std::uint64_t SIZE_BYTES = 1073741824; // 1GB
if (argc > 1)
{
SIZE_BYTES = std::stoull(argv[1]);
std::cout << "Using buffer size from command line: " << formatBytes(SIZE_BYTES)
<< std::endl;
}
else
{
std::cout << "To specify a custom buffer size: big_memcpy_test [SIZE_BYTES] \n"
<< "Using built in buffer size: " << formatBytes(SIZE_BYTES)
<< std::endl;
}
// big array to use for testing
char* p_big_array = NULL;
/////////////
// malloc
{
Timer timer;
p_big_array = (char*)malloc(SIZE_BYTES * sizeof(char));
if (p_big_array == NULL)
{
std::cerr << "ERROR: malloc of " << SIZE_BYTES << " returned NULL!"
<< std::endl;
return 1;
}
std::cout << "malloc for " << formatBytes(SIZE_BYTES) << " took "
<< timer.elapsedMs() << "ms"
<< std::endl;
}
/////////////
// memset
{
Timer timer;
// set all data in p_big_array to 0
memset(p_big_array, 0xF, SIZE_BYTES * sizeof(char));
double elapsed_ms = timer.elapsedMs();
std::cout << "memset for " << formatBytes(SIZE_BYTES) << " took "
<< elapsed_ms << "ms "
<< "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
<< std::endl;
}
/////////////
// memcpy
{
char* p_dest_array = (char*)malloc(SIZE_BYTES);
if (p_dest_array == NULL)
{
std::cerr << "ERROR: malloc of " << SIZE_BYTES << " for memcpy test"
<< " returned NULL!"
<< std::endl;
return 1;
}
memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));
// time only the memcpy FROM p_big_array TO p_dest_array
Timer timer;
memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));
double elapsed_ms = timer.elapsedMs();
std::cout << "memcpy for " << formatBytes(SIZE_BYTES) << " took "
<< elapsed_ms << "ms "
<< "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
<< std::endl;
// cleanup p_dest_array
free(p_dest_array);
p_dest_array = NULL;
}
/////////////
// memmove
{
char* p_dest_array = (char*)malloc(SIZE_BYTES);
if (p_dest_array == NULL)
{
std::cerr << "ERROR: malloc of " << SIZE_BYTES << " for memmove test"
<< " returned NULL!"
<< std::endl;
return 1;
}
memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));
// time only the memmove FROM p_big_array TO p_dest_array
Timer timer;
// memmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));
doMemmove(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));
double elapsed_ms = timer.elapsedMs();
std::cout << "memmove for " << formatBytes(SIZE_BYTES) << " took "
<< elapsed_ms << "ms "
<< "(" << formatBytes(SIZE_BYTES / (elapsed_ms / 1.0e3)) << " bytes/sec)"
<< std::endl;
// cleanup p_dest_array
free(p_dest_array);
p_dest_array = NULL;
}
// cleanup
free(p_big_array);
p_big_array = NULL;
return 0;
}
建立CMake文件
project(big_memcpy_test)
cmake_minimum_required(VERSION 2.4.0)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# create verbose makefiles that show each command line as it is issued
set( CMAKE_VERBOSE_MAKEFILE ON CACHE BOOL "Verbose" FORCE )
# release mode
set( CMAKE_BUILD_TYPE Release )
# grab in CXXFLAGS environment variable and append C++11 and -Wall options
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -Wall -march=native -mtune=native" )
message( INFO "CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}" )
# sources to build
set(big_memcpy_test_SRCS
main.cpp
)
# create an executable file named "big_memcpy_test" from
# the source files in the variable "big_memcpy_test_SRCS".
add_executable(big_memcpy_test ${big_memcpy_test_SRCS})
测试结果
Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | NUMA nodes (numactl --hardware)
---------------------------------------------------------------------------------------------
Laptop 1 | 0 | 127 | 113 | 1
Laptop 2 | 0 | 180 | 120 | 1
Server 1 | 0 | 306 | 301 | 2
Server 2 | 0 | 352 | 325 | 2
正如您所看到的,我们服务器上的memcpys和memset比我们笔记本电脑上的memcpys和memset慢得多。
改变缓冲区大小
我尝试过100MB到5GB的缓冲区,但结果相似(服务器比笔记本电脑慢)
NUMA亲和力
我读到了与NUMA有性能问题的人,所以我尝试使用numactl设置CPU和内存亲和力,但结果保持不变。
服务器NUMA硬件
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 65501 MB
node 0 free: 62608 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 65536 MB
node 1 free: 63837 MB
node distances:
node 0 1
0: 10 21
1: 21 10
笔记本电脑NUMA硬件
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 16018 MB
node 0 free: 6622 MB
node distances:
node 0
0: 10
设置NUMA亲和力
$ numactl --cpunodebind=0 --membind=0 ./big_memcpy_test
非常感谢任何解决此问题的帮助。
编辑:GCC选项
基于评论,我尝试使用不同的GCC选项进行编译:
使用-march和-mtune进行编译设置为本机
g++ -std=c++0x -Wall -march=native -mtune=native -O3 -DNDEBUG -o big_memcpy_test main.cpp
结果:完全相同的表现(没有改善)
使用-O2而不是-O3
进行编译g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp
结果:完全相同的表现(没有改善)
编辑:更改memset以写入0xF而不是0以避免NULL页面(@SteveCox)
使用0以外的值进行memset时没有改善(在这种情况下使用0xF)。
编辑:Cachebench结果
为了排除我的测试程序过于简单,我下载了一个真正的基准测试程序LLCacheBench(http://icl.cs.utk.edu/projects/llcbench/cachebench.html)
我分别在每台机器上构建了基准测试,以避免架构问题。以下是我的结果。
请注意,较大的缓冲区大小的性能差异很大。测试的最后一个尺寸(16777216)在笔记本电脑上以18849.29 MB /秒和在服务器上以6710.40执行。这是性能差异的3倍。您还可以注意到服务器的性能下降比笔记本电脑更陡峭。
编辑:memmove()比服务器上的memcpy()快2倍
根据一些实验,我尝试在我的测试用例中使用memmove()而不是memcpy(),并在服务器上找到了2倍的改进。笔记本电脑上的Memmove()运行速度比memcpy()慢,但奇怪的是运行速度与服务器上的memmove()相同。这引出了一个问题,为什么memcpy这么慢?
更新了代码以测试memmove和memcpy。我必须将memmove()包装在一个函数中,因为如果我离开它内联GCC优化它并执行与memcpy()完全相同(我假设gcc将其优化为memcpy,因为它知道位置没有重叠)。
更新结果
Buffer Size: 1GB | malloc (ms) | memset (ms) | memcpy (ms) | memmove() | NUMA nodes (numactl --hardware)
---------------------------------------------------------------------------------------------------------
Laptop 1 | 0 | 127 | 113 | 161 | 1
Laptop 2 | 0 | 180 | 120 | 160 | 1
Server 1 | 0 | 306 | 301 | 159 | 2
Server 2 | 0 | 352 | 325 | 159 | 2
编辑:天真的Memcpy
根据@Salgar的建议,我已经实现了我自己的天真memcpy功能并进行了测试。
朴素的Memcpy来源
void naiveMemcpy(void* pDest, const void* pSource, std::size_t sizeBytes)
{
char* p_dest = (char*)pDest;
const char* p_source = (const char*)pSource;
for (std::size_t i = 0; i < sizeBytes; ++i)
{
*p_dest++ = *p_source++;
}
}
天真的Memcpy结果与memcpy()
相比Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop 1 | 113 | 161 | 160
Server 1 | 301 | 159 | 159
Server 2 | 325 | 159 | 159
修改:装配输出
简单的memcpy来源
#include <cstring>
#include <cstdlib>
int main(int argc, char* argv[])
{
size_t SIZE_BYTES = 1073741824; // 1GB
char* p_big_array = (char*)malloc(SIZE_BYTES * sizeof(char));
char* p_dest_array = (char*)malloc(SIZE_BYTES * sizeof(char));
memset(p_big_array, 0xA, SIZE_BYTES * sizeof(char));
memset(p_dest_array, 0xF, SIZE_BYTES * sizeof(char));
memcpy(p_dest_array, p_big_array, SIZE_BYTES * sizeof(char));
free(p_dest_array);
free(p_big_array);
return 0;
}
装配输出:这在服务器和笔记本电脑上完全相同。我节省空间而不是粘贴两者。
.file "main_memcpy.cpp"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB25:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movl $1073741824, %edi
pushq %rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
subq $8, %rsp
.cfi_def_cfa_offset 32
call malloc
movl $1073741824, %edi
movq %rax, %rbx
call malloc
movl $1073741824, %edx
movq %rax, %rbp
movl $10, %esi
movq %rbx, %rdi
call memset
movl $1073741824, %edx
movl $15, %esi
movq %rbp, %rdi
call memset
movl $1073741824, %edx
movq %rbx, %rsi
movq %rbp, %rdi
call memcpy
movq %rbp, %rdi
call free
movq %rbx, %rdi
call free
addq $8, %rsp
.cfi_def_cfa_offset 24
xorl %eax, %eax
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE25:
.size main, .-main
.ident "GCC: (GNU) 4.6.1"
.section .note.GNU-stack,"",@progbits
PROGRESS !!!! ASMLib程序
根据@tbenson的建议,我尝试使用memcpy的asmlib版本运行。我的结果最初很差但是在将SetMemcpyCacheLimit()更改为1GB(我的缓冲区的大小)后,我的速度与我的天真for循环相同!
坏消息是memmove的asmlib版本比glibc版本慢,它现在运行在300ms标记(与glcc版本的memcpy相同)。奇怪的是,在笔记本电脑上,当我将SetMemcpyCacheLimit()变为大量时,它会伤害性能......
在下面的结果中,标有SetCache的行将SetMemcpyCacheLimit设置为1073741824.没有SetCache的结果不会调用SetMemcpyCacheLimit()
使用asmlib函数的结果:
Buffer Size: 1GB | memcpy (ms) | memmove(ms) | naiveMemcpy()
------------------------------------------------------------
Laptop | 136 | 132 | 161
Laptop SetCache | 182 | 137 | 161
Server 1 | 305 | 302 | 164
Server 1 SetCache | 162 | 303 | 164
Server 2 | 300 | 299 | 166
Server 2 SetCache | 166 | 301 | 166
开始倾向于缓存问题,但是会导致什么呢?
答案 0 :(得分:23)
[我会将此作为评论,但没有足够的声誉这样做。]
我有一个类似的系统并看到类似的结果,但可以添加一些数据点:
memcpy
的方向(即转换为*p_dest-- = *p_src--
),那么你可能会比前进方向的性能差得多(对我来说约为637毫秒)。 glibc 2.12中的memcpy()
发生了变化,在重叠缓冲区(http://lwn.net/Articles/414467/)上调用memcpy
时遇到了一些错误,我认为问题是由于切换到版本{{1向后运作。因此,后向和前向副本可以解释memcpy
/ memcpy()
差异。memmove()
实现切换到大缓冲区(即大于最后一级缓存)的非临时存储(未缓存)。我测试了Agner Fog的memcpy版本(http://www.agner.org/optimize/#asmlib),发现它的速度与memcpy()
中的版本大致相同。但是,glibc
有一个函数(asmlib
),它允许设置阈值,高于该阈值时使用非临时存储。将该限制设置为8GiB(或仅大于1 GiB缓冲区)以避免非临时存储在我的情况下性能翻倍(时间低至176毫秒)。当然,这只与前向天真的表现相匹配,所以它并不是一流的。SetMemcpyCacheLimit
(104毫秒)的9.6 GB / s。 Haswell系统的RAM是DDR3-1600(与其他系统相同)。<强>更新强>
答案 1 :(得分:10)
这对我来说很正常。
管理带有两个CPU的8x16GB ECC记忆棒比使用2x2GB的单个CPU要困难得多。你的16GB硬盘是双面内存+它们可能有缓冲区+ ECC(甚至在主板级禁用)......所有这些都使数据路径更长时间。你也有2个CPU共享ram,即使你在另一个CPU上什么也不做,总是很少有内存访问。切换此数据需要一些额外的时间。只要看看在与显卡共享某些内存的PC上丢失的巨大性能。
你的服务器仍然是非常强大的数据泵。我不确定在现实生活中的软件中经常复制1GB,但我确信你的128GB比任何硬盘都要快得多,即使是最好的SSD,这也是你可以充分利用服务器的地方。使用3GB进行相同测试会使您的笔记本电脑着火。
这看起来是基于商品硬件的架构如何比大型服务器更高效的完美示例。在这些大型服务器上花费的钱可以买多少台消费者电脑?
感谢您提出非常详细的问题。
编辑:(花了我很长时间才写下这个答案,我错过了图表部分。)
我认为问题在于数据的存储位置。你能比较一下这个:
通过这种方式,您可以看到内存控制器如何处理远离彼此的内存块。我认为你的数据放在不同的内存区域,它需要在数据路径上的某个点进行切换操作,以便与一个区域进行通信,然后与另一个区域进行通信(双面内存存在这样的问题)。
另外,您确定线程绑定到一个CPU吗?
编辑2:
有几种&#34;区域&#34;记忆的分隔符。 NUMA是一个,但这不是唯一的。例如,双面支撑杆需要标记来指向一侧或另一侧。在图表中查看即使在笔记本电脑上(没有NUMA),性能也会因大块内存而降低。 我不确定这一点,但是memcpy可能会使用硬件功能来复制ram(一种DMA),而且这个芯片的缓存必须比CPU少,这可以解释为什么使用CPU的哑副本比memcpy快。
答案 2 :(得分:8)
基于IvyBridge的笔记本电脑中的一些CPU改进可能会比基于SandyBridge的服务器有所提升。
Page-crossing Prefetch - 只要您到达当前的一个线性页面,您的笔记本电脑CPU就会预先提取下一个线性页面,每次都会为您节省一个令人讨厌的TLB错过。要尝试缓解这种情况,请尝试为2M / 1G页面构建服务器代码。
缓存替换方案似乎也得到了改进(参见一个有趣的逆向工程here)。如果这个CPU确实使用了动态插入策略,那么它很容易阻止你复制的数据试图破坏你的Last-Level-Cache(由于它的大小无法有效地使用它),并为其他有用的缓存节省空间像代码,堆栈,页表数据等。)。要对此进行测试,您可以尝试使用流式加载/存储(movntdq
或类似的)来重建您的天真实现,您也可以使用gcc builtin。这种可能性可以解释大数据集大小的突然下降。
我相信对字符串复制也做了一些改进(here),它可能适用于此处,也可能不适用,具体取决于汇编代码的外观。您可以尝试使用Dhrystone进行基准测试,以测试是否存在固有差异。这也可以解释memcpy和memmove之间的区别。
如果您能够获得基于IvyBridge的服务器或Sandy-Bridge笔记本电脑,那么最简单的方法就是测试所有这些服务器。
答案 3 :(得分:4)
我修改了基准测试以在Linux中使用nsec计时器,并在不同处理器上发现了类似的变体,所有处理器都具有相似的内存。所有正在运行的RHEL 6.数字在多次运行中都是一致的。
Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, L2/L3 256K/20M, 16 GB ECC
malloc for 1073741824 took 47us
memset for 1073741824 took 643841us
memcpy for 1073741824 took 486591us
Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, L2/L3 256K/12M, 12 GB ECC
malloc for 1073741824 took 54us
memset for 1073741824 took 789656us
memcpy for 1073741824 took 339707us
Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, L2 256K/8M, 12 GB ECC
malloc for 1073741824 took 126us
memset for 1073741824 took 280107us
memcpy for 1073741824 took 272370us
以下是内联C代码-O3
的结果Sandy Bridge E5-2648L v2 @ 1.90GHz, HT enabled, 256K/20M, 16 GB
malloc for 1 GB took 46 us
memset for 1 GB took 478722 us
memcpy for 1 GB took 262547 us
Westmere E5645 @2.40 GHz, HT not enabled, dual 6-core, 256K/12M, 12 GB
malloc for 1 GB took 53 us
memset for 1 GB took 681733 us
memcpy for 1 GB took 258147 us
Jasper Forest C5549 @ 2.53GHz, HT enabled, dual quad-core, 256K/8M, 12 GB
malloc for 1 GB took 67 us
memset for 1 GB took 254544 us
memcpy for 1 GB took 255658 us
对于它,我还尝试使内联memcpy一次做8个字节。 在这些英特尔处理器上,它没有明显的区别。 Cache将所有字节操作合并到最小数量的内存操作中。我怀疑gcc库代码试图太聪明。
答案 4 :(得分:3)
这个问题已经回答了above,但无论如何,这是一个使用AVX的实现,对于大型副本应该更快,如果这是你担心的:
#define ALIGN(ptr, align) (((ptr) + (align) - 1) & ~((align) - 1))
void *memcpy_avx(void *dest, const void *src, size_t n)
{
char * d = static_cast<char*>(dest);
const char * s = static_cast<const char*>(src);
/* fall back to memcpy() if misaligned */
if ((reinterpret_cast<uintptr_t>(d) & 31) != (reinterpret_cast<uintptr_t>(s) & 31))
return memcpy(d, s, n);
if (reinterpret_cast<uintptr_t>(d) & 31) {
uintptr_t header_bytes = 32 - (reinterpret_cast<uintptr_t>(d) & 31);
assert(header_bytes < 32);
memcpy(d, s, min(header_bytes, n));
d = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(d), 32));
s = reinterpret_cast<char *>(ALIGN(reinterpret_cast<uintptr_t>(s), 32));
n -= min(header_bytes, n);
}
for (; n >= 64; s += 64, d += 64, n -= 64) {
__m256i *dest_cacheline = (__m256i *)d;
__m256i *src_cacheline = (__m256i *)s;
__m256i temp1 = _mm256_stream_load_si256(src_cacheline + 0);
__m256i temp2 = _mm256_stream_load_si256(src_cacheline + 1);
_mm256_stream_si256(dest_cacheline + 0, temp1);
_mm256_stream_si256(dest_cacheline + 1, temp2);
}
if (n > 0)
memcpy(d, s, n);
return dest;
}
答案 5 :(得分:3)
这些数字对我来说很有意义。这里实际上有两个问题,我会回答它们。
首先,我们需要有一个心理模型,说明有多大 1 内存传输在现代英特尔处理器上运行。这个描述是近似,细节可能会从架构到架构有所改变,但高层次的想法是相当稳定的。
L1
数据缓存中的加载未命中时,将分配行缓冲区,它将跟踪未命中请求,直到填充为止。这可能是短时间(十几个周期左右),如果它在L2
缓存中,或者更长(100+纳秒),如果它一直错过DRAM。内存子系统本身具有最大带宽限制,您可以在ARK上方便地找到它。例如,Lenovo笔记本电脑中的3720QM显示限制为25.6 GB。此限制基本上是每次传输的有效频率(1600 Mhz
)乘以8字节(64位)乘以通道数(2)的乘积:1600 * 8 * 2 = 25.6 GB/s
。手上的服务器芯片每个插槽的峰值带宽为51.2 GB/s,总系统带宽约为102 GB / s。
与其他处理器功能不同,因此,在各种芯片中通常只有可能的理论带宽数 它仅取决于许多人经常使用的注释值 不同的芯片,甚至跨架构。这是不现实的 期望DRAM以理论速率完全交付(由于各种原因) 低级别的担忧,讨论了一下 here),但你经常可以得到 大约90%或更多。
因此(1)的主要结果是你可以将RAM作为一种请求响应系统处理。 DRAM未命中分配填充缓冲区,并在请求返回时释放缓冲区。每个CPU只有10个缓冲区用于需求未命中,这会对单个CPU可以生成的需求内存带宽产生严格限制,这是其延迟的函数。
例如,假设您的E5-2680
的DRAM延迟为80ns。每个请求都会带来一个64字节的高速缓存行,所以你只是按顺序向DRAM发出请求,你预计会有一个微不足道的吞吐量64 bytes / 80 ns = 0.8 GB/s
,你再次将其减少一半(至少)获得memcpy
数字,因为它需要读取和写入。幸运的是,您可以使用10个行填充缓冲区,这样您就可以将10个并发请求重叠到内存中,并将带宽增加10倍,从而使理论带宽达到8 GB / s。
如果你想深入了解更多细节,this thread几乎是纯金。您会发现John McCalpin, aka "Dr Bandwidth中的事实和数据将成为下面的共同主题。
让我们深入了解细节并回答两个问题......
您向笔记本电脑系统展示了大约 120毫秒的memcpy
基准测试,而服务器部件则需要 300毫秒。您还表明,这种缓慢主要不是根本性的,因为您可以使用memmove
和手卷memcpy(以下简称hrm
)来实现大约 160 ms的时间强>,比笔记本电脑性能更接近(但仍然慢)。
我们已经在上面表明,对于单核,带宽受到总可用并发和延迟的限制,而不是DRAM带宽。我们希望服务器部件可能具有更长的延迟,但不会300 / 120 = 2.5x
更长!
答案在于流媒体(又名非临时)商店。您使用的memcpy
的libc版本使用它们,但memmove
没有。你和你的&#34;天真的&#34; memcpy
也没有使用它们,以及我配置asmlib
两者都使用流式存储(慢)而不是(快)。
流媒体商店会损害单CPU 号码,因为:
上述链接线程中John McCalpin的引用更好地解释了这两个问题。关于预取有效性和流媒体存储的主题he says:
&#34;普通&#34;存储,L2硬件预取器可以获取行 提前并减少线路填充缓冲器占用的时间, 从而增加持续带宽。另一方面,与 流(缓存旁路)存储,行填充缓冲区条目 商店被占用传递数据所需的全部时间 DRAM控制器。在这种情况下,加载可以加速 硬件预取,但商店不能,所以你得到一些加速, 但是如果装载和存储都没有那么多 加速。
...然后,对于E5上的流媒体商店显然更长的延迟,he says:
更简单的&#34; uncore&#34; Xeon E3可能导致显着降低 流式商店的线路填充缓冲区占用率。 Xeon E5有一个 更复杂的环形结构导航,以便交出 流存储从核心缓冲区到内存控制器,所以 占用率可能比内存大一些(读取) 等待时间。
特别是,麦卡尔平博士测量的E5减速幅度为1.8倍,而使用&#34;客户端的芯片则为#34; uncore,但OP报告的2.5倍减速与STREAM TRIAD报告的1.8x得分一致,STREAM TRIAD的负载比率为2:1,而memcpy
为1:1,商店是有问题的部分。
这并不会使流式传输成为一件坏事 - 实际上,您需要通过延迟来减少总带宽消耗。您获得的带宽较少,因为在使用单核时您的并发性受限,但是您可以避免所有读取所有权流量,因此如果您在所有核心上同时运行测试,您可能会看到(小)优势。
到目前为止,作为您的软件或硬件配置的工件,其他用户使用相同的CPU报告完全相同的减速。
即使纠正了非临时存储问题,您仍然仍然看到服务器部件大致160 / 120 = ~1.33x
减速。是什么给了什么?
这是一个常见的谬论,即服务器CPU在所有方面都更快或至少与客户端相同。事实并非如此 - 您在服务器部件上支付的费用(通常在每片2000美元左右)主要是(a)更多核心(b)更多内存通道(c)支持更多总RAM(d )支持&#34; enterprise-ish&#34; ECC,虚拟化功能等功能 5 。
事实上,延迟方面,服务器部件通常只与其客户端 4 部件相同或更慢。当谈到内存延迟时,尤其如此,因为:
因此,服务器部件的延迟通常比客户端部件长40%到60%。对于E5,您可能会发现~80 ns是typical latency到RAM,而客户端部分接近50 ns。
因此,RAM延迟受限的任何事情都会在服务器部件上运行得更慢,事实证明,单个核心上的memcpy
受到延迟限制。由于memcpy
似乎像带宽测量一样令人困惑,对吗?如上所述,单个内核没有足够的资源来一次保留足够的RAM请求以接近RAM带宽 6 ,因此性能直接取决于延迟。
有很多好消息来源可以阅读更多关于这个东西,这里有几个。
MemLatX86
和NewMemLat
)链接 1 large 我的意思是比LLC大一些。对于适合LLC(或任何更高的缓存级别)的副本,行为是非常不同的。 OP llcachebench
图表显示,实际上性能偏差仅在缓冲区开始超过LLC大小时开始。
2 特别是,行填充缓冲区的数量已有几代apparently been constant at 10,包括此问题中提到的体系结构。
3 当我们在这里说 demand 时,我们的意思是它与代码中的显式加载/存储相关联,而不是说是由预取引入。
4 当我在这里引用服务器部分时,我的意思是带有服务器非核心的CPU。这在很大程度上意味着E5系列,因为E3系列通常是uses the client uncore。
5 将来,您可以添加&#34;指令集扩展&#34;在此列表中,似乎AVX-512
仅出现在Skylake服务器部件上。
6 每little's law,延迟为80 ns,我们始终需要(51.2 B/ns * 80 ns) == 4096 bytes
或64个缓存线才能达到最大带宽,但是一个核心提供的不到20个。
答案 6 :(得分:0)
服务器1规格
- CPU:2x Intel Xeon E5-2680 @ 2.70 Ghz
服务器2规格
- CPU:2x Intel Xeon E5-2650 v2 @ 2.6 Ghz
根据英特尔ARK,E5-2650和E5-2680都有AVX扩展名。
构建CMake文件
这是您问题的一部分。 CMake为你选择一些相当差的旗帜。您可以通过运行make VERBOSE=1
确认它。
您应该将-march=native
和-O3
添加到CFLAGS
和CXXFLAGS
。您可能会看到性能的显着提高。它应该参与AVX扩展。如果没有-march=XXX
,您可以有效地获得最小的i686或x86_64计算机。如果没有-O3
,则不会参与GCC的矢量化。
我不确定GCC 4.6是否能够使用AVX(以及朋友,如BMI)。我知道GCC 4.8或4.9是有能力的,因为当GCC将memcpy和memset外包给MMX单元时,我不得不寻找导致段错误的对齐错误。 AVX和AVX2允许CPU一次操作16字节和32字节数据块。
如果GCC错失了将对齐数据发送到MMX单元的机会,则可能会错过数据对齐的事实。如果您的数据是16字节对齐的,那么您可以尝试告诉GCC,以便它知道对胖块进行操作。为此,请参阅GCC的__builtin_assume_aligned
。另请参阅How to tell GCC that a pointer argument is always double-word-aligned?
由于void*
,这看起来也有点怀疑。它抛弃了关于指针的信息。您应该保留信息:
void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
memmove(pDest, pSource, sizeBytes);
}
可能类似以下内容:
template <typename T>
void doMemmove(T* pDest, const T* pSource, std::size_t count)
{
memmove(pDest, pSource, count*sizeof(T));
}
另一个建议是使用new
,然后停止使用malloc
。它是一个C ++程序,GCC可以对new
做一些关于malloc
无法做出的假设。我相信GCC的内置插件选项页面中详细介绍了一些假设。
另一个建议是使用堆。它在典型的现代系统上始终是16字节对齐的。 GCC应该认识到,当涉及到堆的指针时,它可以卸载到MMX单元(没有潜在的void*
和malloc
问题)。
最后,有一段时间,Clang在使用-march=native
时没有使用本机CPU扩展。例如,请参阅Ubuntu Issue 1616723, Clang 3.4 only advertises SSE2,Ubuntu Issue 1616723, Clang 3.5 only advertises SSE2和Ubuntu Issue 1616723, Clang 3.6 only advertises SSE2。