2010-06-03 3 views
34

나는 memcpy를 수행하는 함수를 가지고 있지만 막대한 양의 사이클을 차지합니다. memcpy를 사용하여 메모리 조각을 이동하는 것보다 빠른 대안/접근법이 있습니까?memcpy보다 빠른 대안?

+1

짧은 답변 : 어쩌면, 그것은 가능하다. 건축술, 플래트 홈 및 다른 사람 같이 세부 사항을 더 제안하십시오. 임베디드 환경에서 그렇게 잘 수행하지 못하는 libc의 함수를 재 작성하는 것은 매우 가능성이 있습니다. – INS

답변

111

memcpy은 메모리에서 바이트를 복사 할 수있는 가장 빠른 방법입니다. 더 빠른 것이 필요한 경우 이 아닌 등의 정보를 복사 해보세요. 데이터 자체가 아닌 포인터 만 교환하십시오.

+2

+1, 우리는 최근 일부 코드가 갑자기 속도가 느려지고 특정 파일을 처리 할 때 많은 추가 메모리를 소비했을 때 문제가 발생했습니다.파일에는 거대한 메타 데이터 블록이 있었지만 다른 파리에는 메타 데이터 나 작은 블록이 없었습니다. 그리고 그 메타 데이터는 복사, 복사, 복사되고 시간과 메모리 모두를 소비했습니다. const 별 참조에 의한 복사로 대체되었습니다. – sharptooth

+6

더 빠른 memcpy에 대해서는 좋은 질문입니다. 그러나이 대답은 해결 방법이 아니라 답을 제공합니다. 예 : http://software.intel.com/en-us/articles/memcpy-performance/는 memcpy가 종종 그렇게 효율적이지 않은 이유에 대해 설명합니다. –

+0

낮은 수준에서 또는 의도적으로 코드에서 쓰기 복사 기술을 사용할 수 있습니까? 정수 배수의 페이지와 비슷한 크기의 메모리 덩어리가 필요합니까? 그런 다음 실제 메모리를 가리키는 두 포인터를 같은 메모리에두고 메모리 관리자가 데이터가 변경 될 때 필요에 따라 페이지 사본을 작성하게합니다. –

6

일반적으로 컴파일러와 함께 제공되는 표준 라이브러리는 이미 대상 플랫폼에서 가능한 가장 빠른 방법 인 memcpy()을 구현합니다.

3

일반적으로 복사를하지 않는 것이 더 빠릅니다. 복사하지 못하도록 기능을 조정할 수 있는지 여부는 알지 못합니다.하지만 조사할만한 가치가 있습니다.

3

때로는 방어 적이기, memset 함수와 같은 기능 ... 두 가지 방법으로 구현됩니다 :

  • 한 번 즉시 모든
  • 하지 인라인있어 일부 조립과 같은 실제 함수로 한 번

    • 컴파일러는 기본적으로 인라인 어셈블리 버전을 사용하므로 컴파일러에서 기본적으로 함수 변형을 사용할 수 있으므로 함수 호출로 인해 약간의 오버 헤드가 발생합니다. 함수의 고유 변형 ​​(명령 행 옵션, pragma 's, ...)을 취하는 방법을 보려면 컴파일러를 확인하십시오.

      편집 : Microsoft C 컴파일러의 내장 함수에 대한 설명은 http://msdn.microsoft.com/en-us/library/tzkfha43%28VS.80%29.aspx을 참조하십시오.

    0

    memcpy의 성능이 문제가 될 경우 주위에 복사하려는 거대한 메모리 영역이 있어야한다고 가정합니다.

    이 경우, 내가 물건을 복사하지 몇 가지 방법을 알아 내기 위해 번호의 제안에 동의 할 거라고

    .. 당신이 그것을 변경해야 할 때마다

    대신 메모리를 하나 개의 거대한 덩어리를 갖는

    주위에 복사 할 경우 아마도 대신 대체 데이터 구조를 시도해야합니다.

    문제 영역에 대해 알지 못하는 사이에 persistent data structures을 잘 살펴보고 기존 구현을 재사용 할 것을 제안합니다.

    2

    컴파일러/플랫폼 매뉴얼을 확인하십시오. 일부 마이크로 프로세서 및 memcpy를 사용하는 DSP 키트는 intrinsic functions 또는 DMA 작업보다 훨씬 느립니다.

    2

    플랫폼에서 지원하는 경우 mmap() 시스템 호출을 사용하여 데이터를 파일에 남겨 둘 수 있는지 살펴보십시오. 일반적으로 OS가이를 더 잘 관리 할 수 ​​있습니다. 그리고 모두가 말했듯이 가능하다면 복사하지 마십시오. 포인터는 이런 경우에 당신의 친구입니다.

    10

    자세한 내용을 제공해주십시오. i386 아키텍처에서는 memcpy가 가장 빠른 복사 방법 일 가능성이 높습니다. 하지만 컴파일러가 최적화 된 버전을 가지고 있지 않은 다른 아키텍처에서는 memcpy 함수를 다시 작성하는 것이 가장 좋습니다. 어셈블리 언어를 사용하여 사용자 정의 ARM 아키텍처에서이 작업을 수행했습니다. 만약 당신이 큰 덩어리의 메모리를 전송하면 DMA 아마 당신이 찾고있는 해답입니다.

    아키텍처, 운영 체제 (해당하는 경우)를 추가로 제공하십시오.

    +1

    ARM의 경우 libc impl이 이제는 사용자가 직접 만들 수있는 것보다 빠릅니다. 작은 사본 (페이지가 적은 것)의 경우, 함수 내에서 ASM 루프를 사용하는 것이 더 빠를 수 있습니다. 그러나 큰 복사본의 경우 diff 프로세서는 약간 다른 "최적의"코드 경로를 가지고 있기 때문에 libc impl을 능가 할 수 없습니다. 예를 들어 Cortex8은 NEON 복사 명령어에서 가장 잘 작동하지만 Cortex9는 ldm/stm ARM 명령어보다 빠릅니다. 두 프로세서 모두에 대해 빠른 코드를 작성할 수는 없지만 큰 버퍼의 경우 memcpy를 호출하면됩니다. – MoDJ

    +0

    @MoDJ : 표준 C 라이브러리에는 모든 정의 된 동작이 있지만 최적화 된 경우가 다르며 경우에 따라 정렬 또는 정렬 된 사용에 대한 제한이있는 경우 일반적으로 동일한 의미를 갖는 몇 가지 다른 memcpy 변형이 포함되어 있었으면합니다. 코드가 일반적으로 적은 수의 바이트 또는 정렬 될 단어를 복사해야하는 경우 순진한 한 번에 한 번에 한 번만 구현하면 짧은 시간 내에 결정을 내릴 수있는 memcpy() 구현보다 더 빨리 작업을 수행 할 수 있습니다. 행동의 과정. – supercat

    0

    nos가 옳다면, 너무 많이 부릅니다.

    디버거에서 호출하는 위치와 이유를 보려면 디버거에서 몇 번 멈추고 스택을 살펴보십시오.

    0

    메모리를 가지고 있으며, 방어 적이기는 일반적으로 그것을 사용합니다. 그리고 이것은 보통 가장 빠른 방법입니다.

    CPU가 정확히 무엇을하고 있는지 확인해야합니다. Linux에서는 sar -B 1 또는 vmstat 1 또는/proc/memstat을 사용하여 swapi in 및 out 및 가상 메모리 효율성을 확인하십시오. 복사본이 페이지를 여유 공간으로 밀어 넣거나 읽는 등 많은 페이지를 밀어 내야하는 경우가 있습니다.

    그러면 문제는 복사본에 사용하는 것이 아니라 시스템에서 사용하는 방법입니다. 기억. 파일 캐시를 줄이거 나 이전에 쓰기를 시작하거나 메모리에 페이지를 잠글 수 있습니다.

    6

    실제로 memcpy는 특히 여러 번 호출하는 경우 가장 빠른 방법이 아닙니다. 또한 속도를 높이기 위해 정말로 필요한 코드가 있었고, 불필요한 검사가 너무 많아서 memcpy가 느립니다. 예를 들어 대상 메모리 블록과 소스 메모리 블록이 겹쳐 있는지, 그리고 정면이 아닌 블록의 뒷면에서 복사를 시작해야하는지 확인합니다. 그러한 고려 사항에 대해 신경 쓰지 않는다면 분명히 훨씬 더 잘할 수 있습니다. 몇 가지 코드가 있지만 여기에 아마도 더 좋은 버전이 있습니다.

    Very fast memcpy for image processing?.

    검색하면 다른 구현도 찾을 수 있습니다. 그러나 진정한 속도를 위해서는 어셈블리 버전이 필요합니다.

    +0

    나는 sse2를 사용하여 이것과 비슷한 코드를 시도했다. 내 amd 시스템에서는 내장 된 것보다 4 배 빨라졌습니다. 당신이 그것을 도울 수 있다면 항상 복사하지 않는 것이 좋습니다. – Matt

    +0

    'memmove'는 중첩을 검사하고 처리해야하지만,'memcpy'는 그렇게 할 필요가 없습니다. 큰 문제는 큰 블록을 복사 할 때 효율적이기 위해서'memcpy '의 구현은 작업을 시작하기 전에 복사 접근법을 선택해야한다는 것이다. 코드가 임의의 바이트 수를 복사 할 수 있어야하지만 그 수는 시간의 90 %, 시간의 두 번 9 %, 시간의 세 번 0.9 % 및'count'의 값이됩니다. 'dest', 그리고'src'는 그 다음에 필요하지 않을 것입니다.'if (count) do * dest + = * src; while (- count> 0);은 "더 똑똑한"루틴보다 좋을 수 있습니다. – supercat

    +0

    일부 임베디드 시스템에서, 'memcpy'가 가장 빠른 접근법이 아닐 수도있는 또 다른 이유는 DMA 컨트롤러가 때때로 CPU보다 적은 오버 헤드로 메모리 블록을 복사 할 수 있지만 복사본을 만드는 가장 효율적인 방법입니다 DMA를 시작한 다음 DMA가 실행되는 동안 다른 처리를 수행하는 것일 수 있습니다. 별도의 프론트 엔드 코드와 데이터 버스가있는 시스템에서는 CPU가 다른 데이터 버스를 필요로하지 않을 때마다 모든 사이클에서 데이터를 복사하도록 DMA를 구성 할 수 있습니다. 이는 ... – supercat

    1

    코드에 대해 생성 된 어셈블리 코드를 확인해야합니다. 원하지 않는 것은 memcpy 호출이 표준 라이브러리의 memcpy 함수에 대한 호출을 생성하도록하는 것입니다. 원하는 것은 가장 큰 양의 데이터를 복사하기 위해 최상의 ASM 명령을 반복적으로 호출하는 것입니다 (예 : rep movsq).

    어떻게이 작업을 수행 할 수 있습니까? 음, 컴파일러는 memcpy에 대한 호출을 얼마나 많은 데이터를 복사해야하는지 알고있는 한 mov으로 바꾸어 호출을 최적화합니다. 잘 결정된 (constexpr) 값을 가진 memcpy을 쓰면 이것을 볼 수 있습니다. 컴파일러가 값을 모르는 경우 memcpy의 바이트 수준 구현으로 폴백해야합니다. 즉, memcpy은 1 바이트 단위를 준수해야한다는 점입니다. 그것은 한 번에 128 비트 씩 움직일 것입니다.하지만 128b가 지나면 128b로 복사 할 충분한 데이터가 있는지 확인해야합니다. 그렇지 않으면 64 비트로, 32 비트와 8로 폴백해야합니다 (16 비트는 차선책입니다) 어쨌든, 나는 잘 모르겠다.)

    그래서 원하는 것은 memcpy에 컴파일러가 최적화 할 수있는 const식이있는 데이터 크기를 알릴 수 있습니다.이 방법으로 memcpy에 대한 호출이 수행되지 않습니다. 당신이 원하지 않는 것은 실행시에만 알려질 변수 인 memcpy을 전달하는 것입니다. 이것은 최상의 호출 명령어를 확인하기위한 함수 호출과 수많은 테스트로 해석됩니다. 때로는 단순한 for 루프가 memcpy보다 낫습니다 (하나의 함수 호출 제거). 그리고 무엇 당신은 정말로 정말로 원하지 않는다memcpy에 전달할 홀수 바이트로 복사됩니다.

    6

    이것은 AVX2 명령어 세트가있는 x86_64에 대한 대답입니다. SIMD가있는 ARM/AArch64에도 비슷한 내용이 적용될 수 있습니다.

    단일 메모리 채널이 완전히 채워진 Ryzen 1800X (2 슬롯, 각각 16GB DDR4)에서 다음 코드는 MSVC++ 2017 컴파일러에서 memcpy()보다 1.56 배 빠릅니다. 두 메모리 채널을 모두 2 개의 DDR4 모듈로 채우는 경우, 즉 4 개의 DDR4 슬롯이 모두 사용 중이면 메모리 복사 속도가 2 배 빨라질 수 있습니다. 트리플 (쿼드) 채널 메모리 시스템의 경우, 코드가 유사한 AVX512 코드로 확장되는 경우 1.5 (2.0) 배 더 빠른 메모리 복사를 얻을 수 있습니다. 모든 슬롯이 Busy 인 AVX2 전용 triple/quad 채널 시스템은 완전히로드하기 때문에 한 번에 32 바이트를로드/저장할 필요가 있습니다 (쿼드 채널의 경우 트리플 및 64 바이트의 경우 48 바이트) 시스템), AVX2는 동시에 32 바이트를로드/저장할 수 있습니다. 일부 시스템의 멀티 스레딩은 AVX512 또는 AVX2 없이도이를 완화 할 수 있습니다.

    크기가 32의 배수이고 블록이 32 바이트로 정렬 된 큰 메모리 블록을 복사한다고 가정하는 복사 코드는 다음과 같습니다.

    다중 크기가 아닌 블록의 경우, 블록 헤드 및 테일에 대해 한 줄에 16 (SSE4.1), 8, 4, 2 및 마지막으로 1 바이트로 너비를 줄이는 프롤로그/에필로그 코드를 작성할 수 있습니다 . 또한 가운데에서 2-3 __m256i 값의 로컬 배열은 소스에서 정렬 된 읽기와 대상에 정렬 된 쓰기 사이의 프록시로 사용될 수 있습니다. CPU 캐시 (_stream_없이 즉, AVX 명령어가 사용되는) 관련된 경우, 복사 속도는 내 시스템에 여러 번 떨어 :

    #include <immintrin.h> 
    #include <cstdint> 
    /* ... */ 
    void fastMemcpy(void *pvDest, void *pvSrc, size_t nBytes) { 
        assert(nBytes % 32 == 0); 
        assert((intptr_t(pvDest) & 31) == 0); 
        assert((intptr_t(pvSrc) & 31) == 0); 
        const __m256i *pSrc = reinterpret_cast<const __m256i*>(pvSrc); 
        __m256i *pDest = reinterpret_cast<__m256i*>(pvDest); 
        int64_t nVects = nBytes/sizeof(*pSrc); 
        for (; nVects > 0; nVects--, pSrc++, pDest++) { 
        const __m256i loaded = _mm256_stream_load_si256(pSrc); 
        _mm256_stream_si256(pDest, loaded); 
        } 
        _mm_sfence(); 
    } 
    

    이 코드의 주요 기능은 복사가 때 CPU 캐시를 건너 뛰고 있다는 것입니다.

    내 DDR4 메모리는 2.6GHz CL13입니다. 이러한 측정에서 입력 및 출력 버퍼의 총 사이즈가 경과 된 시간 (초)로 나눈 것으로

    memcpy(): 17 208 004 271 bytes/sec. 
    Stream copy: 26 842 874 528 bytes/sec. 
    

    참고 : 또 하나 개의 배열에서 8GB의 데이터를 복사 할 때 그래서 다음의 속도를 얻었다. 배열의 각 바이트에는 2 개의 메모리 액세스가 있기 때문에 하나는 입력 배열에서 바이트를 읽고, 다른 하나는 출력 배열에 바이트를 씁니다. 즉, 한 어레이에서 다른 어레이로 8GB를 복사 할 때 16GB의 메모리 액세스 작업을 수행합니다.

    보통 멀티 스레드는 성능을 약 1.44 배 향상시킬 수 있으므로 memcpy() 이상의 총 증가율은 내 컴퓨터에서 2.55 배에 이릅니다. 다음은 스트림 복사 성능을 내 컴퓨터에서 사용되는 스레드의 수에 따라 달라집니다 방법은 다음과 같습니다

    Stream copy 1 threads: 27114820909.821 bytes/sec 
    Stream copy 2 threads: 37093291383.193 bytes/sec 
    Stream copy 3 threads: 39133652655.437 bytes/sec 
    Stream copy 4 threads: 39087442742.603 bytes/sec 
    Stream copy 5 threads: 39184708231.360 bytes/sec 
    Stream copy 6 threads: 38294071248.022 bytes/sec 
    Stream copy 7 threads: 38015877356.925 bytes/sec 
    Stream copy 8 threads: 38049387471.070 bytes/sec 
    Stream copy 9 threads: 38044753158.979 bytes/sec 
    Stream copy 10 threads: 37261031309.915 bytes/sec 
    Stream copy 11 threads: 35868511432.914 bytes/sec 
    Stream copy 12 threads: 36124795895.452 bytes/sec 
    Stream copy 13 threads: 36321153287.851 bytes/sec 
    Stream copy 14 threads: 36211294266.431 bytes/sec 
    Stream copy 15 threads: 35032645421.251 bytes/sec 
    Stream copy 16 threads: 33590712593.876 bytes/sec 
    

    코드는 다음과 같습니다

    void AsyncStreamCopy(__m256i *pDest, const __m256i *pSrc, int64_t nVects) { 
        for (; nVects > 0; nVects--, pSrc++, pDest++) { 
        const __m256i loaded = _mm256_stream_load_si256(pSrc); 
        _mm256_stream_si256(pDest, loaded); 
        } 
    } 
    
    void BenchmarkMultithreadStreamCopy(double *gpdOutput, const double *gpdInput, const int64_t cnDoubles) { 
        assert((cnDoubles * sizeof(double)) % sizeof(__m256i) == 0); 
        const uint32_t maxThreads = std::thread::hardware_concurrency(); 
        std::vector<std::thread> thrs; 
        thrs.reserve(maxThreads + 1); 
    
        const __m256i *pSrc = reinterpret_cast<const __m256i*>(gpdInput); 
        __m256i *pDest = reinterpret_cast<__m256i*>(gpdOutput); 
        const int64_t nVects = cnDoubles * sizeof(*gpdInput)/sizeof(*pSrc); 
    
        for (uint32_t nThreads = 1; nThreads <= maxThreads; nThreads++) { 
        auto start = std::chrono::high_resolution_clock::now(); 
        lldiv_t perWorker = div((long long)nVects, (long long)nThreads); 
        int64_t nextStart = 0; 
        for (uint32_t i = 0; i < nThreads; i++) { 
         const int64_t curStart = nextStart; 
         nextStart += perWorker.quot; 
         if ((long long)i < perWorker.rem) { 
         nextStart++; 
         } 
         thrs.emplace_back(AsyncStreamCopy, pDest + curStart, pSrc+curStart, nextStart-curStart); 
        } 
        for (uint32_t i = 0; i < nThreads; i++) { 
         thrs[i].join(); 
        } 
        _mm_sfence(); 
        auto elapsed = std::chrono::high_resolution_clock::now() - start; 
        double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count(); 
        printf("Stream copy %d threads: %.3lf bytes/sec\n", (int)nThreads, cnDoubles * 2 * sizeof(double)/nSec); 
    
        thrs.clear(); 
        } 
    }