memcpy() 및 memmove()가 포인터의 증가보다 빠른 이유는 무엇입니까?
를 N바이트에서 .pSrc
로로 합니다.pDest
1번으로 하다
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++)가 아닌 어셈블리에 작성됩니다.지연 숨김과 throughput의 이점을 최대한 활용하려면 수동으로 부하를 배치하고 명령을 저장해야 하기 때문입니다.
일반적으로 데이터의 캐시 행 전체를 롤되지 않은 루프의 1회 반복으로 복사해야 합니다.
다음 단계로 넘어가겠습니다. 프리페치(pre-fetching)를 추가합니다.이것들은 프로세서의 캐시 시스템에 메모리의 특정 부분을 캐시에 로드하도록 지시하는 특별한 명령입니다.명령어 발행과 캐시 행의 기입 사이에는 지연이 있기 때문에, 명령어는, 데이터가 카피되고 있는 그대로의 타이밍에 사용할 수 있도록, 또는 나중에 사용할 수 있도록 배치될 필요가 있습니다.
즉, 기능의 시작 부분과 메인 복사 루프 내부에 프리페치 명령을 넣는 것입니다.복사 루프 중간에 프리페치 지침을 사용하여 여러 번 반복할 데이터를 가져옵니다.
기억나지 않지만, 송신원 주소뿐만 아니라 수신처 주소도 프리페치 하는 것이 좋을지도 모릅니다.
요인들
메모리의 카피 속도에 영향을 주는 주된 요인은 다음과 같습니다.
- 프로세서, 그 캐시 및 메인 메모리 사이의 지연 시간.
- 프로세서의 캐시 라인의 크기와 구조.
- 프로세서의 메모리 이동/복사 명령(지연, throughput, 레지스터 크기 등)
따라서 효율적이고 빠른 메모리 대응 루틴을 작성하려면 해당 프로세서와 아키텍처에 대해 많은 것을 알아야 합니다.즉, 임베디드 플랫폼에 글을 쓰는 것이 아니라면 내장 메모리 복사 루틴을 사용하는 것이 훨씬 쉬워집니다.
memset, memcpy 및 memmove의 MacOS 구현을 확인할 수 있습니다.
기동시에, OS 는 어느 프로세서에서 동작하고 있는지를 판별합니다.지원되는 각 프로세서에 대해 특별히 최적화된 코드가 내장되어 있으며 부팅 시 jmp 명령을 올바른 코드로 고정 읽기/전용 위치에 저장합니다.
C memset, memcpy 및 memmove의 실장은 그 고정 위치에 대한 점프일 뿐입니다.
실장에서는, memcpy 와 memmove 의 송신원과 수신처의 얼라인먼트에 따라 다른 코드를 사용합니다.이들은 사용 가능한 모든 벡터 기능을 사용합니다.또한 대량의 데이터를 복사할 때 캐시되지 않은 변형 모델을 사용하고 페이지 테이블 대기 시간을 최소화하기 위한 지침을 제공합니다.어셈블러 코드뿐만 아니라 각 프로세서 아키텍처에 대해 매우 잘 아는 사람이 작성한 어셈블러 코드입니다.
또, 인텔은, 스트링 조작을 고속화하는 어셈블러 명령도 추가했습니다.예를 들어, 256 바이트를 1 사이클로 비교하는 strst를 지원하는 명령입니다.
는 알 수 .memcpy
더프 디바이스는 여기서 언급할 가치가 있다고 생각합니다.
Wikipedia에서:
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
레지스터에 합니다.이것은 약간 다른 조작을 구현합니다.이치노위키피디아입니다.
memcpy
는 컴퓨터 아키텍처에 따라 한 번에 여러 바이트를 복사할 수 있습니다.대부분의 최신 컴퓨터는 단일 프로세서 명령으로 32비트 이상을 사용할 수 있습니다.
00026 * 빠른 복사를 위해 두 포인터가 모두 있는 일반적인 경우를 최적화합니다.00027 * 및 길이는 워드 정렬이며 대신 워드 한 번에 복사00028 * 한 번에 바이트 수.그렇지 않으면 바이트 단위로 복사합니다.
'실행할 수 요.memcpy()
다음 기술 중 하나를 사용하는 경우, 일부는 성능 향상을 위해 아키텍처에 의존하며, 모두 코드보다 훨씬 빠릅니다.
바이트 대신 32비트 단어와 같은 더 큰 단위를 사용합니다.여기서도 얼라인먼트를 처리할 수 있습니다(또는 처리해야 할 수도 있습니다).예를 들어 일부 플랫폼에서는 32비트 단어를 읽고 쓸 수 없으며 다른 플랫폼에서는 엄청난 성능 저하가 발생합니다.이 문제를 해결하려면 주소가 4로 나눌 수 있는 단위여야 합니다.64비트 CPU의 경우 최대 64비트 또는 SIMD(Single Instruction, Multiple Data) 명령(MMX, SSE 등)을 사용하여 이 값을 더 높일 수 있습니다.
컴파일러가 C에서 최적화하지 못할 수 있는 특수한 CPU 명령을 사용할 수 있습니다.예를 들어 80386에서는 "rep" 프리픽스 명령 + "movsb" 명령을 사용하여 N을 카운트 레지스터에 배치하여 N바이트를 이동할 수 있습니다.좋은 컴파일러는 이 기능을 제공하지만, 좋은 컴파일러가 없는 플랫폼일 수 있습니다.주의: 이 예에서는 속도를 제대로 보여주지 못하는 경향이 있지만 정렬 + 더 큰 장치 명령과 조합하면 특정 CPU의 다른 모든 명령보다 속도가 더 빠를 수 있습니다.
루프 언롤링 -- 일부 CPU에서는 브랜치 비용이 많이 들기 때문에 루프를 언롤링하면 브랜치 수가 줄어듭니다.이는 SIMD 명령 및 매우 큰 유닛과 조합하는 데에도 좋은 기술입니다.
예를 들어 http://www.agner.org/optimize/#asmlib에는memcpy
(매우 적은 양으로) 가장 뛰어난 구현입니다.소스 코드를 읽으면 위의 3가지 기술을 모두 실행할 수 있는 수많은 인라인 어셈블리코드로 가득합니다.실행하고 있는 CPU에 근거해, 어느 기술을 사용할지를 선택합니다.
버퍼에서 바이트를 검색하기 위한 유사한 최적화도 있습니다. strchr()
그리고 친구들은 종종 당신의 손놀림보다 더 빨리 지나가게 될 것이다.이것은 특히 에 해당됩니다.NET 및 Java.예를 들어, 입니다.NET, 빌트인String.IndexOf()
는 위의 최적화 기술을 사용하기 때문에 Boyer-Moore 문자열 검색보다 훨씬 빠릅니다.
간단한 답변:
- 캐시 충전
- 가능한 경우 바이트 전송 대신 워드 크기 전송
- SIMD 매직
memcpy 복사본이 1바이트 청크보다 크다는 다른 설명과 같습니다.단어 크기 청크로 복사하는 것이 훨씬 빠릅니다.그러나 대부분의 구현에서는 한 걸음 더 나아가 루프하기 전에 여러 MOV(word) 명령을 실행합니다.예를 들어 루프당8 워드 블록의 복사는 루프 자체에 비용이 많이 든다는 장점이 있습니다.이 기술은 조건부 분기 수를 8배 줄여 자이언트 블록에 대한 복사를 최적화합니다.
답변은 훌륭하지만 빠른 구현을 원하신다면memcpy
C의 빠른 memcpy, 빠른 memcpy에 대한 흥미로운 블로그 게시물이 있습니다.
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;
}
메모리 액세스를 최적화하면 더 좋을 수 있습니다.
많은 라이브러리 루틴과 마찬가지로 실행 중인 아키텍처에 최적화되어 있기 때문입니다.다른 사람들은 사용할 수 있는 다양한 기술을 게시했다.
선택사항이 있는 경우, 직접 롤하지 말고 라이브러리 루틴을 사용하십시오.이것은 DRY의 변형으로 DRO(Don't Repeat Others)라고 부릅니다.또, 라이브러리의 루틴은, 자신의 실장보다 잘못될 가능성이 적습니다.
메모리 액세스체커에서 메모리 또는 문자열 버퍼의 범위를 벗어난 읽기에 대해 불평하는 것을 본 적이 있습니다.이러한 읽기가 워드 사이즈의 배수가 아닙니다.이는 최적화가 사용된 결과입니다.
언급URL : https://stackoverflow.com/questions/7776085/why-are-memcpy-and-memmove-faster-than-pointer-increments
'programing' 카테고리의 다른 글
페이지 전체에서 키 누르기 이벤트를 청취하려면 어떻게 해야 하나요? (0) | 2023.03.18 |
---|---|
UI-Router가 Angular에 있는 기본 서브스테이트로 상태 리다이렉트JS (0) | 2023.03.18 |
Flask SQL Chemy를 사용하여 SQLite에서 MySQL로 전환 (0) | 2023.02.04 |
macports 업데이트 후 mariadb가 포트 3306에서 수신을 하지 않는 이유는 무엇입니까? (0) | 2023.02.04 |
PHP 7: VCRUNTIME 140.dll이 없습니다. (0) | 2023.02.04 |