IT TIP

Linux에서 memcpy 성능 저하

itqueen 2020. 11. 5. 20:02
반응형

Linux에서 memcpy 성능 저하


최근에 일부 새 서버를 구입했으며 memcpy 성능이 저하되고 있습니다. memcpy 성능은 노트북에 비해 서버에서 3 배 느립니다.

서버 사양

  • 섀시 및 Mobo : SUPER MICRO 1027GR-TRF
  • CPU : 2x Intel Xeon E5-2680 @ 2.70Ghz
  • 메모리 : 8x 16GB DDR3 1600MHz

편집 : 나는 또한 약간 더 높은 사양으로 다른 서버에서 테스트하고 위의 서버와 동일한 결과를 봅니다.

서버 2 사양

  • 섀시 및 Mobo : SUPER MICRO 10227GR-TRFT
  • CPU : 2x Intel Xeon E5-2650 v2 @ 2.6Ghz
  • 메모리 : 8x 16GB DDR3 1866MHz

노트북 사양

  • 섀시 : Lenovo W530
  • CPU : 1x Intel Core i7 i7-3720QM @ 2.6Ghz
  • 메모리 : 4x 4GB DDR3 1600MHz

운영 체제

$ 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 버퍼와 memcpy를 생성하여 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 

결과 : 정확히 동일한 성능 (개선 없음)

-O3 대신 -O2로 컴파일

g++ -std=c++0x -Wall -march=native -mtune=native -O2 -DNDEBUG -o big_memcpy_test main.cpp

결과 : 정확히 동일한 성능 (개선 없음)

편집 : NULL 페이지 (@SteveCox)를 피하기 위해 0 대신 0xF를 작성하도록 memset을 변경했습니다.

0 이외의 값으로 memsetting하면 개선되지 않습니다 (이 경우 0xF 사용).

편집 : Cachebench 결과

내 테스트 프로그램이 너무 단순하다는 것을 배제하기 위해 실제 벤치마킹 프로그램 LLCacheBench ( http://icl.cs.utk.edu/projects/llcbench/cachebench.html )를 다운로드했습니다.

아키텍처 문제를 피하기 위해 각 컴퓨터에서 벤치 마크를 별도로 구축했습니다. 아래는 내 결과입니다.

노트북 대 서버 memcpy 성능

매우 큰 차이는 더 큰 버퍼 크기에서의 성능입니다. 테스트 된 마지막 크기 (16777216)는 랩톱에서 18849.29 MB / sec, 서버에서 6710.40으로 수행되었습니다. 이는 약 3 배의 성능 차이입니다. 또한 서버의 성능 저하가 랩톱보다 훨씬 더 가파르다는 것을 알 수 있습니다.

편집 : memmove ()는 서버에서 memcpy ()보다 2 배 빠릅니다.

몇 가지 실험을 기반으로 테스트 케이스에서 memcpy () 대신 memmove () 사용을 시도했으며 서버에서 2 배 개선 된 것으로 나타났습니다. 랩톱의 Memmove ()는 memcpy ()보다 느리게 실행되지만 이상하게도 서버의 memmove ()와 동일한 속도로 실행됩니다. 이것은 memcpy가 왜 그렇게 느린가요?

memcpy와 함께 memmove를 테스트하도록 코드를 업데이트했습니다. 나는 memmove ()를 함수 안에 감쌌다. 왜냐하면 내가 그것을 인라인 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

편집 : Naive Memcpy

@Salgar의 제안에 따라 내 순진한 memcpy 기능을 구현하고 테스트했습니다.

Naive 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 ()와 비교 한 Naive 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로 실행 해 보았습니다 . 내 결과는 처음에는 좋지 않았지만 SetMemcpyCacheLimit ()를 1GB (내 버퍼 크기)로 변경 한 후 순진한 for 루프와 동등한 속도로 실행되었습니다!

나쁜 소식은 memmove의 asmlib 버전이 glibc 버전보다 느리다는 것입니다. 이제 300ms 마크에서 실행되고 있습니다 (memcpy의 glibc 버전과 동등 함). 이상한 점은 노트북에서 내가 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

캐시 문제에 의지하기 시작했지만 그 원인은 무엇입니까?


[댓글을 달고 싶지만 그렇게 할만한 평판이 충분하지 않습니다.]

비슷한 시스템이 있고 비슷한 결과가 표시되지만 몇 가지 데이터 포인트를 추가 할 수 있습니다.

  • 순진한 방향을 바꾸면 memcpy(예 :으로 변환 *p_dest-- = *p_src--), 순방향 방향보다 훨씬 더 나쁜 성능을 얻을 수 있습니다 (저는 ~ 637ms). memcpy()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 프리 페처, MLC Spatial 프리 페처, DCU Streamer 프리 페처 및 DCU IP 프리 페처). 나는 각각을 비활성화하려고 시도했지만 최선을 다해 성능 패리티를 유지하고 몇 가지 설정에 대해 성능을 저하 시켰습니다.
  • 실행 평균 전력 제한 (RAPL) DRAM 모드를 비활성화해도 아무런 영향이 없습니다.
  • Fedora 19 (glibc 2.17)를 실행하는 다른 Supermicro 시스템에 액세스 할 수 있습니다. Supermicro X9DRG-HF 보드, Fedora 19 및 Xeon E5-2670 CPU를 사용하면 위와 유사한 성능을 볼 수 있습니다. Xeon E3-1275 v3 (Haswell) 및 Fedora 19를 실행하는 Supermicro X10SLM-F 단일 소켓 보드에서 memcpy(104ms)에 대해 9.6GB / s가 표시됩니다. Haswell 시스템의 RAM은 DDR3-1600 (다른 시스템과 동일)입니다.

업데이트

  • CPU 전원 관리를 최대 성능으로 설정하고 BIOS에서 하이퍼 스레딩을 비활성화했습니다. 를 기반으로 /proc/cpuinfo코어는 3GHz로 클럭되었습니다. 그러나 이로 인해 메모리 성능이 약 10 % 감소했습니다.
  • memtest86 + 4.10은 9091 MB / s의 메인 메모리에 대역폭을보고합니다. 이것이 읽기, 쓰기 또는 복사에 해당하는지 찾을 수 없습니다.
  • STREAM 벤치 마크는 / 13,422메가바이트를보고 사본이야,하지만 그들은 읽기와 쓰기 모두 같은 바이트를 계산하므로 ~ 6.5 GB에 해당하는이 / 우리가 위의 결과를 비교하려는 s의 경우 것이다.

이것은 나에게 정상적인 것 같습니다.

2 개의 CPU로 8x16GB ECC 메모리 스틱을 관리하는 것은 2x2GB의 단일 CPU보다 훨씬 어려운 작업입니다. 16GB 스틱은 양면 메모리 + 버퍼 + ECC (마더 보드 수준에서 비활성화 됨)가있을 수 있습니다.이 모든 것이 RAM에 대한 데이터 경로를 훨씬 더 길게 만듭니다. 또한 램을 공유하는 2 개의 CPU가 있으며 다른 CPU에서 아무 작업도하지 않더라도 항상 메모리 액세스가 거의 없습니다. 이 데이터를 전환하려면 약간의 추가 시간이 필요합니다. 그래픽 카드와 일부 램을 공유하는 PC에서 손실 된 엄청난 성능을 살펴보십시오.

여전히 서버는 정말 강력한 데이터 펌프입니다. 실제 소프트웨어에서 1GB 복제가 매우 자주 발생하는지는 모르겠지만 128GB가 하드 드라이브, 심지어 최고의 SSD보다 훨씬 빠르며 서버를 활용할 수있는 곳이라고 확신합니다. 3GB로 동일한 테스트를 수행하면 노트북에 불이 붙습니다.

이것은 상용 하드웨어를 기반으로하는 아키텍처가 대형 서버보다 훨씬 더 효율적일 수있는 완벽한 예처럼 보입니다. 이러한 대형 서버에 지출 한 비용으로 얼마나 많은 소비자 PC를 감당할 수 있습니까?

매우 상세한 질문에 감사드립니다.

편집 : (그래프 부분을 놓친이 답변을 작성하는 데 너무 오래 걸렸습니다.)

문제는 데이터가 저장되는 위치에 있다고 생각합니다. 이것을 비교할 수 있습니까?

  • 테스트 하나 : 500Mb 램의 두 개의 연속 블록을 할당하고 하나에서 다른 블록으로 복사합니다 (이미 수행 한 작업).
  • 테스트 2 : 500Mb 메모리의 20 개 (또는 그 이상) 블록을 할당하고 첫 번째부터 마지막 ​​블록까지 복사하여 서로 멀리 떨어져 있도록합니다 (실제 위치를 확신 할 수 없더라도).

이렇게하면 메모리 컨트롤러가 서로 멀리 떨어진 메모리 블록을 처리하는 방법을 알 수 있습니다. 데이터가 다른 메모리 영역에 저장되고 데이터 경로의 특정 지점에서 한 영역과 다른 영역과 통신하려면 전환 작업이 필요하다고 생각합니다 (양면 메모리에 이러한 문제가 있습니다).

또한 스레드가 하나의 CPU에 바인딩되어 있는지 확인하고 있습니까?

편집 2 :

메모리에는 여러 종류의 "영역"구분 기호가 있습니다. NUMA는 하나이지만 이것이 유일한 것은 아닙니다. 예를 들어 양면 스틱은 한쪽 또는 다른 쪽을 주소 지정하는 플래그가 필요합니다. 랩톱 (NUMA가없는 경우)에서도 대용량 메모리로 인해 성능이 어떻게 저하되는지 그래프를보십시오. 확실하지 않지만 memcpy는 하드웨어 기능을 사용하여 ram (DMA의 일종)을 복사 할 수 있으며이 칩은 CPU보다 캐시가 적어야합니다. 이것은 CPU를 사용한 바보 복사가 memcpy보다 빠른 이유를 설명 할 수 있습니다.


IvyBridge 기반 노트북의 일부 CPU 개선 사항이 SandyBridge 기반 서버에 비해 이러한 이점에 기여할 수 있습니다.

  1. 페이지 교차 프리 페치 -노트북 CPU는 현재 페이지의 끝에 도달 할 때마다 다음 선형 페이지를 미리 프리 페치하여 매번 불쾌한 TLB 누락을 방지합니다. 이를 완화하려면 2M / 1G 페이지에 대한 서버 코드를 작성하십시오.

  2. 캐시 교체 방식도 개선 된 것 같습니다 ( 여기 에서 흥미로운 리버스 엔지니어링 참조 ). 실제로이 CPU가 동적 삽입 정책을 사용하는 경우 복사 된 데이터가 Last-Level-Cache (크기 때문에 효과적으로 사용할 수 없음)를 쓰러 뜨리는 것을 쉽게 방지하고 다른 유용한 캐싱을위한 공간을 절약합니다. 코드, 스택, 페이지 테이블 데이터 등). 이를 테스트하려면 스트리밍로드 / 스토어를 사용하여 순진한 구현을 다시 빌드 해 볼 수 있습니다 ( movntdq또는 이와 유사한 것,이를 위해 gcc 내장을 사용할 수도 있음). 이 가능성은 큰 데이터 세트 크기의 급격한 감소를 설명 할 수 있습니다.

  3. 문자열 복사로도 일부 개선이 이루어 졌다고 생각합니다 ( 여기 ), 어셈블리 코드가 어떻게 보이는지에 따라 여기에 적용되거나 적용되지 않을 수 있습니다. Dhrystone 으로 벤치마킹 하여 고유 한 차이가 있는지 테스트 할 수 있습니다. 이것은 또한 memcpy와 memmove의 차이점을 설명 할 수 있습니다.

IvyBridge 기반 서버 또는 Sandy-Bridge 노트북을 확보 할 수 있다면이 모든 것을 함께 테스트하는 것이 가장 간단 할 것입니다.


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 라이브러리 코드가 너무 영리하다고 생각합니다.


질문은 이미 위에서 답변 되었지만 어쨌든 걱정되는 경우 큰 복사본에 대해 더 빠른 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;
}

숫자는 나에게 의미가 있습니다. 여기에는 실제로 두 가지 질문이 있으며 두 가지 모두에 답할 것입니다.

하지만 먼저 최신 인텔 프로세서와 같은 것에서 1 메모리 전송 이 얼마나 큰지에 대한 정신적 모델이 필요합니다 . 이 설명은 대략적인 것이며 세부 사항은 아키텍처에서 아키텍처로 다소 변경 될 수 있지만 높은 수준의 아이디어는 매우 일정합니다.

  1. L1데이터 캐시 에서로드가 누락되면 채워질 때까지 누락 요청을 추적 하는 라인 버퍼 가 할당됩니다. L2캐시에 적중하면 짧은 시간 (12 회 정도) , DRAM에 도달하지 않으면 훨씬 더 길다 (100 나노초 이상).
  2. 코어 1 이러한 라인 버퍼의 수는 제한되어 있으며, 일단 가득 차면 추가 미스가 1 개를 기다리며 지연됩니다.
  3. 수요 3 로드 / 스토어에 사용되는 이러한 채우기 버퍼 외에 DRAM과 L2 간의 메모리 이동을위한 추가 버퍼와 ​​프리 페치에 사용되는 하위 수준 캐시가 있습니다.
  4. 메모리 하위 시스템 자체에는 최대 대역폭 제한 이 있으며 ARK에 편리하게 나열되어 있습니다. 예를 들어 Lenovo 노트북의 3720QM은 25.6GB로 제한 됩니다 . 이 제한은 기본적으로 유효 주파수 ( 1600 Mhz) x 전송 당 8 바이트 (64 비트) x 채널 수 (2)의 곱 1600 * 8 * 2 = 25.6 GB/s입니다. 손에 들고있는 서버 칩의 최대 대역폭은 소켓 당 51.2GB / s 이며 총 시스템 대역폭은 ~ 102GB / s입니다.

    다른 프로세서 기능과는 달리, 여러 칩에서, 심지어 아키텍처에서도 동일한 값에 의존하기 때문에 다양한 칩에서 가능한 이론적 대역폭 수치 만있는 경우가 많습니다. DRAM이 이론적 인 속도로 정확하게 전달 될 것으로 기대하는 것은 비현실적 이지만 ( 여기 에서 약간 논의한 다양한 저수준 문제로 인해 ) 종종 약 90 % 이상을 얻을 수 있습니다.

따라서 (1)의 주요 결과는 RAM 누락을 일종의 요청 응답 시스템으로 처리 할 수 ​​있다는 것입니다. DRAM이 누락되면 채우기 버퍼가 할당 되고 요청이 돌아 오면 버퍼가 해제됩니다. 수요 미스에 대해 CPU 당 이러한 버퍼 중 10 개만 있으며, 이는 단일 CPU가 대기 시간의 함수로 생성 할 수있는 수요 메모리 대역폭에 엄격한 제한 을 둡니다 .

예를 들어 E5-2680DRAM에 대한 지연 시간이 80ns라고 가정 해 보겠습니다. 모든 요청은 64 바이트 캐시 라인을 가져 오므로 요청을 DRAM에 직렬로 실행하면 paltry의 처리량을 기대할 64 bytes / 80 ns = 0.8 GB/s수 있으며 memcpy읽기가 필요하므로 수치 를 얻기 위해 다시 절반으로 줄였습니다 (적어도) 그리고 쓰십시오. 다행히도 10 개의 라인 채우기 버퍼를 사용할 수 있으므로 메모리에 대한 동시 요청 10 개를 겹치고 대역폭을 10 배 증가시켜 이론적 대역폭 8GB / s로 이어질 수 있습니다.

더 자세한 내용을 알고 싶다면 이 실은 거의 순금입니다. John McCalpin의 사실과 수치 , 일명 "Dr Bandwidth 는 아래에서 공통된 주제가 될 것입니다.

이제 세부 사항을 살펴보고 두 가지 질문에 답해 보겠습니다.

memcpy가 서버의 memmove 또는 수동 복사보다 훨씬 느린 이유는 무엇입니까?

랩톱 시스템은 memcpy120ms에 벤치 마크를 수행하는 반면 서버 부품은 약 300ms가 걸립니다 . 또한 랩톱 성능에 훨씬 더 가깝지만 (여전히 더 느린) 160ms 의 시간을 달성하기 위해 memmove손으로 굴린 memcpy (이하 hrm)를 사용할 수 있었기 때문에이 속도가 대부분 기본적이지 않다는 것을 보여주었습니다 .

위에서 이미 단일 코어의 경우 대역폭이 DRAM 대역폭이 아닌 사용 가능한 총 동시성 및 지연 시간에 의해 제한된다는 것을 보여주었습니다. 서버 부분의 지연 시간은 길지만 더 길지는 않을 것으로 예상됩니다 300 / 120 = 2.5x!

답은 스트리밍 (일명 비 시간적) 저장소에 있습니다. 사용중인 libc 버전은 memcpy이를 사용하지만 사용 memmove하지 않습니다. 당신은 memcpy또한 그들을 사용하지 않는 "순진한" 뿐만 아니라 asmlib스트리밍 저장소 (느리게)를 사용하고 (빠른) 사용하지 않도록 구성 하는 것으로 확인했습니다.

스트리밍 저장소는 다음과 같은 이유로 단일 CPU 번호를 손상시킵니다 .

  • (A) 프리 페치가 저장 될 라인을 캐시로 가져 오는 것을 방지하여 프리 페치 하드웨어에 로드 / 스토어 사용을 요구 하는 10 개의 채우기 버퍼초과하는 다른 전용 버퍼가 있기 때문에 더 많은 동시성을 허용 합니다.
  • (B) E5-2680은 스트리밍 스토어에서 특히 느린 것으로 알려져 있습니다.

두 문제 모두 위의 링크 된 스레드에서 John McCalpin의 인용문으로 더 잘 설명됩니다. 프리 페치 효과 및 스트리밍 스토어에 대해 그는 다음과 같이 말합니다 .

"일반"저장소를 사용하는 L2 하드웨어 프리 페처는 미리 라인을 가져오고 라인 채우기 버퍼가 사용되는 시간을 줄여 지속적인 대역폭을 증가시킬 수 있습니다. 반면에 스트리밍 (캐시 우회) 저장소를 사용하면 저장소에 대한 라인 채우기 버퍼 항목이 DRAM 컨트롤러에 데이터를 전달하는 데 필요한 전체 시간 동안 점유됩니다. 이 경우 하드웨어 프리 페치로 로드 를 가속화 할 수 있지만 스토어는 그렇지 않으므로 속도가 약간 향상되지만로드와 스토어가 모두 가속화 될 경우 얻을 수있는 것만 큼은 아닙니다.

... 그리고 E5의 스트리밍 스토어에 대해 분명히 훨씬 더 긴 지연 시간에 대해 그는 다음과 같이 말합니다 .

Xeon E3의 더 단순한 "uncore"는 스트리밍 스토어의 라인 채우기 버퍼 점유율을 크게 낮출 수 있습니다. Xeon E5는 스트리밍 저장소를 코어 버퍼에서 메모리 컨트롤러로 전달하기 위해 탐색 할 훨씬 복잡한 링 구조를 가지고 있으므로 점유는 메모리 (읽기) 지연 시간보다 더 큰 요인으로 다를 수 있습니다.

특히 McCalpin 박사는 "클라이언트"언 코어가있는 칩에 비해 E5에서 ~ 1.8 배의 속도 저하를 측정했지만 OP가보고 한 2.5 배의 속도 저하는 STREAM TRIAD에 대해 1.8 배의 점수가보고 되었기 때문에 이와 일치합니다. 부하 : 점포의 2 : 1 비율 인 반면 memcpy, 1 : 1은 점포가 문제가되는 부분입니다.

This doesn't make streaming a bad thing - in effect, you are trading off latency for smaller total bandwidth consumption. You get less bandwidth because you are concurrency limited when using a single core, but you avoid all the read-for-ownership traffic, so you would likely see a (small) benefit if you ran the test simultaneously on all cores.

So far from being an artifact of your software or hardware configuration, the exact same slowdowns have been reported by other users, with the same CPU.

Why is the server part still slower when using ordinary stores?

Even after correcting the non-temporal store issue, you are still seeing roughly a 160 / 120 = ~1.33x slowdown on the server parts. What gives?

글쎄요, 서버 CPU가 모든 측면에서 더 빠르거나 적어도 클라이언트와 동일하다는 것은 일반적인 오류입니다. 사실이 아닙니다. 서버 부품에 대해 지불하는 비용 (종종 칩당 2,000 달러 정도)은 대부분 (a) 더 많은 코어 (b) 더 많은 메모리 채널 (c) 더 많은 총 RAM 지원 (d) " ECC, 가상화 기능 등과 같은 엔터프라이즈 급 "기능 5 .

실제로 대기 시간 측면에서 서버 부분은 일반적으로 클라이언트 4 부분 과 동일하거나 느립니다 . 메모리 대기 시간에 관해서는 다음과 같은 이유로 특히 그렇습니다.

  • 서버 부품은 더 많은 코어를 지원해야하는 경우가 많고 결과적으로 RAM 경로가 더 긴 확장 성이 뛰어나지 만 복잡한 "uncore"를 가지고 있습니다.
  • The server parts support more RAM (100s of GB or a few TB) which often requires electrical buffers to support such a large quantity.
  • As in the OP's case server parts are usually multi-socket, which adds cross-socket coherence concerns to the memory path.

So it is typical that server parts have a latency 40% to 60% longer than client parts. For the E5 you'll probably find that ~80 ns is a typical latency to RAM, while client parts are closer to 50 ns.

So anything that is RAM latency constrained will run slower on server parts, and as it turns out, memcpy on a single core is latency constrained. that's confusing because memcpy seems like a bandwidth measurement, right? Well as described above, a single core doesn't have enough resources to keep enough requests to RAM in flight at a time to get close to the RAM bandwidth6, so performance depends directly on latency.

The client chips, on the other hand, have both lower latency and lower bandwidth, so one core comes much closer to saturating the bandwidth (this is often why streaming stores are a big win on client parts - when even a single core can approach the RAM bandwidth, the 50% store bandwidth reduction that stream stores offers helps a lot.

References

There are lots of good sources to read more on this stuff, here are a couple.


1 By large I just mean somewhat larger than the LLC. For copies that fit in the LLC (or any higher cache level) the behavior is very different. The OPs llcachebench graph shows that in fact the performance deviation only starts when the buffers start to exceed the LLC size.

2 In particular, the number of line fill buffers has apparently been constant at 10 for several generations, including the architectures mentioned in this question.

3 When we say demand here, we mean that it is associated with an explicit load/store in the code, rather than say being brought in by a prefetch.

4 When I refer to a server part here, I mean a CPU with a server uncore. This largely means the E5 series, as the E3 series generally uses the client uncore.

5 In the future, it looks like you can add "instruction set extensions" to this list, as it seems that AVX-512 will appear only on the Skylake server parts.

6 Per little's law at a latency of 80 ns, we'd need (51.2 B/ns * 80 ns) == 4096 bytes or 64 cache lines in flight at all times to reach the maximum bandwidth, but one core provides less than 20.


Server 1 Specs

  • CPU: 2x Intel Xeon E5-2680 @ 2.70 Ghz

Server 2 Specs

  • CPU: 2x Intel Xeon E5-2650 v2 @ 2.6 Ghz

According to Intel ARK, both the E5-2650 and E5-2680 have AVX extension.

CMake File to Build

This is part of your problem. CMake chooses some rather poor flags for you. You can confirm it by running make VERBOSE=1.

You should add both -march=native and -O3 to your CFLAGS and CXXFLAGS. You will likely see a dramatic performance increase. It should engage the AVX extensions. Without -march=XXX, you effectively get a minimal i686 or x86_64 machine. Without -O3, you don't engage GCC's vectorizations.

I'm not sure if GCC 4.6 is capable of AVX (and friends, like BMI). I know GCC 4.8 or 4.9 is capable because I had to hunt down an alignment bug that was causing a segfault when GCC was outsourcing memcpy's and memset's to the MMX unit. AVX and AVX2 allow the CPU to operate on 16-byte and 32-byte blocks of data at a time.

If GCC is missing an opportunity to send aligned data to the MMX unit, it may be missing the fact that data is aligned. If your data is 16-byte aligned, then you might try telling GCC so it knows to operate on fat blocks. For that, see GCC's __builtin_assume_aligned. Also see questions like How to tell GCC that a pointer argument is always double-word-aligned?

This also looks a little suspect because of the void*. Its kind of throwing away information about the pointer. You should probably keep the information:

void doMemmove(void* pDest, const void* pSource, std::size_t sizeBytes)
{
  memmove(pDest, pSource, sizeBytes);
}

Maybe something like the following:

template <typename T>
void doMemmove(T* pDest, const T* pSource, std::size_t count)
{
  memmove(pDest, pSource, count*sizeof(T));
}

Another suggestion is to use new, and stop using malloc. Its a C++ program and GCC can make some assumptions about new that it cannot make about malloc. I believe some of the assumptions are detailed in GCC's option page for the built-ins.

또 다른 제안은 힙을 사용하는 것입니다. 일반적인 최신 시스템에서는 항상 16 바이트로 정렬됩니다. GCC는 힙의 포인터가 관련 될 때 MMX 장치로 오프로드 할 수 있음을 인식해야합니다 (잠재적 문제 void*malloc문제가 없음).

마지막으로 Clang은 .NET Core를 사용할 때 기본 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

반응형