programing

malloc+memset이 calloc보다 느린 이유는 무엇입니까?

goodsources 2022. 8. 16. 23:40
반응형

malloc+memset이 calloc보다 느린 이유는 무엇입니까?

인 것으로 알려져 있다calloc와는 다르다malloc할당되어 있는 메모리를 초기화합니다.와 함께calloc메모리가 0으로 설정되어 있습니다.와 함께malloc메모리는 클리어 되지 않습니다.

그래서 저는 일상 업무에서calloc~하듯이malloc+memset덧붙여서, 저는 재미삼아 다음과 같은 코드를 벤치마크로 작성했습니다.

결과가 혼란스럽다.

코드 1:

#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)calloc(1,BLOCK_SIZE);
                i++;
        }
}

코드 1의 출력:

time ./a.out  
**real 0m0.287s**  
user 0m0.095s  
sys 0m0.192s  

코드 2:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
        int i=0;
        char *buf[10];
        while(i<10)
        {
                buf[i] = (char*)malloc(BLOCK_SIZE);
                memset(buf[i],'\0',BLOCK_SIZE);
                i++;
        }
}

코드 2의 출력:

time ./a.out   
**real 0m2.693s**  
user 0m0.973s  
sys 0m1.721s  

교환memset와 함께bzero(buf[i],BLOCK_SIZE)같은 결과를 얻을 수 있습니다.

질문입니다.왜?malloc+memset보다 훨씬 느리다calloc어떻게 할 수 있습니까?calloc그렇게 해요?

쇼트 버전:항상 사용calloc()대신malloc()+memset()대부분의 경우, 이들은 동일합니다.경우에 따라서는calloc()스킵할 수 있기 때문에 작업량이 줄어듭니다.memset()전적으로.다른 경우에는calloc()커닝도 할 수 있고 메모리 할당도 할 수 없습니다.하지만,malloc()+memset()항상 모든 일을 다 할 것이다.

이를 이해하려면 메모리 시스템을 잠시 둘러봐야 합니다.

메모리의 퀵 투어

여기에는 프로그램, 표준 라이브러리, 커널 및 페이지 테이블의 4가지 주요 부분이 있습니다.당신은 이미 프로그램을 알고 있으니까...

메모리 할당자는 다음과 같습니다.malloc()그리고.calloc()대부분의 경우 작은 할당(1바이트에서 100KB까지의 임의의 할당)을 받아 큰 메모리 풀로 그룹화합니다.예를 들어 16바이트를 할당하면malloc()는 먼저 풀 중 하나에서16바이트를 취득하려고 합니다.그리고 풀이 고갈되면 커널로부터 메모리를 증설하도록 요구합니다.다만, 당신이 요구하는 프로그램은 한 번에 대량의 메모리를 할당하고 있기 때문에,malloc()그리고.calloc()커널에서 직접 메모리를 요구합니다.이 동작의 문턱값은 시스템에 따라 다르지만 문턱값으로 1MiB가 사용되고 있습니다.

커널은 실제 RAM을 각 프로세스에 할당하고 프로세스가 다른 프로세스의 메모리를 방해하지 않도록 합니다.이것은 메모리 보호라고 불리며, 1990년대부터 매우 보편화되어 왔습니다.이것이 하나의 프로그램이 시스템 전체를 다운시키지 않고 크래쉬 할 수 있는 이유입니다.따라서 프로그램이 메모리를 더 필요로 할 때 단순히 메모리를 사용할 수 있는 것이 아니라 다음과 같은 시스템 호출을 사용하여 커널로부터 메모리를 요구할 수 있습니다.mmap()또는sbrk()커널은 페이지 테이블을 수정하여 각 프로세스에 RAM을 제공합니다.

페이지 테이블은 메모리 주소를 실제 물리 RAM에 매핑합니다.프로세스의 주소인 0x00000000~32비트 시스템의 0xFFFFFF는 실제 메모리가 아니라 가상 메모리 내의 주소입니다.프로세서는 이러한 주소를 4개의 KiB 페이지로 분할하여 페이지 테이블을 수정함으로써 각 페이지를 다른 물리 RAM에 할당할 수 있습니다.페이지 테이블을 수정할 수 있는 것은 커널뿐입니다.

동작하지 않는 구조

256 MiB 할당은 다음과 같이 동작하지 않습니다.

  1. 프로세스 호출calloc()256MiB를 요구합니다.

  2. 표준 라이브러리가 호출mmap()256MiB를 요구합니다.

  3. 커널은 사용되지 않은 256 MiB RAM을 찾아 페이지 테이블을 수정하여 프로세스에 제공합니다.

  4. 표준 라이브러리는 RAM을 0으로 설정합니다.memset()및 에서 반환됩니다.calloc().

  5. 프로세스가 종료되고 커널은 RAM을 회수하여 다른 프로세스에서 사용할 수 있도록 합니다.

실제 작동 방식

위의 과정은 작동하지만, 이러한 방식으로 수행되지는 않습니다.크게 세 가지 차이점이 있습니다.

  • 프로세스가 커널로부터 새로운 메모리를 취득하면, 그 메모리는 이전에 다른 프로세스에서 사용되었을 가능성이 있습니다.이것은 보안상의 리스크입니다.해당 메모리에 암호, 암호화 키 또는 비밀 살사 레시피가 있으면 어떻게 됩니까?기밀 데이터가 유출되는 것을 방지하기 위해 커널은 프로세스를 수행하기 전에 항상 메모리를 스크럽합니다.메모리를 제로로 하는 것이 좋을지도 모릅니다.새 메모리가 제로로 되어 있으면 보증으로 할 수도 있습니다.mmap()는 반환되는 새로운 메모리가 항상 제로임을 보증합니다.

  • 메모리를 할당하지만 바로 사용하지 않는 프로그램이 많이 있습니다.메모리가 할당되어 있지만 사용되지 않는 경우가 있습니다.커널은 이것을 알고 게으릅니다.새로운 메모리를 할당할 때 커널은 페이지 테이블에 전혀 접촉하지 않고 프로세스에 RAM도 제공하지 않습니다.대신 프로세스에서 주소 공간을 찾아 그곳에 가야 할 내용을 메모하고 프로그램에서 실제로 RAM을 사용할 경우 RAM을 설치하겠다고 약속합니다.프로그램이 이러한 주소로부터 읽기 또는 쓰기를 시도하면 프로세서가 페이지 장애를 트리거하고 커널이 개입하여 RAM을 해당 주소에 할당하고 프로그램을 재개합니다.메모리를 전혀 사용하지 않으면 페이지 장애가 발생하지 않고 프로그램이 실제로 RAM을 얻을 수 없습니다.

  • 일부 프로세스에서는 메모리를 할당한 후 수정하지 않고 메모리를 읽습니다.즉, 메모리 내의 여러 프로세스에 걸쳐 많은 페이지가 0으로 채워질 수 있습니다.mmap()이 페이지들은 모두 같기 때문에 커널은 이 모든 가상 주소를 0으로 채워진 메모리의 공유 4KiB 페이지를 가리키도록 합니다.이 메모리에 쓰려고 하면 프로세서가 다른 페이지 장애를 트리거하고 커널이 개입하여 다른 프로그램과 공유되지 않는 새로운 제로 페이지가 나타납니다.

최종 프로세스는 다음과 같습니다.

  1. 프로세스 호출calloc()256MiB를 요구합니다.

  2. 표준 라이브러리가 호출mmap()256MiB를 요구합니다.

  3. 커널은 사용되지 않은 주소 공간의 256MiB를 찾아 해당 주소 공간이 현재 무엇에 사용되는지 기록한 후 반환합니다.

  4. 표준 라이브러리는 다음과 같은 결과를 알고 있습니다.mmap()는 항상 0으로 채워져 있기 때문에(또는 실제로 RAM을 갖게 되면), 메모리에 닿지 않기 때문에 페이지 폴트가 없고 RAM은 프로세스에 할당되지 않습니다.

  5. 프로세스가 종료되고 커널은 RAM을 처음부터 할당하지 않았기 때문에 RAM을 회수할 필요가 없습니다.

사용하시는 경우memset()페이지를 0으로 합니다.memset()는 페이지 장애를 트리거하고 RAM을 할당한 후 이미 0으로 채워져 있는 경우에도 0으로 설정합니다.이것은 엄청난 양의 추가 작업이며, 그 이유를 설명합니다.calloc()보다 빠르다malloc()그리고.memset()어차피 메모리를 쓰게 되면calloc()아직 보다 빠르다malloc()그리고.memset()하지만 그 차이가 그렇게 터무니없지는 않다.


이것이 항상 효과가 있는 것은 아니다.

모든 시스템에서 가상 메모리를 페이징한 것은 아니기 때문에 모든 시스템에서 이러한 최적화를 사용할 수 있는 것은 아닙니다.이는 80286과 같은 매우 오래된 프로세서 및 정교한 메모리 관리 유닛에 비해 크기가 너무 작은 임베디드 프로세서에 적용됩니다.

또, 할당량이 적은 경우에도 항상 동작하는 것은 아닙니다.할당량이 적을 경우calloc()는 커널로 직접 이동하는 대신 공유 풀에서 메모리를 가져옵니다.일반적으로 공유 풀에는 에서 사용한 오래된 메모리의 정크 데이터가 저장되어 있을 수 있습니다.free(),그렇게calloc()그 기억을 가지고 전화를 할 수 있는memset()치우기 위해서요.공통 구현에서는 공유 풀의 어떤 부분이 아직 0으로 채워져 있는지 추적하지만, 모든 구현이 이를 수행하는 것은 아닙니다.

일부 오답 해소

operating system에 따라서는, 나중에 제로 된 메모리를 입수할 필요가 있는 경우에 대비해, 커널은 빈 시간에 메모리를 제로 하거나 제로 하지 않는 경우가 있습니다.Linux는 메모리를 사전에 제로화하지 않습니다.Dragonfly BSD도 최근 커널에서 이 기능을 삭제했습니다.그러나 일부 다른 커널은 미리 메모리를 0으로 만듭니다.아이돌 중에 페이지를 0으로 하는 것만으로는 퍼포먼스의 큰 차이를 설명할 수 없습니다.

calloc()함수가 특수한 메모리 정렬 버전을 사용하지 않습니다.memset()어쨌든 그렇게 빨리 되진 않을 거야대부분의.memset()최신 프로세서의 실장은 다음과 같습니다.

function memset(dest, c, len)
    // one byte at a time, until the dest is aligned...
    while (len > 0 && ((unsigned int)dest & 15))
        *dest++ = c
        len -= 1
    // now write big chunks at a time (processor-specific)...
    // block size might not be 16, it's just pseudocode
    while (len >= 16)
        // some optimized vector code goes here
        // glibc uses SSE2 when available
        dest += 16
        len -= 16
    // the end is not aligned, so one byte at a time
    while (len > 0)
        *dest++ = c
        len -= 1

보시면 아시겠지만memset()속도가 매우 빠르며 대용량 메모리 블록으로는 더 나은 것을 얻을 수 없습니다.

이 사실은memset()이미 제로 되어 있는 메모리를 제로화하고 있는 것은 메모리가2번 제로 되어 있는 것을 의미합니다만, 2배의 퍼포먼스 차이밖에 설명되지 않습니다.퍼포먼스 차이가 훨씬 커집니다(시스템에서 측정했을 때 3배 이상의 크기를 측정했습니다).malloc()+memset()그리고.calloc()).

파티 트릭

루프를 10회 반복하는 대신 메모리를 할당하는 프로그램을 작성합니다.malloc()또는calloc()NULL을 반환합니다.

를 추가하면 어떻게 됩니까?memset()?

많은 시스템에서 OS는 여유 처리 시간에 빈 메모리를 스스로 0으로 설정하고 이를 안전한 상태로 표시하기 위해calloc()그래서 전화할 때calloc()이미 빈 제로 메모리를 탑재하고 있을 가능성이 있습니다.

일부 모드의 플랫폼에서는 malloc이 메모리를 반환하기 전에 보통 제로 이외의 값으로 초기화하기 때문에 두 번째 버전은 메모리를 2회 충분히 초기화할 수 있습니다.

언급URL : https://stackoverflow.com/questions/2688466/why-mallocmemset-is-slower-than-calloc

반응형