Linux에서의 낮은 memcpy 퍼포먼스
최근 새로운 서버를 구입하여 memcpy 퍼포먼스가 저하되고 있습니다.서버의 memcpy 퍼포먼스는 당사의 노트북에 비해 3배 느립니다.
서버 사양
- 섀시 및 Mobo: SUPER MICRO 1027GR-TRF
- CPU: 인텔 Xeon E5-2680 (2.70 GHz)x 2
- 메모리: 16GB DDR3 1600MHz x 8
편집: 조금 더 높은 사양의 다른 서버에서도 테스트하고 있으며, 위의 서버와 같은 결과를 보고 있습니다.
서버 2의 사양
- 섀시 및 Mobo: SUPER MICRO 10227GR-TRFT
- CPU: 인텔 Xeon E5-2650 v2 (2.6 Ghz)x 2
- 메모리: 16GB DDR3 1866MHz x 8
노트북 사양
- 섀시:Lenovo W530
- CPU: 인텔 Core i7 i7-3720QM (2.6Ghz)x 1
- 메모리: 4 GB DDR3 1600MHz x 4
운영 체제
$ 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로 테스트했습니다.컴파일러 간에 성능 차이는 없었습니다.
테스트 코드 아래 테스트 코드는 생산 코드에서 발생한 문제를 재현하기 위한 캔 테스트입니다.이 벤치마크가 단순하다는 것은 알지만 우리의 문제를 이용하고 식별할 수 있었습니다.이 코드에 의해 2개의 1GB 버퍼와 그 사이에 memcpy가 생성되어 memcpy 콜의 타이밍이 설정됩니다.명령줄에서 대체 버퍼 크기를 지정하려면 ./big_memcpy_test [SIZE_B]를 사용합니다.YTES]
#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
보시다시피 서버의 memcpy와 memset은 노트북의 memcpy와 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
결과: 퍼포먼스 완전 동일 (개선 없음)
-O3 대신 -O2로 컴파일
g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp
결과: 퍼포먼스 완전 동일 (개선 없음)
편집: NULL 페이지(@SteveCox)를 피하기 위해 memset을 0이 아닌 0xF로 변경했습니다.
0 이외의 값(이 경우 0xF 사용)으로 memsetting을 실시해도 개선되지 않습니다.
편집: Cachebench 결과
테스트 프로그램이 너무 단순하다는 것을 배제하기 위해 실제 벤치마크 프로그램 LLCacheBench(http://icl.cs.utk.edu/projects/llcbench/cachebench.html)를 다운로드했습니다.
아키텍처의 문제를 피하기 위해 각 머신에 벤치마크를 개별적으로 구축했습니다.다음은 저의 결과입니다.
매우 큰 차이는 큰 버퍼 크기에서의 퍼포먼스입니다.마지막으로 테스트한 사이즈(16777216)는 노트북에서는 18849.29 MB/초, 서버에서는 6710.40으로 동작했습니다.이는 성능의 약 3배 차이입니다.또, 서버의 퍼포먼스 저하가 노트북보다 훨씬 가팔라집니다.
편집: 서버의 memmove()가 memcpy()보다 2배 빠릅니다.
몇 가지 실험을 통해 테스트 케이스에서 memcpy() 대신 memmove()를 사용해보니 서버 성능이 2배 향상되었습니다.노트북의 memmove()는 memcpy()보다 느리게 실행되지만 이상하게도 서버의 memmove()와 같은 속도로 실행됩니다.여기서 의문이 생깁니다.왜 memcpy는 이렇게 느리죠?
memcpy와 함께 memmove를 테스트하는 코드가 업데이트되었습니다.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
편집: Naigive Memcpy
@Salgar의 제안에 따라 나만의 순진한 memcpy 함수를 구현하여 테스트하였습니다.
Naigive 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()와 비교한 Naigive 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
진행중!!!asmlib.
@tbenson의 제안에 따라 asmlib 버전의 memcpy를 실행해 보았습니다.처음에는 결과가 좋지 않았지만 Set MemcpyCacheLimit()를 1GB(버퍼 크기)로 변경한 후 순진한 루프와 같은 속도로 실행되었습니다.
단, memmove의 asmlib 버전이 glibc 버전보다 느리고 현재 300ms 마크(memcpy의 glib 버전과 동등)로 동작하고 있습니다.이상한 점은 노트북에서 많은 수의 MemcpyCacheLimit()를 설정하면 퍼포먼스가 저하된다는 것입니다.
다음 결과에서 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
캐시 문제로 기울기 시작했는데, 그 원인은 무엇일까요?
[코멘트하고 싶지만 평판이 좋지 않다]
유사한 시스템을 사용하고 있으며 유사한 결과를 볼 수 있지만 다음과 같은 데이터 포인트를 추가할 수 있습니다.
- 했던 방향을
memcpy
(로 ), (로 변환)*p_dest-- = *p_src--
)의 경우는, 순방향보다 퍼포먼스가 큰폭으로 저하할 가능성이 있습니다(저의 경우는 약 637밀리초). there there there there in in in in in in in에 변화가 .memcpy()
2.에서 glibc 2.12를 호출하기 몇 되었습니다.memcpy
오버랩 버퍼(http://lwn.net/Articles/414467/) 및 이 문제는 버전 변경으로 인해 발생한 것으로 생각됩니다.memcpy
역방향으로 동작합니다.거꾸로 할 수 .memcpy()
/memmove()
격차 - 비임시점포는 사용하지 않는 것이 좋을 것 같습니다.되어 있는 많은 ★★★★★
memcpy()
구현은 대용량 버퍼(예: 마지막 수준의 캐시보다 큰)를 위해 비캐시 저장소(캐시되지 않음)로 전환됩니다.Agner Fog의 memcpy 버전(http://www.agner.org/optimize/#asmlib)을 테스트한 결과, 의 버전과 거의 같은 속도였습니다.glibc
asmlib
으)는SetMemcpyCacheLimit
를 사용하여 할 수 : 비표준 스토어가 사용되는 임계값을 설정할 수 있습니다.이 제한을 8GiB(또는 1GiB 버퍼보다 조금 더 큰)로 설정하면 비일시적인 스토어를 회피할 수 있습니다(시간 단축은 176ms).물론, 그것은 전진적인 순진한 퍼포먼스와 일치했을 뿐이기 때문에, 뛰어난 것은 아닙니다. - 이러한 시스템의 BIOS 에서는, 4 개의 다른 하드웨어 프리페처를 유효/비활성화할 수 있습니다(MLC Streamer Prefetcher, MLC Spatial Prefetcher, DCU Streamer Prefetcher, DCU IP 프리페처).각각의 디세블로 해 보았습니다만, 퍼포먼스 패리티를 유지하고, 몇개의 설정에서는 퍼포먼스를 저하시킵니다.
- Running Average Power Limit(RAPL; 실행 평균 전력 제한) DRAM 모드를 디세블로 해도 영향은 없습니다.
- Fedora 19 (glibc 2.17) Supermicro입니다.Supermicro X9DRG-HF, Fedora 19, Xeon E5-2670 CPU, Supermicro X9DRG-HF.및19를 하고 있는 에서는 Xeon E3-1275 v3(Haswell)의 경우 9.6 됩니다.
memcpy
(104ms).Haswell (램) DDR3-1600 (램) has 、 DDR3 - 1600 ) 。
갱신
- CPU 전원 관리를 최대 퍼포먼스로 설정하고 BIOS에서 하이퍼스레딩을 비활성화했습니다.에에 based based
/proc/cpuinfo
후 그러나 이로 인해 메모리 성능이 10% 정도 저하되었습니다. - memtest86+ 4.10은 9091 MB/s의 메인메모리에 대역폭을 보고합니다.이것이 읽기, 쓰기 또는 복사에 해당하는지 찾을 수 없었습니다.
- STREAM 벤치마크에서는 복사용으로 13422MB/s가 보고되지만 읽기 및 쓰기 모두 바이트가 카운트되므로 위의 결과와 비교할 경우 최대 6.5GB/s에 해당합니다.
그 숫자들은 내게 이해가 간다.사실 여기 두 가지 질문이 있는데, 둘 다 대답할게요.
다만, 우선, 최신의 인텔·프로세서등에서, 메모리 전송의1 대용량을 염두에 둘 필요가 있습니다.이 설명은 대략적인 것이며 세부 사항은 아키텍처마다 다소 달라질 수 있지만, 개괄적인 아이디어는 상당히 일정합니다.
- 가 「」에서 .
L1
데이터 캐시: 회선 버퍼가 할당되어 있습니다.이 버퍼는, 기입될 때까지 미스의 요구를 추적합니다.이것은, 단시간(수십 사이클 정도)에 해당하는 경우가 있습니다.L2
캐시, 또는 DRAM에 도달하지 못할 경우(100나노초 이상)보다 길어집니다. - 이러한 회선 버퍼의 수는 코어마다1 한정되어 있어, 그것들이 가득 차면, 그 이후의 누락은 1개의 회선 버퍼를 기다리는 동안 정지합니다.
- 디맨드 로드/스토어에 사용되는3 이러한 채우기 버퍼 외에 DRAM과 L2 간의 메모리 이동용 버퍼와 프리페치에 의해 사용되는 하위 수준의 캐시가 있습니다.
메모리 서브시스템 자체에는 최대 대역폭 제한이 있습니다.이러한 대역폭은 ARK에 기재되어 있습니다.예를 들어 Lenovo 노트북의 3720QM에는 25.6GB의 제한이 있습니다.이 제한은 기본적으로 유효 주파수의 곱입니다.
1600 Mhz
x) x 채널(2 x "8" x "64" x "2":1600 * 8 * 2 = 25.6 GB/s
서버 칩의 최대 대역폭은 소켓당 51.2GB/s이며, 총 시스템 대역폭은 102GB/s입니다.다른 프로세서의 기능과 달리 이론상 대역폭 수치는 칩 전체에 걸쳐 존재할 수 있습니다.이는 많은 다른 칩에서 심지어 아키텍처 전체에서 동일한 주목값에만 의존할 수 있는 것은 기재된 값뿐입니다.DRAM이 이론적인 속도로 제공되기를 기대하는 것은 비현실적이지만(다양한 저수준의 우려로 인해 여기서 잠시 설명함) 많은 경우 약 90% 이상을 얻을 수 있습니다.
따라서 (1)의 주요 결과는 RAM에 대한 오류를 일종의 요청 응답 시스템으로 처리할 수 있다는 것입니다.DRAM에 대한 미스는 채우기 버퍼를 할당하고 요구가 돌아왔을 때 버퍼가 해방됩니다.디맨드 미스용 버퍼는 CPU당 10개뿐입니다.이는 지연의 함수로 단일 CPU가 생성할 수 있는 디맨드 메모리 대역폭에 엄격한 제한을 가합니다.
를 들어, '어울리다'라고 요.E5-2680
80ns DRAM행을 따라서 됩니다.64 bytes / 80 ns = 0.8 GB/s
( memcpy
읽고 써야 하기 때문에 수치로 표현합니다.다행히 10개의 라인 채우기 버퍼를 사용할 수 있기 때문에 메모리에 대한 10개의 동시 요구를 오버랩하여 대역폭을 10배로 늘릴 수 있으므로 이론상 대역폭은 8GB/s가 됩니다.
좀 더 자세히 들여다보면 이 실은 거의 순금이다.John McCalpin의 사실과 수치는 "Dr Bandwidth는 아래 일반적인 주제가 될 것입니다.
그럼 이제 세부사항을 살펴보고 두 가지 질문에 답해볼까요?
memcpy가 서버의 memmove 또는 수동 복사보다 훨씬 느린 이유는 무엇입니까?
이 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★」memcpy
벤치마크에 걸리는 시간은 약 120밀리초이며 서버 부품은 약 300밀리초입니다.또한 이 느림 현상은 기본적이지 않다는 것을 보여주었습니다.memmove
memcpy, memcpy, memcpy,hrm
약 160밀리초의 시간을 실현하는 것으로, 노트북의 퍼포먼스보다 훨씬 (단, 아직 느린) 것입니다.
앞에서 설명한 바와 같이 단일 코어의 대역폭은 DRAM 대역폭이 아니라 사용 가능한 총 동시성과 지연에 의해 제한됩니다.지연 수 시간은 길어질 수 없습니다.300 / 120 = 2.5x
길게 더 길게!
답은 스트리밍(비임시적) 스토어에 있습니다.의 libc 버전memcpy
「사용하고 있습니다」라고 하는 은,memmove
하다, 순진하다, 순진하다, 순진하다, 순진하다, 하다, 순진하다, 순진하다, 순진하다, 순진하다, 순진하다, 순진하다, 순진하다, 하다, 순진하게.memcpy
, 제도 안 쓰이고, 제 설정도 안 쓰이고,asmlib
둘 다 스트리밍 스토어를 사용합니다(느림). (빠름)이 아닙니다.
스트리밍 스토어는 다음과 같은 이유로 단일 CPU 번호를 손상시킵니다.
- (A) 프리페치가 저장될 행을 캐시에 가져오는 것을 방지합니다.프리페치 하드웨어에는 부하/스토어가 사용하는 10개의 채우기 버퍼를 넘는 다른 전용 버퍼가 있기 때문에 동시성이 향상됩니다.
- (B) E5-2680은 스트리밍 스토어에서 특히 느린 것으로 알려져 있습니다.
두 문제 모두 위의 링크된 스레드에서 John McCalpin의 인용문으로 더 잘 설명되어 있습니다.프리페치 효과와 스트리밍 스토어에 대해 그는 다음과 같이 말한다.
「일반」스토어를 사용하면, L2 하드웨어 프리페처는 회선을 미리 취득해, 회선 채우기 버퍼가 점유되는 시간을 단축할 수 있기 때문에, 지속 대역폭이 증가합니다.한편 스트리밍(캐시 바이패스) 스토어에서는 데이터를 DRAM 컨트롤러에 전달하는데 필요한 모든 시간 동안 스토어의 Line Fill Buffer 엔트리가 점유됩니다.이 경우 하드웨어 프리페치에 의해 로드를 가속할 수 있지만 스토어는 고속화할 수 없기 때문에 어느 정도 속도를 높일 수 있지만 로드와 스토어를 모두 가속화할 경우 얻을 수 있는 만큼의 속도는 얻을 수 없습니다.
...그리고 E5의 스트리밍 스토어의 대기시간이 훨씬 길어지는 것에 대해 그는 다음과 같이 말합니다.
Xeon E3의 심플한 「언코어」는, 스트리밍 스토어의 Line Fill Buffer 점유율을 큰폭으로 낮출 수 있습니다.Xeon E5는 코어 버퍼에서 메모리컨트롤러로 스트리밍 스토어를 전달하기 위해 탐색하는 링 구조가 매우 복잡하기 때문에 점유율이 메모리(읽기) 지연보다 큰 폭으로 다를 수 있습니다.
. 미포함가 약 1.느리다는 STREAM TRIAD의 1되고 있기 의 속도는 2. 느리다는 합니다. TRIAD는 2:이 "Store, "McCal"는 "Store"의 경우 1.8배입니다. 느리다고 보고되었기 때문에 OP 보고서는 일관성이 있습니다. STREAM TRIAD는 2:1의 부하:스토어 비율을 가지고 있습니다.memcpy
1을 1로, 1을 1로, 1을 1로, 1을 1로, 1을 1로, 1을 1로 하다.
이로 인해 스트리밍이 나쁜 것은 아닙니다.즉, 지연 시간을 더 적은 총 대역폭 소비로 교환하는 것입니다.단일 코어를 사용하면 동시성이 제한되기 때문에 대역폭이 줄어들지만 모든 읽기/소유 트래픽을 피할 수 있으므로 모든 코어로 테스트를 동시에 실행하면 (작은) 이점을 얻을 수 있습니다.
소프트웨어나 하드웨어 구성의 아티팩트가 아니라 CPU가 동일한 다른 사용자가 동일한 속도 저하를 보고했습니다.
일반 매장을 이용할 때 서버 부분이 왜 아직 느리죠?
비일시적인 스토어 문제를 수정한 후에도 대략적으로160 / 120 = ~1.33x
서버 부품의 속도가 느려집니다.왜왜????
서버 CPU가 모든 면에서 클라이언트 CPU보다 빠르거나 적어도 동등하다는 것은 일반적인 오류입니다.그러나 실제로는 그렇지 않습니다.서버 부품에 대한 비용(대부분 칩당 2,000달러 정도)은 (a)코어 증가 (b)메모리 채널 증가 (c)ECC, 가상화 기능 등 엔터프라이즈5 수준의 기능을 위한 총 RAM 지원 증가 (d)입니다.
실제로 지연 시간 측면에서 서버 부품은 일반적으로4 클라이언트 부품과 같거나 느립니다.메모리 지연에 관해서는 특히 다음과 같은 이유로 그렇습니다.
- 서버 부품은 확장성이 뛰어나지만 복잡한 "코어 해제"를 가지고 있어 많은 경우 더 많은 코어를 지원해야 하므로 RAM으로의 경로가 길어집니다.
- 서버 부품은 더 많은 RAM(100 GB 또는 수 TB)을 지원합니다.이러한 대용량을 지원하려면 많은 경우 전기 버퍼가 필요합니다.
- OP의 경우와 마찬가지로 서버 부품은 보통 멀티 소켓이므로 메모리 경로에 크로스 소켓 일관성에 대한 우려가 높아집니다.
따라서 일반적으로 서버 부품은 클라이언트 부품보다 레이텐시가 40~60% 길어집니다.E5의 경우 RAM의 일반적인 지연 시간은 최대 80ns인 반면 클라이언트 부품은 50ns에 가깝습니다.
RAM 된 모든 부품에서 가 느려집니다. 「RAM」은 「RAM」이라고 하는 이 판명되었습니다.★★★★★★★★★★★★★★★★★★·memcpy
단일 코어의 경우 지연 시간이 제한됩니다.혼란스럽네요 왜냐하면memcpy
대역폭을 측정하는 것 같죠?위에서 설명한 바와 같이 단일 코어는 RAM6 대역폭에 근접하기 위해 RAM에 대한 충분한 요구를 동시에 처리할 수 있는 리소스가 부족하기 때문에 퍼포먼스는 레이텐시에 직접 의존합니다.
한편 클라이언트 칩은 레이텐시와 대역폭이 모두 낮기 때문에 1개의 코어가 대역폭을 포화시키는 데 더 가까워집니다(이것은 스트리밍 스토어가 클라이언트 부품에 큰 도움이 되는 이유이기도 합니다).단일 코어라도 RAM 대역폭에 근접할 수 있는 경우에는 스트림 스토어가 제공하는 50% 스토어 대역폭 삭감이 큰 도움이 됩니다.
레퍼런스
자세한 내용을 읽을 수 있는 좋은 소스가 많이 있습니다. 여기 몇 가지 있습니다.
- 메모리 지연 시간 구성 요소에 대한 자세한 설명
- 새로운 CPU와 오래된 CPU 간에 많은 메모리 지연이 발생합니다( 참조).
MemLatX86
★★★★★★★★★★★★★★★★★」NewMemLat
링크 - Sandy Bridge(및 Opteron) 메모리 레이텐시의 상세 분석 - OP에서 사용하는 칩과 거의 동일합니다.
1 대체로 나는 단지 LLC보다 조금 더 큰 것을 의미한다.LLC(또는 캐시 레벨이 높은 경우)에 적합한 복사본의 동작은 매우 다릅니다.OPsllcachebench
그래프는 실제로 버퍼가 LLC 크기를 초과하기 시작했을 때만 성능 편차가 시작됨을 나타냅니다.
2 특히 라인필 버퍼의 수는 이 질문에서 언급한 아키텍처를 포함하여 몇 세대에 걸쳐 10으로 일정하게 유지되고 있습니다.
3 여기서 demand라고 하는 것은 프리페치에 의해 도입되고 있다고 하는 것이 아니라, 코드의 명시적인 로드/스토어와 관련되어 있는 것을 의미합니다.
4 여기서 서버 부품을 언급하는 것은 서버가 저장되지 않은 CPU를 의미합니다.이는 E3 시리즈는 일반적으로 클라이언트를 사용하지 않기 때문에 E5 시리즈를 의미합니다.
5 향후에는 이 목록에 "명령어 세트 확장자"를 추가할 수 있을 것 같습니다.AVX-512
스카이레이크
6 80ns의 대기시간에서 작은 법칙에 따라(51.2 B/ns * 80 ns) == 4096 bytes
최대 대역폭에 도달하기 위해 항상 64개의 캐시 회선을 가동하고 있지만 1개의 코어가 20개 미만입니다.
이건 평범해 보여요.
2개의 CPU로 8x16GB ECC 메모리 스틱을 관리하는 것은 2x2GB의 단일 CPU보다 훨씬 어려운 작업입니다.16GB 스틱은 양면 메모리 + 버퍼 + ECC (마더보드 레벨에서는 무효가 되어 있는 경우도 있습니다)...RAM에 대한 데이터 경로를 훨씬 더 길게 만듭니다.또, 2개의 CPU가 메모리를 공유하고 있어, 다른 CPU로 아무것도 하지 않아도, 항상 메모리 액세스가 거의 없습니다.이 데이터를 전환하려면 시간이 좀 더 필요합니다.그래픽 카드와 램을 공유하는 PC의 퍼포먼스가 크게 저하되고 있는 것을 봐 주세요.
그래도 서버는 정말 강력한 데이터펌프입니다.실제 소프트웨어에서 1GB의 복사가 자주 발생하는 것은 아니지만 128GB는 하드 드라이브보다 훨씬 빠를 것입니다.최고의 SSD라고 해도, 이것이 서버를 활용할 수 있는 부분입니다.3GB에서도 같은 테스트를 하면 노트북에 불이 붙습니다.
이는 범용 하드웨어를 기반으로 하는 아키텍처가 대형 서버보다 훨씬 더 효율적일 수 있음을 보여주는 완벽한 예입니다.이러한 대규모 서버에 드는 돈으로 몇 대의 개인 사용자를 위한 PC를 구입할 수 있습니까?
매우 상세한 질문 감사합니다.
(이 답을 쓰는 데 너무 오래 걸려서 그래프 부분을 놓쳤어요.)
문제는 데이터 저장 장소인 것 같아요.이것과 비교해 주시겠습니까?
- test one :500 Mb RAM의 2개의 연속된 블록을 할당하고 한쪽에서 다른 쪽으로 복사합니다(이미 실행한 작업).
- test two : 500Mb 메모리의 20(또는 그 이상) 블록과 복사가 처음에서 마지막까지 이루어지기 때문에 서로 멀리 떨어져 있습니다(실제 위치를 확신할 수 없는 경우라도).
이렇게 하면 메모리 컨트롤러가 서로 멀리 떨어져 있는 메모리 블록을 처리하는 방법을 알 수 있습니다.데이터는 다른 메모리 존에 배치되어 있어 데이터 패스의 어느 시점에서 다른 존과 통신하기 위해서는 스위칭 조작이 필요하다고 생각합니다(양면 메모리에는 이러한 문제가 있습니다).
또, 스레드가 1개의 CPU에 바인드 되어 있는 것을 확인하시겠습니까?
편집 2:
메모리에는 몇 가지 종류의 "존" 딜리미터가 있습니다.NUMA도 있지만 이뿐만이 아닙니다.예를 들어, 양면 스틱은 한쪽 또는 다른 한쪽을 가리키는 깃발을 필요로 합니다.노트북(NUMA가 없는 경우)에서도 대용량 메모리 청크로 인해 성능이 저하되는 경우를 그래프에서 확인하십시오.확실하지 않지만, memcpy는 하드웨어 기능을 사용하여 RAM(DMA의 일종)을 복사할 수 있으며, 이 칩은 CPU보다 캐시가 적어야 합니다.이것은 CPU를 사용한 덤 복사가 memcpy보다 빠른 이유를 설명할 수 있습니다.
SandyBridge 기반 서버에 비해 IvyBridge 기반 노트북의 CPU 성능 향상에 기여하고 있을 가능성이 있습니다.
페이지 크로스 프리페치 - 노트북의 CPU는 현재 페이지의 끝에 도달할 때마다 다음 리니어 페이지의 프리페치를 실시하기 때문에 매번 TLB를 놓치는 일이 없어집니다.이를 완화하기 위해 2M/1G 페이지의 서버 코드를 구축해 보십시오.
캐시 치환 방식도 개선된 것으로 보입니다(여기서 흥미로운 리버스 엔지니어링 참조).실제로 이 CPU가 동적 삽입 정책을 사용하면 복사된 데이터가 (크기로 인해 효과적으로 사용할 수 없는) 마지막 수준 캐시를 스래시하는 것을 쉽게 방지할 수 있으며 코드, 스택, 페이지 테이블 데이터 등 기타 유용한 캐싱을 위한 공간을 절약할 수 있습니다.이를 테스트하려면 스트리밍 로드/스토어를 사용하여 간단한 구현을 재구축해 보십시오.
movntdq
빌트인을도 있습니다.)gcc 빌트인()이 가능성은 대규모 데이터 세트사이즈가 갑자기 감소하는 것을 설명할 수 있습니다.스트링 카피(여기도)도 개선되었다고 생각합니다만, 조립 코드의 모양에 따라서는 여기에 적용될 수도 있고 아닐 수도 있습니다.Drystone과의 벤치마킹을 통해 본질적인 차이가 있는지 테스트할 수 있습니다.이는 memcpy와 memmove의 차이도 설명할 수 있습니다.
IvyBridge 기반 서버 또는 Sandy-Bridge 노트북을 입수할 수 있다면 이 모든 것을 함께 테스트하는 것이 가장 간단합니다.
위의 질문에 이미 답했습니다.어느 경우든 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;
}
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바이트를 한번에 처리하도록 시도했습니다.이러한 인텔 프로세서에서는, 큰 차이는 없습니다.캐시는 모든 바이트 작업을 최소 메모리 작업 수로 병합합니다.나는 gcc 라이브러리 코드가 너무 영리하려고 하는 것 같아.
서버 1의 사양
- CPU: 인텔 Xeon E5-2680 (2.70 GHz)x 2
서버 2의 사양
- CPU: 인텔 Xeon E5-2650 v2 (2.6 Ghz)x 2
인텔 ARK에 따르면 E5-2650과 E5-2680은 모두 AVX 확장 기능을 갖추고 있습니다.
빌드할 CMake 파일
★★★★★★★★★★★★★★★★★★★★★★★★★★★CMake는 다소 빈약한 플래그를 선택합니다. , 「알겠습니다」를 실행해 .make VERBOSE=1
.
다 요.-march=native
★★★★★★★★★★★★★★★★★」-O3
your CFLAGS
★★★★★★★★★★★★★★★★★」CXXFLAGS
퍼포먼스가 비약적으로 향상될 가능성이 있습니다.AVX를 사용하다-march=XXX
최소 i686 또는 x86_64 머신을 사용할 수 있습니다.-O3
GCC를 사용하다
GCC 4.6이 AVX(및 BMI와 같은 친구)에 대응하고 있는지는 잘 모르겠습니다.GCC가 MMX 유닛에 memcpy 및 memset을 아웃소싱할 때 세그먼트 장애를 일으키는 얼라인먼트 버그를 찾아내야 했기 때문에 GCC 4.8 또는 4.9가 가능하다는 것을 알고 있습니다.AVX 및 AVX2를 사용하면 CPU는 한 번에 16바이트 및 32바이트의 데이터 블록으로 동작할 수 있습니다.
GCC가 정렬된 데이터를 MMX 장치로 전송할 기회를 놓치면 데이터가 정렬되어 있다는 사실을 놓칠 수 있습니다.데이터가 16바이트로 정렬된 경우 GCC에 전달하여 Fat Block에서 작동하도록 할 수 있습니다.이에 대해서는 GCC를 참조하십시오.또한 포인터 인수는 항상 이중으로 정렬되어 있음을 GCC에 알리는 방법 등의 질문을 참조하십시오.
것요.왜냐하면 '이것'은 '이것'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
는 GCC에 대해 몇 가지 을 할 수new
수 것 같지 않다malloc
GCC의 빌트인 옵션 페이지에 몇 가지 전제조건이 상세하게 기재되어 있습니다.
을 하다(잠재적인 GCC의 경우), 할 수 해야 합니다.void*
★★★★★★★★★★★★★★★★★」malloc
★★★★★★★★★★★★★★★★★★」
은 CPU를 사용할 때 CPU .-march=native
예를 들어 Ubuntu Issue 1616723, Clang 3.4는 SSE2, Ubuntu Issue 1616723, Clang 3.5는 SSE2, Ubuntu Issue 1616723, Clang 3.6은 SSE2만 애드버타이즈 합니다.
언급URL : https://stackoverflow.com/questions/22793669/poor-memcpy-performance-on-linux
'sourcecode' 카테고리의 다른 글
컴포넌트로 랩된html을 캡처하여 vue.js의 데이터 값을 설정합니다. (0) | 2022.09.03 |
---|---|
"MVC"의 "컨트롤러"에 들어가는 내용은 무엇입니까? (0) | 2022.08.31 |
Java: 0 <= x < n 범위의 난수 (0) | 2022.08.31 |
계산된 구성 요소와 vuejs 동적 가져오기의 구성 요소를 가져올 때의 차이점은 무엇입니까? (0) | 2022.08.31 |
문자열 형식으로 주어진 수학 식을 어떻게 평가합니까? (0) | 2022.08.31 |