memmove가 memcpy보다 빠른 이유는 무엇입니까?


89

memmove (3)에서 시간의 50 %를 소비하는 응용 프로그램의 성능 핫스팟을 조사하고 있습니다. 응용 프로그램은 수백만 개의 4 바이트 정수를 정렬 된 배열에 삽입하고 삽입 된 값을위한 공간을 만들기 위해 memmove를 사용하여 데이터를 "오른쪽으로"이동합니다.

나의 기대는 메모리 복사가 매우 빠르다는 것이었고, memmove에 너무 많은 시간을 소비하는 것에 놀랐습니다. 그러나 memmove가 겹치는 영역을 이동하기 때문에 느리다는 생각이 들었습니다. 이는 큰 메모리 페이지를 복사하는 대신 긴밀한 루프로 구현해야합니다. memcpy와 memmove 사이에 성능 차이가 있는지 알아보기 위해 작은 마이크로 벤치 마크를 작성했습니다. memcpy가 이길 것으로 예상했습니다.

두 대의 컴퓨터 (코어 i5, 코어 i7)에서 벤치 마크를 실행 한 결과 memmove가 실제로 memcpy보다 빠르다는 것을 알았습니다. 구형 코어 i7에서는 거의 두 배나 빠릅니다! 이제 설명을 찾고 있습니다.

여기 내 벤치 마크가 있습니다. memcpy로 100mb를 복사 한 다음 memmove로 약 100mb를 이동합니다. 소스와 대상이 겹칩니다. 소스 및 대상에 대한 다양한 "거리"가 시도됩니다. 각 테스트는 10 회 실행되며 평균 시간이 인쇄됩니다.

https://gist.github.com/cruppstahl/78a57cdf937bca3d062c

다음은 Core i5의 결과입니다 (Linux 3.5.0-54-generic # 81 ~ precise1-Ubuntu SMP x86_64 GNU / Linux, gcc는 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5)). 괄호 안의 숫자는 다음과 같습니다. 소스와 목적지 사이의 거리 (간격 크기) :

memcpy        0.0140074
memmove (002) 0.0106168
memmove (004) 0.01065
memmove (008) 0.0107917
memmove (016) 0.0107319
memmove (032) 0.0106724
memmove (064) 0.0106821
memmove (128) 0.0110633

Memmove는 SSE 최적화 된 어셈블러 코드로 구현되어 뒤에서 앞으로 복사됩니다. 하드웨어 프리 페치를 사용하여 데이터를 캐시에로드하고 128 바이트를 XMM 레지스터에 복사 한 다음 대상에 저장합니다.

( memcpy-ssse3-back.S , 라인 1650 ff)

L(gobble_ll_loop):
    prefetchnta -0x1c0(%rsi)
    prefetchnta -0x280(%rsi)
    prefetchnta -0x1c0(%rdi)
    prefetchnta -0x280(%rdi)
    sub $0x80, %rdx
    movdqu  -0x10(%rsi), %xmm1
    movdqu  -0x20(%rsi), %xmm2
    movdqu  -0x30(%rsi), %xmm3
    movdqu  -0x40(%rsi), %xmm4
    movdqu  -0x50(%rsi), %xmm5
    movdqu  -0x60(%rsi), %xmm6
    movdqu  -0x70(%rsi), %xmm7
    movdqu  -0x80(%rsi), %xmm8
    movdqa  %xmm1, -0x10(%rdi)
    movdqa  %xmm2, -0x20(%rdi)
    movdqa  %xmm3, -0x30(%rdi)
    movdqa  %xmm4, -0x40(%rdi)
    movdqa  %xmm5, -0x50(%rdi)
    movdqa  %xmm6, -0x60(%rdi)
    movdqa  %xmm7, -0x70(%rdi)
    movdqa  %xmm8, -0x80(%rdi)
    lea -0x80(%rsi), %rsi
    lea -0x80(%rdi), %rdi
    jae L(gobble_ll_loop)

memmove가 memcpy보다 빠른 이유는 무엇입니까? memcpy가 메모리 페이지를 복사 할 것으로 예상하는데, 이는 루핑보다 훨씬 빠릅니다. 최악의 경우 memcpy가 memmove만큼 빠르기를 기대합니다.

추신 : 내 코드에서 memmove를 memcpy로 바꿀 수 없다는 것을 알고 있습니다. 코드 샘플이 C와 C ++를 혼합한다는 것을 알고 있습니다. 이 질문은 정말 학문적 목적을위한 것입니다.

업데이트 1

다양한 답변을 바탕으로 다양한 테스트를 실행했습니다.

  1. memcpy를 두 번 실행하면 두 번째 실행이 첫 번째 실행보다 빠릅니다.
  2. memcpy ( memset(b2, 0, BUFFERSIZE...)) 의 대상 버퍼를 "접촉"하면 memcpy 의 첫 번째 실행도 더 빠릅니다.
  3. memcpy는 여전히 memmove보다 약간 느립니다.

결과는 다음과 같습니다.

memcpy        0.0118526
memcpy        0.0119105
memmove (002) 0.0108151
memmove (004) 0.0107122
memmove (008) 0.0107262
memmove (016) 0.0108555
memmove (032) 0.0107171
memmove (064) 0.0106437
memmove (128) 0.0106648

내 결론 : @Oliver Charlesworth의 의견에 따르면 운영 체제는 memcpy 대상 버퍼에 처음으로 액세스하자마자 물리적 메모리를 커밋해야합니다 (누군가가 이것을 "증명"하는 방법을 알고 있다면 답을 추가하십시오! ). 또한 @Mats Petersson이 말했듯이 memmove는 memcpy보다 캐시 친화적입니다.

모든 훌륭한 답변과 의견에 감사드립니다!


1
memmove 코드도 보셨나요? memcpy 코드도 보셨나요?
Oliver Charlesworth 2015

8
내 기대는 메모리 복사가 매우 빠르다 는 것입니다. 메모리가 L1 캐시에있을 때만 가능합니다. 데이터가 캐시에 맞지 않으면 복사 성능이 저하됩니다.
Maxim Egorushkin 2015

1
BTW, 당신은 memmove. 이 분기는 소스가 대상과 겹치고 대상이 하위 주소에있을 때 이동을 처리 할 수 ​​없습니다.
Maxim Egorushkin 2015

2
리눅스 머신에 접근 할 시간이 없었기 때문에 아직이 이론을 테스트 할 수 없습니다. 그러나 또 다른 가능한 설명은 overcommitting입니다 . 당신의 memcpy루프의 내용이 처음이다 b2액세스 할이, 따라서 OS가 간다로 물리적 메모리를 커밋한다.
올리버 찰스 워드

2
추신 : 이것이 병목 현상이라면 접근 방식을 재고 할 것입니다. 값을 목록이나 트리 구조 (예 : 이진 트리)에 넣은 다음 마지막에 배열로 읽는 것은 어떻습니까? 이러한 접근 방식의 노드는 풀 할당을위한 훌륭한 후보가 될 것입니다. 한꺼번에 해제 될 때까지만 추가됩니다. 처음에 얼마나 많이 필요할지 안다면 특히 그렇습니다. 부스트 라이브러리에는 풀 할당자가 있습니다.
Persixty

답변:


57

당신의 memmove당신의 동안 전화 2 128 바이트로를 따라 메모리를 걸어 갔다하는 memcpy소스와 대상이 완전히 다르다. 어떻게 든 이것이 성능 차이를 설명합니다. 같은 장소에 복사하면 ideone.com과 같이 memcpy더 빨리 끝날 수 있습니다 .

memmove (002) 0.0610362
memmove (004) 0.0554264
memmove (008) 0.0575859
memmove (016) 0.057326
memmove (032) 0.0583542
memmove (064) 0.0561934
memmove (128) 0.0549391
memcpy 0.0537919

거기에 거의 아무것도하지만 - 이미 메모리 페이지에 오류가 발생한 다시를 작성하는 것은이 있다는 증거 많은 영향을, 우리는 확실히 시간의 반감을 표시되지 않는 ...하지만 그것을 만드는 아무것도 잘못이 있다고 보여 않는 memcpy사과를 비교했을 때 불필요하게 느리게 -사과.


내 버퍼가 캐시보다 훨씬 크기 때문에 CPU 캐시가 차이를 일으키지 않을 것이라고 예상했을 것입니다.
cruppstahl

2
그러나 각각은 동일한 총 메인 메모리 액세스 수를 필요로합니다. (즉 100MB의 읽기 및 100MB의 쓰기). 캐시 패턴은 그와 맞지 않습니다. 따라서 하나가 다른 것보다 느릴 수있는 유일한 방법은 일부 항목을 메모리에서 두 번 이상 읽거나 써야하는 경우입니다.
Oliver Charlesworth 2015

2
- @Tony D 내 결론은 똑똑 나보다 사람들에게 물어이었다)
cruppstahl

1
또한 같은 위치에 복사했지만 memcpy먼저 다시하면 어떻게됩니까?
올리버 찰스 워드

1
@OliverCharlesworth : 첫 번째 테스트 실행은 항상 상당한 히트를 기록하지만 두 개의 memcpy 테스트를 수행합니다. memcpy 0.0688002 0.0583162 | memmove 0.0577443 0.05862 0.0601029 ... ideone.com/8EEAcA 참조
Tony Delroy

25

를 사용 memcpy하는 경우 쓰기가 캐시로 이동해야합니다. memmove작은 단계 앞으로 복사 할 때 사용 하는 경우 복사중인 메모리는 이미 캐시에 있습니다 (2, 4, 16 또는 128 바이트 "뒤"로 읽었 기 때문). memmove대상이 몇 메가 바이트 (> 4 * 캐시 크기) 인 곳에서 시도해보십시오 . 유사한 결과를 얻을 수있을 것이라고 생각합니다 (테스트 할 수는 없습니다).

대용량 메모리 작업을 할 때 ALL은 캐시 유지 관리에 관한 것임을 보증합니다.


+1 언급 한 이유 때문에 역방향 반복 memmove가 memcpy보다 캐시에 더 친숙하다고 생각합니다. 그러나 memcpy 테스트를 두 번 실행하면 두 번째 실행이 memmove만큼 빠르다는 것을 발견했습니다. 왜? 버퍼가 너무 커서 memcpy의 두 번째 실행은 첫 번째 실행만큼 비효율적이어야합니다 (캐시 방식). 따라서 여기에 성능 저하를 유발하는 추가 요인이있는 것 같습니다.
cruppstahl

3
적절한 상황이 주어지면 memcpyTLB가 미리 채워져 있기 때문에 1 초가 눈에 띄게 빨라질 것입니다. 또한 1 초 동안 memcpy"제거"해야 할 항목의 캐시를 비울 필요가 없습니다 (더러운 캐시 라인은 여러면에서 성능에 대해 "나쁜"것입니다. 그러나 확실하게 말하자면, .에 같은 "반환 한"샘플 물건 같은 것을 실행 캐시를-미스, TLB 누락 등
매트 피터슨

15

역사적으로 memmove와 memcopy는 동일한 기능입니다. 그들은 동일한 방식으로 작동하고 동일한 구현을 가졌습니다. 그런 다음 memcopy가 특정 방식으로 겹치는 영역을 처리하기 위해 정의 할 필요가없고 정의 할 필요가 없다는 것을 깨달았습니다.

최종 결과는 memmove가 성능에 영향을 미치는 경우에도 특정 방식으로 겹치는 영역을 처리하도록 정의 된 것입니다. Memcopy는 겹치지 않는 영역에 사용할 수있는 최상의 알고리즘을 사용합니다. 구현은 일반적으로 거의 동일합니다.

당신이 겪은 문제는 x86 하드웨어의 변형이 너무 많아서 메모리를 이동하는 방법이 가장 빠른 방법을 알 수 없다는 것입니다. 그리고 한 상황에서 결과가 있다고 생각하더라도 메모리 레이아웃에 다른 '스트라이드'를 갖는 것과 같은 단순한 것은 캐시 성능이 크게 다를 수 있습니다.

실제로 수행중인 작업을 벤치마킹하거나 문제를 무시하고 C 라이브러리에 대해 수행 된 벤치 마크에 의존 할 수 있습니다.

편집 : 아, 그리고 마지막 한가지; 많은 메모리 내용을 이동하는 것은 매우 느립니다. 정수를 처리하는 간단한 B-Tree 구현과 같은 방식으로 응용 프로그램이 더 빨리 실행될 것이라고 생각합니다. (오, 그래요)

Edit2 : 코멘트에서 나의 확장을 요약하면 : 마이크로 벤치 마크가 여기서 문제이며, 당신이 생각하는 것을 측정하지 않습니다. memcpy와 memmove에 주어진 작업은 서로 크게 다릅니다. memcpy에 주어진 작업이 memmove 또는 memcpy로 여러 번 반복되면 최종 결과는 영역이 겹치지 않는 한 사용하는 메모리 이동 기능에 의존하지 않습니다.


하지만 그게 전부입니다. 제가 실제로하고있는 일을 벤치마킹하고 있습니다. 이 질문은 벤치 마크의 결과를 해석하는 것인데, 이는 여러분이 주장하는 것과 모순되는 것입니다. 즉, memcpy가 겹치지 않는 영역에서 더 빠릅니다.
cruppstahl

내 응용 프로그램 b-tree입니다! 리프 노드에 정수가 삽입 될 때마다 memmove가 호출되어 공간을 만듭니다. 저는 데이터베이스 엔진에서 일하고 있습니다.
cruppstahl

1
마이크로 벤치 마크를 사용하고 있고 memcopy와 memmove가 동일한 데이터를 이동하지도 않습니다. 처리중인 데이터가있는 메모리의 정확한 위치는 캐싱과 CPU가 수행해야하는 메모리 왕복 횟수에 영향을줍니다.
user3710044

이 대답은 맞지만 이 경우에 느린 설명하지는 않습니다. 본질적으로 "어떤 경우에는 더 느릴 수 있기 때문에 더 느립니다"라고 말하는 것입니다.
Oliver Charlesworth

구현이 동일하기 때문에 벤치 마크를 복사 / 이동하는 동일한 메모리 레이아웃을 포함하여 동일한 상황에서 동일 할 것이라고 말하고 있습니다. 문제는 마이크로 벤치 마크에 있습니다.
user3710044

2

"memcpy는 memmove보다 효율적입니다." 귀하의 경우에는 두 기능을 실행하는 동안 동일한 작업을 수행하지 않을 것입니다.

일반적으로 필요한 경우에만 memmove를 사용하십시오. 소스 및 대상 영역이 겹칠 가능성이 매우 높을 때 사용하십시오.

참조 : https://www.youtube.com/watch?v=Yr1YnOVG-4g Dr. Jerry Cain, (Stanford Intro Systems Lecture-7) 시간 : 36:00

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.