memcpy () 및 memmove ()가 포인터 증가보다 빠른 이유는 무엇입니까?


92

N 바이트를 pSrc에서 pDest. 이것은 단일 루프에서 수행 할 수 있습니다.

for (int i = 0; i < N; i++)
    *pDest++ = *pSrc++

이것이 memcpy또는 보다 느린 이유는 무엇 memmove입니까? 속도를 높이기 위해 어떤 트릭을 사용합니까?


2
루프는 한 위치 만 복사합니다. 나는 당신이 어떻게 든 포인터를 증가시키는 것을 의미했다고 생각합니다.
Mysticial

13
아니면 내가 한 것처럼 그들을 위해 고칠 수 있습니다. 그리고, BTW, 진정한 C 프로그래머 지금까지 의 카운트 1N, 그것의 항상 에서 0N-1:-)
paxdiablo

6
@paxdiablo : 배열을 반복하고 있다면 확실합니다. 그러나 1에서 N으로 반복하는 것이 괜찮은 경우가 많이 있습니다. 데이터로 수행하는 작업에 따라 다릅니다. 예를 들어 1부터 시작하는 번호 매기기 목록을 사용자에게 표시하는 경우 1부터 시작하는 것이 더 합리적 일 수 있습니다. 어쨌든 int서명되지 않은 유형을 size_t대신 사용해야 할 때 카운터로 사용하는 더 큰 문제는 무시합니다 .
Billy ONeal 2011 년

2
@paxdiablo N에서 1까지 계산할 수도 있습니다. 일부 프로세서에서는 비교 명령어가 0에 도달하면 감소가 분기 명령어에 대한 적절한 비트를 설정하므로 하나를 제거합니다.
onemasse

6
질문의 전제는 거짓이라고 생각합니다. 현대 컴파일러는 이것을 memcpyor 로 변환합니다 memmove(포인터가 별칭을 사용할 수 있는지 여부에 따라).
데이비드 슈워츠

답변:


120

memcpy는 바이트 포인터 대신 워드 포인터를 사용하기 때문에 memcpy 구현은 종종 한 번에 128 비트를 섞을 수있는 SIMD 명령어로 작성됩니다 .

SIMD 명령어는 최대 16 바이트 길이의 벡터의 각 요소에 대해 동일한 작업을 수행 할 수있는 어셈블리 명령어입니다. 여기에는로드 및 저장 지침이 포함됩니다.


15
당신은 GCC를 위해 전원을 켜면 -O3, 그것이 알고 적어도 경우 루프 SIMD를 사용 pDest하고 pSrc별칭을하지 않습니다.
Dietrich Epp 2011 년

저는 현재 64 바이트 (512 비트) SIMD를 사용하는 Xeon Phi에서 작업 중이므로 "최대 16 바이트"라는 내용은 저를 웃게 만듭니다. 또한 SIMD를 활성화 할 대상 CPU를 지정해야합니다 (예 : -march = native).
yakoudbz

내 대답을 수정해야 할 것 같습니다. :)
onemasse

게시 시점에서도 매우 구식입니다. x86 (2011 년 출시)의 AVX 벡터는 길이가 32 바이트이고 AVX-512는 길이가 64 바이트입니다. 1024 비트 또는 2048 비트 벡터 또는 ARM SVE와 같은 가변 벡터 폭이있는 일부 아키텍처가 있습니다.
phuclv

@phuclv 지침을 사용할 수있는 동안 memcpy가 지침을 사용한다는 증거가 있습니까? 라이브러리가 따라 잡는 데는 일반적으로 시간이 걸리며 내가 찾을 수있는 최신 라이브러리는 SSSE3를 사용하며 2011 년보다 훨씬 더 최신입니다.
Pete Kirkham

81

메모리 복사 루틴은 다음과 같은 포인터를 통한 단순한 메모리 복사보다 훨씬 더 복잡하고 빠를 수 있습니다.

void simple_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;
  for (int i = 0; i < bytes; ++i)
    *b_dst++ = *b_src++;
}

개량

첫 번째 개선 사항은 단어 경계에 포인터 중 하나를 정렬하고 (단어 단위는 기본 정수 크기, 일반적으로 32 비트 / 4 바이트를 의미하지만 최신 아키텍처에서는 64 비트 / 8 바이트 일 수 있음) 단어 크기 이동을 사용하는 것입니다. / 카피 지침. 이를 위해서는 포인터가 정렬 될 때까지 바이트 간 복사를 사용해야합니다.

void aligned_memory_copy(void* dst, void* src, unsigned int bytes)
{
  unsigned char* b_dst = (unsigned char*)dst;
  unsigned char* b_src = (unsigned char*)src;

  // Copy bytes to align source pointer
  while ((b_src & 0x3) != 0)
  {
    *b_dst++ = *b_src++;
    bytes--;
  }

  unsigned int* w_dst = (unsigned int*)b_dst;
  unsigned int* w_src = (unsigned int*)b_src;
  while (bytes >= 4)
  {
    *w_dst++ = *w_src++;
    bytes -= 4;
  }

  // Copy trailing bytes
  if (bytes > 0)
  {
    b_dst = (unsigned char*)w_dst;
    b_src = (unsigned char*)w_src;
    while (bytes > 0)
    {
      *b_dst++ = *b_src++;
      bytes--;
    }
  }
}

서로 다른 아키텍처는 소스 또는 대상 포인터가 적절하게 정렬되었는지에 따라 다르게 수행됩니다. 예를 들어 XScale 프로세서에서 소스 포인터가 아닌 대상 포인터를 정렬하여 더 나은 성능을 얻었습니다.

성능을 더욱 향상시키기 위해 일부 루프 언 롤링을 수행하여 더 많은 프로세서 레지스터에 데이터를로드 할 수 있습니다. 즉,로드 / 저장 명령을 인터리브 할 수 있고 추가 명령 (예 : 루프 카운팅 등)에 의해 지연 시간을 숨길 수 있습니다. 로드 / 저장 명령 지연 시간이 상당히 다를 수 있기 때문에 이것이 가져 오는 이점은 프로세서에 따라 상당히 다릅니다.

이 단계에서 코드는 C (또는 C ++)가 아닌 어셈블리로 작성됩니다. 대기 시간 숨김 및 처리량의 최대 이점을 얻으려면 수동으로로드 및 저장 명령을 배치해야하기 때문입니다.

일반적으로 데이터의 전체 캐시 라인은 언 롤링 된 루프의 한 반복에서 복사되어야합니다.

다음 개선 사항으로 프리 페치를 추가합니다. 이는 프로세서의 캐시 시스템에 메모리의 특정 부분을 캐시에로드하도록 지시하는 특수 명령입니다. 명령어를 발행하고 캐시 라인을 채우는 사이에 지연이 있기 때문에 데이터를 복사 할 때와 조만간 또는 나중에 사용할 수 있도록 명령어를 배치해야합니다.

이것은 프리 페치 명령어를 함수의 시작 부분과 메인 복사 루프 안에 넣는 것을 의미합니다. 여러 반복 시간에 복사 될 데이터를 가져 오는 복사 루프 중간에 프리 페치 명령어를 사용합니다.

기억이 나지 않지만 목적지 주소와 소스 주소를 미리 가져 오는 것도 도움이 될 수 있습니다.

요인

메모리 복사 속도에 영향을 미치는 주요 요인은 다음과 같습니다.

  • 프로세서, 캐시 및 주 메모리 간의 대기 시간입니다.
  • 프로세서 캐시 라인의 크기와 구조.
  • 프로세서의 메모리 이동 / 복사 명령 (대기 시간, 처리량, 레지스터 크기 등).

따라서 효율적이고 빠른 메모리 대처 루틴을 작성하려면 작성중인 프로세서와 아키텍처에 대해 많은 것을 알아야합니다. 일부 임베디드 플랫폼에서 작성하지 않는 한 내장 메모리 복사 루틴을 사용하는 것이 훨씬 쉬울 것입니다.


최신 CPU는 선형 메모리 액세스 패턴을 감지하고 자체적으로 프리 페치를 시작합니다. 이로 인해 프리 페치 지침이 큰 차이가 없을 것으로 예상합니다.
maxy

@maxy 메모리 복사 루틴을 구현 한 몇 안되는 아키텍처에서 프리 페치를 추가하는 것이 상당한 도움이되었습니다. 현재 세대의 Intel / AMD 칩이 충분히 앞서 프리 페치를 수행하는 것은 사실 일 수 있지만, 그렇지 않은 구형 칩과 기타 아키텍처가 많이 있습니다.
Daemin

누구든지 "(b_src & 0x3)! = 0"을 설명 할 수 있습니까? 나는 그것을 이해할 수 없으며 또한 컴파일되지 않을 것입니다 (오류가 발생합니다 : 잘못된 연산자를 바이너리로 & : unsigned char 및 int);
David Refaeli

"(b_src & 0x3)! = 0"은 최하위 2 비트가 0이 아닌지 확인합니다. 따라서 소스 포인터가 4 바이트의 배수로 정렬되는지 여부를 확인합니다. 컴파일 오류는 0x3을 in이 아닌 바이트로 취급하기 때문에 발생합니다. 0x00000003 또는 0x3i를 사용하여 수정할 수 있습니다 (제 생각에).
Daemin

b_src & 0x3포인터 유형에 대해 비트 연산을 수행 할 수 없기 때문에 컴파일되지 않습니다. 당신은 그것을 캐스트 할 필요가 (u)intptr_t첫째
phuclv

18

memcpy컴퓨터의 아키텍처에 따라 한 번에 둘 이상의 바이트를 복사 할 수 있습니다. 대부분의 최신 컴퓨터는 단일 프로세서 명령에서 32 비트 이상으로 작동 할 수 있습니다.

에서 하나의 구현 예 :

    00026 * 빠른 복사를 위해 두 포인터가 모두
    00027 * 및 길이는 단어로 정렬되며 대신 한 번에 한 단어 씩 복사
    한 번에 00028 * 바이트. 그렇지 않으면 바이트 단위로 복사하십시오.

8
온보드 캐시가없는 386 (예 : 예)에서 이것은 큰 차이를 만들었습니다. 대부분의 최신 프로세서에서 읽기 및 쓰기는 한 번에 한 캐시 라인에서 발생하며 일반적으로 메모리 대 버스가 병목 현상이 발생하므로 4 배에 가까운 곳이 아닌 몇 퍼센트의 개선을 기대할 수 있습니다.
Jerry Coffin 2011 년

2
나는 당신이 "출처에서"라고 말할 때 좀 더 명시 적이어야한다고 생각합니다. 물론 이것은 일부 아키텍처의 "원본"이지만 BSD 또는 Windows 시스템에서는 확실히 그렇지 않습니다. (그리고 지옥, 심지어 GNU 시스템 사이에 차이가 많이이 기능 종종있다)
빌리 ONeal

@Billy ONeal : +1 정말 맞습니다 ... 고양이 피부를 만드는 방법은 여러 가지가 있습니다. 그것은 하나의 예일뿐입니다. 결정된! 건설적인 의견에 감사드립니다.
마크 바이어스

7

memcpy()다음 기술 중 하나를 사용하여 구현할 수 있으며 일부는 성능 향상을 위해 아키텍처에 따라 다르며 모두 코드보다 훨씬 빠릅니다.

  1. 바이트 대신 32 비트 단어와 같은 더 큰 단위를 사용하십시오. 여기에서도 정렬을 처리 할 수 ​​있습니다 (또는해야 할 수도 있습니다). 예를 들어 일부 플랫폼에서는 32 비트 단어를 이상한 메모리 위치로 읽고 쓸 수 없으며 다른 플랫폼에서는 엄청난 성능 저하를 지불합니다. 이 문제를 해결하려면 주소는 4로 나눌 수있는 단위 여야합니다. 64 비트 CPU의 경우 최대 64 비트까지 가져 오거나 SIMD (단일 명령, 다중 데이터) 명령 ( MMX , SSE 등)을 사용하여 더 높은 값을 취할 수 있습니다 .

  2. 컴파일러가 C에서 최적화 할 수없는 특수 CPU 명령어를 사용할 수 있습니다. 예를 들어 80386에서는 "rep"접두사 명령어 + "movsb"명령어를 사용하여 개수에 N을 배치하여 지시 된 N 바이트를 이동할 수 있습니다. 레지스터. 좋은 컴파일러는 당신을 위해 이것을 할 것이지만 당신은 좋은 컴파일러가없는 플랫폼에있을 수 있습니다. 이 예제는 속도의 나쁜 데모 인 경향이 있지만 정렬 + 더 큰 단위 명령과 결합하면 특정 CPU의 다른 모든 것보다 빠를 수 있습니다.

  3. 루프 풀기 -분기는 일부 CPU에서 매우 비쌀 수 있으므로 루프를 풀면 분기 수를 줄일 수 있습니다. 이것은 또한 SIMD 명령어 및 매우 큰 단위와 결합하는 좋은 기술입니다.

예를 들어, http://www.agner.org/optimize/#asmlib는memcpy그 비트 대부분의 거기 (아주 작은 양만큼) 구현을. 소스 코드를 읽으면 위의 세 가지 기술을 모두 끌어내는 수많은 인라인 어셈블리 코드로 가득 차 있으며 실행중인 CPU에 따라 이러한 기술을 선택합니다.

버퍼에서도 바이트를 찾기 위해 만들 수있는 유사한 최적화가 있습니다. strchr()그리고 친구들은 종종 당신의 손으로 굴리는 것보다 더 빠를 것입니다. .NETJava의 경우 특히 그렇습니다 . 예를 들어, .NET에서 내장 은 위의 최적화 기술을 사용하기 때문에 Boyer–Moore 문자열 검색String.IndexOf() 보다 훨씬 빠릅니다 .


1
당신이 연결하는 동일한 Agner Fog는 또한 루프 언 롤링이 현대 CPU에서 역효과를 낳는다는 이론을 내 세웁니다 .

요즘 대부분의 CPU는 좋은 분기 예측 기능을 가지고 있으므로 일반적인 경우 루프 풀기의 이점을 무효화해야합니다. 좋은 최적화 컴파일러는 여전히 그것을 사용할 수 있습니다.
thomasrutter

5

짧은 답변:

  • 캐시 채우기
  • 가능한 경우 바이트 전송 대신 단어 처리
  • SIMD 마술

4

실제 구현에서 실제로 사용되는지 여부는 모르겠지만 Duff의 장치 는 여기서 언급 할 가치가 memcpy있다고 생각 합니다.

에서 위키 백과 :

send(to, from, count)
register short *to, *from;
register count;
{
        register n = (count + 7) / 8;
        switch(count % 8) {
        case 0:      do {     *to = *from++;
        case 7:              *to = *from++;
        case 6:              *to = *from++;
        case 5:              *to = *from++;
        case 4:              *to = *from++;
        case 3:              *to = *from++;
        case 2:              *to = *from++;
        case 1:              *to = *from++;
                } while(--n > 0);
        }
}

위의 내용은 memcpy의도적으로 to포인터를 증가시키지 않기 때문에 a 가 아닙니다 . 메모리 매핑 레지스터에 쓰기라는 약간 다른 작업을 구현합니다. 자세한 내용은 Wikipedia 기사를 참조하십시오.


Duff의 장치 또는 초기 점프 메커니즘은 포인터가 더 큰 메모리 이동 명령을 사용할 수있는 더 좋은 경계에 정렬되도록 처음 1..3 (또는 1..7) 바이트를 복사하는 데 유용합니다.
Daemin

@MarkByers :이 코드는 약간 다른 작업을 보여줍니다 ( *to메모리 매핑 레지스터를 나타내며 의도적으로 증가되지 않음-링크 된 문서 참조). 내가 분명히 말한 것처럼 내 대답은 효율적인을 제공하려는 memcpy것이 아니라 단순히 호기심 많은 기술을 언급합니다.
NPE 2011 년

@Daemin 동의합니다. do {} while ()을 건너 뛸 수 있으며 스위치는 컴파일러에 의해 점프 테이블로 변환됩니다. 나머지 데이터를 관리하고 싶을 때 매우 유용합니다. Duff의 장치에 대한 경고가 언급되어야합니다. 분명히 새로운 아키텍처 (새로운 x86)에서는 분기 예측이 매우 효율적이므로 Duff의 장치는 실제로 단순한 루프보다 느립니다.
onemasse

1
오 안돼 .. 더프의 장치가 아니야. Duff의 장치를 사용하지 마십시오. 부디. PGO를 사용하고 컴파일러가 의미있는 곳에서 루프 언 롤링을 수행하도록합니다.
Billy ONeal 2011 년

아니요, Duff의 장치는 현대 구현에서 사용되지 않습니다.
gnasher729

3

다른 사람들과 마찬가지로 memcpy는 1 바이트 청크보다 큰 복사본을 말합니다. 단어 크기의 청크로 복사하는 것이 훨씬 빠릅니다. 그러나 대부분의 구현에서는 한 단계 더 나아가 루핑하기 전에 여러 MOV (단어) 명령을 실행합니다. 예를 들어 루프 당 8 개의 단어 블록을 복사하는 것의 장점은 루프 자체가 비용이 많이 든다는 것입니다. 이 기술은 조건부 분기의 수를 8 배로 줄여 거대한 블록에 대한 복사본을 최적화합니다.


1
나는 이것이 사실이라고 생각하지 않는다. 루프를 풀 수는 있지만 대상 아키텍처에서 한 번에 처리 할 수있는 것보다 더 많은 데이터를 단일 명령어로 복사 할 수는 없습니다. 또한 루프를
풀어야

@Billy ONeal : 그게 VoidStar가 의미하는 바라고 생각하지 않습니다. 여러 개의 연속적인 이동 명령을 사용하면 단위 수를 계산하는 오버 헤드가 줄어 듭니다.
wallyk

@Billy ONeal : 요점을 놓치고 있습니다. 한 번에 한 단어는 MOV, JMP, MOV, JMP 등과 같습니다. 어디에서 MOV MOV MOV MOV JMP를 할 수 있습니다. 나는 전에 mempcy를 작성했습니다 내가 그 일을하는 방법을 많이 벤치마킹 한)
VoidStar

@wallyk : 아마도. 그러나 그는 "더 큰 청크 복사"라고 말합니다. 이것은 실제로 불가능합니다. 그가 루프 언 롤링을 의미한다면, 그는 "대부분의 구현은 한 단계 더 나아가 루프를 풀다"라고 말해야합니다. 작성된 답은 오해의 소지가 있고 최악의 경우에는 잘못되었습니다.
Billy ONeal 2011 년

@VoidStar : 동의합니다 --- 이제 더 좋습니다. +1.
Billy ONeal 2011 년

2

대답은 훌륭하지만 여전히 빠른 속도를 구현 memcpy하려면 fast memcpy, Fast memcpy in C에 대한 흥미로운 블로그 게시물이 있습니다.

void *memcpy(void* dest, const void* src, size_t count)
{
    char* dst8 = (char*)dest;
    char* src8 = (char*)src;

    if (count & 1) {
        dst8[0] = src8[0];
        dst8 += 1;
        src8 += 1;
    }

    count /= 2;
    while (count--) {
        dst8[0] = src8[0];
        dst8[1] = src8[1];

        dst8 += 2;
        src8 += 2;
    }
    return dest;
}

심지어 메모리 액세스를 최적화하면 더 나을 수 있습니다.


1

많은 라이브러리 루틴과 마찬가지로 실행중인 아키텍처에 최적화되어 있기 때문입니다. 다른 사람들은 사용할 수있는 다양한 기술을 게시했습니다.

선택권이 주어지면 직접 롤링하지 말고 라이브러리 루틴을 사용하십시오. 이것은 DRO (Do n't Repeat Others)라고 부르는 DRY의 변형입니다. 또한 라이브러리 루틴은 자체 구현보다 잘못 될 가능성이 적습니다.

메모리 액세스 검사기가 단어 크기의 배수가 아닌 메모리 또는 문자열 버퍼에서 범위를 벗어난 읽기에 대해 불평하는 것을 보았습니다. 이것은 사용중인 최적화의 결과입니다.


0

memset, memcpy 및 memmove의 MacOS 구현을 볼 수 있습니다.

부팅시 OS는 실행중인 프로세서를 결정합니다. 지원되는 각 프로세서에 대해 특별히 최적화 된 코드가 내장되어 있으며 부팅시 고정 된 읽기 / 전용 위치에 올바른 코드에 jmp 명령을 저장합니다.

C memset, memcpy 및 memmove 구현은 고정 된 위치로 이동하는 것입니다.

구현은 memcpy 및 memmove에 대한 소스 및 대상의 정렬에 따라 다른 코드를 사용합니다. 그들은 분명히 사용 가능한 모든 벡터 기능을 사용합니다. 또한 많은 양의 데이터를 복사 할 때 비 캐싱 변형을 사용하고 페이지 테이블 대기를 최소화하는 지침이 있습니다. 이는 단순한 어셈블러 코드가 아니라 각 프로세서 아키텍처에 대해 매우 잘 알고있는 사람이 작성한 어셈블러 코드입니다.

인텔은 또한 문자열 작업을 더 빠르게 할 수있는 어셈블러 명령어를 추가했습니다. 예를 들어 한 사이클에서 256 바이트를 비교하는 strstr을 지원하는 명령이 있습니다.


memset / memcpy / memmove의 Apple 오픈 소스 버전은 SIMD를 사용하는 실제 버전보다 훨씬 느린 일반 버전
일뿐입니다
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.