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

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

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

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



답변

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

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


답변

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

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 ++)가 아닌 어셈블리로 작성됩니다. 대기 시간 숨김 및 처리량의 최대 이점을 얻으려면 수동으로로드 및 저장 명령을 배치해야하기 때문입니다.

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

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

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

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

요인

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

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

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


답변

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

에서 하나의 구현 예 :

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


답변

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() 보다 훨씬 빠릅니다 .


답변

짧은 답변:

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

답변

실제 구현에서 실제로 사용되는지 여부는 모르겠지만 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 기사를 참조하십시오.


답변

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