programing

메모리셋을 사용하는 것보다 메모리를 제로화하는 속도가 더 빠릅니까?

firstcheck 2022. 8. 7. 16:28
반응형

메모리셋을 사용하는 것보다 메모리를 제로화하는 속도가 더 빠릅니까?

는 그것을 .memset(ptr, 0, nbytes)정말 빠르지만 (적어도 x86에서는) 더 빠른 방법이 있을까요?

은 memset을 사용하고 있을 입니다.mov하는 경우 는 ""를 사용합니다.xor더 빠를수록 좋겠죠?edit1: GregS가 지적한 바와 같이 레지스터에서만 동작합니다.나는 무슨 생각을 했을까요?

또, 저보다 어셈블러를 잘 아는 사람에게 stdlib를 봐달라고 부탁했더니, x86 memset에서는 32비트 와이드 레지스터를 충분히 활용하고 있지 않다고 합니다.하지만 그때는 너무 피곤해서 제대로 이해했는지 잘 모르겠어요.

edit2: 이 문제를 재검토하여 테스트를 실시했습니다.테스트한 내용은 다음과 같습니다.

    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>
    #include <sys/time.h>

    #define TIME(body) do {                                                     \
        struct timeval t1, t2; double elapsed;                                  \
        gettimeofday(&t1, NULL);                                                \
        body                                                                    \
        gettimeofday(&t2, NULL);                                                \
        elapsed = (t2.tv_sec - t1.tv_sec) * 1000.0 + (t2.tv_usec - t1.tv_usec) / 1000.0; \
        printf("%s\n --- %f ---\n", #body, elapsed); } while(0)                 \


    #define SIZE 0x1000000

    void zero_1(void* buff, size_t size)
    {
        size_t i;
        char* foo = buff;
        for (i = 0; i < size; i++)
            foo[i] = 0;

    }

    /* I foolishly assume size_t has register width */
    void zero_sizet(void* buff, size_t size)
    {
        size_t i;
        char* bar;
        size_t* foo = buff;
        for (i = 0; i < size / sizeof(size_t); i++)
            foo[i] = 0;

        // fixes bug pointed out by tristopia
        bar = (char*)buff + size - size % sizeof(size_t);
        for (i = 0; i < size % sizeof(size_t); i++)
            bar[i] = 0;
    }

    int main()
    {
        char* buffer = malloc(SIZE);
        TIME(
            memset(buffer, 0, SIZE);
        );
        TIME(
            zero_1(buffer, SIZE);
        );
        TIME(
            zero_sizet(buffer, SIZE);
        );
        return 0;
    }

결과:

0_1이 -O3을 제외하고 가장 느립니다. -O1, -O2, -O3에서 제로_sizet이 가장 빠르고 성능이 거의 동일합니다. memset은 항상 제로_sizet보다 느렸습니다.(-O3의 경우 속도가 2배 느림).한 가지 흥미로운 점은 -O3 0_1에서 0_sizet과 동일하게 빠르다는 것입니다.그러나 분해된 기능은 대략 4배의 명령어를 가지고 있었습니다(루프 언롤이 원인인 것 같습니다).또한 zero_sizet을 더 최적화하려고 했지만 컴파일러가 항상 나를 앞섰지만 여기서는 놀랄 일이 아니다.

현재 memset의 승리에서는 이전 결과는 CPU 캐시에 의해 왜곡되었습니다.(모든 테스트는 Linux에서 실행) 추가 테스트가 필요.다음으로 어셈블러를 시험합니다. : )

edit3: 테스트 코드의 버그 수정, 테스트 결과는 영향을 받지 않습니다.

edit4: 분해된 VS2010 C 런타임에 대해 조사하던 중memsetSSE 0으로 지정됩니다.이걸 이기기는 어려울 거야.

x86은 비교적 광범위한 디바이스입니다.

완전히 일반적인 x86 타깃의 경우, "rep movsd"가 있는 어셈블리 블록은 한 번에 32비트로 0을 출력할 수 있습니다.이 작업의 대부분이 DWORD에 맞춰져 있는지 확인하십시오.

mmx를 가진 칩의 경우 movq를 가진 어셈블리 루프는 한 번에 64비트를 히트시킬 수 있습니다.

C/C++ 컴파일러가 롱 또는 _m64 포인터와 함께 64비트 쓰기를 사용하도록 할 수 있습니다.최상의 성능을 얻으려면 타겟이 8바이트 정렬되어야 합니다.

sse를 가진 칩의 경우, movaps는 빠르지만 주소가 16바이트 정렬된 경우에만 movsb를 사용하여 정렬된 후 movsb를 사용하여 movaps 루프를 사용하여 클리어합니다.

Win32는 "Zero Memory()"를 가지고 있지만, 그것이 memset에 대한 매크로인지 아니면 실제 "좋은" 구현인지 잊어버립니다.

memset는 일반적으로 매우 빠른 범용 설정/제로 코드를 사용하도록 설계되었습니다.크기와 배열이 다른 모든 사례를 처리하며, 작업에 사용할 수 있는 명령의 종류에 영향을 미칩니다.사용하고 있는 시스템(및 stdlib의 벤더)에 따라서는, 그 아키텍처 고유의 어셈블러에 실장되어 그 네이티브 속성이 무엇이든 활용할 수 있습니다.또, 제로잉(다른 값의 설정과는 반대로)의 경우를 처리하기 위해서, 내부적인 특수한 케이스가 있는 경우도 있습니다.

구체적이고 있는 제로잉을 할 수 memset구현에 도움이 됩니다. memset

memset 기능은 속도를 희생하더라도 유연하고 단순하도록 설계되었습니다.많은 구현에서는 지정된 값을 지정된 바이트 수에 걸쳐 한 번에1 바이트씩 복사하는 단순한 while 루프입니다.보다 고속의 memset(또는 memcpy, memmove 등)을 필요로 하는 경우는, 거의 항상 스스로 코드 업 할 수 있습니다.

가장 간단한 커스터마이즈는, 행선지 주소가 32비트 또는 64비트로 정렬될 때까지(칩의 아키텍처와 일치하는 것) 싱글 바이트의 「set」 조작을 실행해, CPU 레지스터 전체를 한 번에 카피하는 것입니다.범위가 정렬된 주소로 끝나지 않으면 마지막에 싱글바이트 "set" 작업을 몇 개 수행해야 할 수 있습니다.

사용하시는 CPU에 따라서는 스트리밍 SIMD 명령이 준비되어 있을 수 있습니다.이러한 방법은 일반적으로 정렬된 주소에서 더 잘 작동하므로, 정렬된 주소를 사용하는 위의 기술은 여기서도 유용합니다.

메모리의 큰 부분을 제로 아웃 하는 경우는, 범위를 섹션으로 분할해 각 섹션을 병렬로 처리함으로써(섹션의 수는 코어/하드웨어 스레드 수와 같음) 고속화를 확인할 수도 있습니다.

가장 중요한 것은, 당신이 시도하지 않는 한, 이것들 중 어떤 것이 도움이 되는지 알 수 있는 방법이 없다는 것입니다.적어도 각 케이스에 대해 컴파일러가 출력하는 내용을 확인합니다.표준 'memset'에 대해 다른 컴파일러가 무엇을 방출하는지 확인하십시오(이러한 컴파일러의 구현이 컴파일러보다 효율적일 수 있습니다).

요즘은 컴파일러가 모든 작업을 대신해야 합니다. 있는 는 "gcc"에 대한 하는 데 매우 효율적입니다.memset(어느쪽인가 하면)

또 피하세요.memset"이것들"은 다음과 같습니다.

  • 히프 메모리에 calloc 사용
  • 초기화를... = { 0 }

정말 큰 쓰세요.mmap츠키노이것은, 시스템으로부터 「무료」로 초기화된 메모리를 제로 취득합니다.

특별한 요구가 있거나 컴파일러/stdlib이 형편없다는 것을 모르는 한 memset을 계속 사용합니다.범용이며, 일반적으로 성능이 우수해야 합니다.또한 컴파일러는 memset()을 본질적으로 지원할 수 있기 때문에 memset()을 최적화/인라인하는 데 더 시간이 걸릴 수 있습니다.

예를 들어 Visual C++는 라이브러리 함수에 대한 호출만큼 작은 memcpy/memset 인라인 버전을 생성하므로 푸시/콜/ret 오버헤드를 피할 수 있습니다.또한 컴파일 시 size 파라미터를 평가할 수 있는 경우에는 최적화가 더욱 가능합니다.

즉, 특정 요구가 있는 경우(크기가 항상 작거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 크거나 하면예를 들어, L2 캐시를 오염시키지 않고 대용량 메모리 청크를 제로화하는 쓰기 작업을 사용합니다.

단, 모든 것은 상황에 따라 다릅니다.일반적인 것은 memset/memcpy로 해 주세요:)

제 기억이 맞다면 (몇 년 전) 선임 개발자 중 한 명이 PowerPC에서 bzero()를 빠르게 실행할 수 있는 방법에 대해 이야기하고 있었습니다(스펙은 전원을 켤 때 거의 모든 메모리를 제로화해야 한다고 말했습니다).x86으로 잘 번역되지 않을 수도 있지만 검토할 가치가 있을 수 있습니다.

이 아이디어는 데이터 캐시 라인을 로드하고 데이터 캐시 라인을 클리어한 다음 클리어된 데이터 캐시 라인을 메모리에 다시 쓰는 것이었습니다.

어쨌든 도움이 됐으면 좋겠어요.

이 훌륭하고 유용한 테스트에는 다음과 같은 치명적인 결함이 있습니다.memset은 첫 번째 명령이기 때문에 메모리 오버헤드 등이 있어 매우 느립니다.memset의 타이밍을 2위로 이동하거나 다른 것을 1위로 이동하거나 단순히 memset의 타이밍을 2회 설정하는 것만으로 모든 컴파일 스위치에서 가장 빠른 memset이 됩니다!!!

그것은 재미 있는 질문입니다.VC++ 2012에서 32비트 릴리스를 컴파일할 때는 약간 고속이지만 측정이 어려운 실장을 실시했습니다.아마 많이 개선할 수 있을 거예요.멀티스레드 환경에서 이 기능을 클래스에 추가하면 병목현상이 보고되므로 퍼포먼스가 더욱 향상될 수 있습니다.memset()멀티스레드 시나리오에서는요.

// MemsetSpeedTest.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
#include "Windows.h"
#include <time.h>

#pragma comment(lib, "Winmm.lib") 
using namespace std;

/** a signed 64-bit integer value type */
#define _INT64 __int64

/** a signed 32-bit integer value type */
#define _INT32 __int32

/** a signed 16-bit integer value type */
#define _INT16 __int16

/** a signed 8-bit integer value type */
#define _INT8 __int8

/** an unsigned 64-bit integer value type */
#define _UINT64 unsigned _INT64

/** an unsigned 32-bit integer value type */
#define _UINT32 unsigned _INT32

/** an unsigned 16-bit integer value type */
#define _UINT16 unsigned _INT16

/** an unsigned 8-bit integer value type */
#define _UINT8 unsigned _INT8

/** maximum allo

wed value in an unsigned 64-bit integer value type */
    #define _UINT64_MAX 18446744073709551615ULL

#ifdef _WIN32

/** Use to init the clock */
#define TIMER_INIT LARGE_INTEGER frequency;LARGE_INTEGER t1, t2;double elapsedTime;QueryPerformanceFrequency(&frequency);

/** Use to start the performance timer */
#define TIMER_START QueryPerformanceCounter(&t1);

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP QueryPerformanceCounter(&t2);elapsedTime=(t2.QuadPart-t1.QuadPart)*1000.0/frequency.QuadPart;wcout<<elapsedTime<<L" ms."<<endl;
#else
/** Use to init the clock */
#define TIMER_INIT clock_t start;double diff;

/** Use to start the performance timer */
#define TIMER_START start=clock();

/** Use to stop the performance timer and output the result to the standard stream. Less verbose than \c TIMER_STOP_VERBOSE */
#define TIMER_STOP diff=(clock()-start)/(double)CLOCKS_PER_SEC;wcout<<fixed<<diff<<endl;
#endif    


void *MemSet(void *dest, _UINT8 c, size_t count)
{
    size_t blockIdx;
    size_t blocks = count >> 3;
    size_t bytesLeft = count - (blocks << 3);
    _UINT64 cUll = 
        c 
        | (((_UINT64)c) << 8 )
        | (((_UINT64)c) << 16 )
        | (((_UINT64)c) << 24 )
        | (((_UINT64)c) << 32 )
        | (((_UINT64)c) << 40 )
        | (((_UINT64)c) << 48 )
        | (((_UINT64)c) << 56 );

    _UINT64 *destPtr8 = (_UINT64*)dest;
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr8[blockIdx] = cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 2;
    bytesLeft = bytesLeft - (blocks << 2);

    _UINT32 *destPtr4 = (_UINT32*)&destPtr8[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr4[blockIdx] = (_UINT32)cUll;

    if (!bytesLeft) return dest;

    blocks = bytesLeft >> 1;
    bytesLeft = bytesLeft - (blocks << 1);

    _UINT16 *destPtr2 = (_UINT16*)&destPtr4[blockIdx];
    for (blockIdx = 0; blockIdx < blocks; blockIdx++) destPtr2[blockIdx] = (_UINT16)cUll;

    if (!bytesLeft) return dest;

    _UINT8 *destPtr1 = (_UINT8*)&destPtr2[blockIdx];
    for (blockIdx = 0; blockIdx < bytesLeft; blockIdx++) destPtr1[blockIdx] = (_UINT8)cUll;

    return dest;
}

int _tmain(int argc, _TCHAR* argv[])
{
    TIMER_INIT

    const size_t n = 10000000;
    const _UINT64 m = _UINT64_MAX;
    const _UINT64 o = 1;
    char test[n];
    {
        cout << "memset()" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                memset((void*)test, 0, n);  

        TIMER_STOP;
    }
    {
        cout << "MemSet() took:" << endl;
        TIMER_START;

        for (int i = 0; i < m ; i++)
            for (int j = 0; j < o ; j++)
                MemSet((void*)test, 0, n);

        TIMER_STOP;
    }

    cout << "Done" << endl;
    int wait;
    cin >> wait;
    return 0;
}

32비트 시스템의 릴리스 컴파일의 출력은 다음과 같습니다.

memset() took:
5.569000
MemSet() took:
5.544000
Done

64비트 시스템의 릴리스 컴파일의 출력은 다음과 같습니다.

memset() took:
2.781000
MemSet() took:
2.765000
Done

여기서 버클리 소스코드를 찾을 수 있어요memset()이치노

memset은 컴파일러에 의해 일련의 효율적인 opcode로 삽입되어 몇 사이클 동안 전개될 수 있습니다.4000x2000 64비트 프레임버퍼와 같은 매우 큰 메모리 블록의 경우 여러 스레드(단일 태스크에 대비)에 걸쳐 최적화를 시도할 수 있습니다.각각은 독자적인 부품을 설정합니다.bzero()도 있지만 memset만큼 더 불분명하고 최적화되지 않을 가능성이 높기 때문에 컴파일러는 반드시 0을 넘길 것입니다.

큰 블록의 단순히 큰 블록을 하는 입니다.따라서 작은 블록에서는 단순히 다음과 같이 하는 것이 효율적입니다.*(uint64_t*)p = 0이치노

일반적으로 모든 x86 CPU는 (일부 표준화된 플랫폼용으로 컴파일하지 않는 한) 다르며, Pentium 2용으로 최적화한 것은 Core Duo 또는 i486에서 다르게 동작합니다.따라서 치약의 마지막 몇 조각을 짜내고 싶다면 exe가 컴파일하여 다른 인기 CPU 모델에 최적화된 여러 버전을 출하하는 것이 좋습니다.개인 경험상 Clang - march=march는 게임의 FPS를 60에서 65로 늘렸습니다.

언급URL : https://stackoverflow.com/questions/3654905/faster-way-to-zero-memory-than-with-memset

반응형