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


256

할당 된 메모리를 초기화한다는 점과 calloc는 다른 것으로 알려져 malloc있습니다. 을 사용 calloc하면 메모리가 0으로 설정됩니다. 를 사용 malloc하면 메모리가 지워지지 않습니다.

그래서 일상적인 작업에서는 +로 간주 calloc됩니다 . 덧붙여서, 재미로, 벤치 마크를 위해 다음 코드를 작성했습니다.mallocmemset

결과는 혼란 스럽다.

코드 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  

교체 memsetbzero(buf[i],BLOCK_SIZE)코드 2는 동일한 결과를 생성합니다.

내 질문은 :malloc+가 memset너무 느려 calloc? 어떻게 할 수 calloc있습니까?

답변:


455

짧은 버전 : 항상 calloc()대신 사용하십시오 malloc()+memset(). 대부분의 경우 동일합니다. 경우에 따라 완전히 calloc()건너 뛸 수 있기 때문에 작업이 줄어 듭니다 memset(). 다른 경우에는, calloc()심지어 치트하고 메모리를 할당 할 수 없습니다! 그러나 malloc()+memset()항상 많은 양의 작업을 수행합니다.

이를 이해하려면 메모리 시스템을 약간 둘러보아야합니다.

기억의 빠른 여행

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

같은 메모리 할당 자 malloc()하고 calloc()있습니다 대부분이 메모리의 큰 수영장으로 그룹을 (KB의 100 단위에 1 바이트에서 무엇이든) 작은 할당을 촬영합니다. 예를 들어 16 바이트를 할당 malloc()하면 먼저 풀 중 하나에서 16 바이트를 가져 오려고 시도한 다음 풀이 마르면 커널에서 더 많은 메모리를 요청합니다. 그러나 프로그램 이후 약 한 번에 많은 양의 메모리를 위해 할당되는 요구하고, malloc()그리고 calloc()바로 커널에서 직접 해당 메모리를 요청합니다. 이 동작의 임계 값은 시스템에 따라 다르지만 1MiB가 임계 값으로 사용되는 것을 보았습니다.

커널은 각 프로세스에 실제 RAM을 할당하고 프로세스가 다른 프로세스의 메모리를 방해하지 않도록합니다. 이것을 메모리 보호 라고하며 , 1990 년대 이후로 일반적이지 않은 먼지였으며, 하나의 프로그램이 전체 시스템을 중단시키지 않고 충돌 할 수있는 이유입니다. 따라서 프로그램이 더 많은 메모리를 필요로 할 때 메모리를 가져갈 수는 없지만 대신 mmap()or 같은 시스템 호출을 사용하여 커널에서 메모리를 요청합니다 sbrk(). 커널은 페이지 테이블을 수정하여 각 프로세스에 RAM을 제공합니다.

페이지 테이블은 메모리 주소를 실제 물리적 RAM에 매핑합니다. 32 비트 시스템에서 프로세스 주소 0x00000000 ~ 0xFFFFFFFF는 실제 메모리가 아니라 가상 메모리의 주소입니다 . 프로세서는이 주소를 4 KiB 페이지로 나누고 페이지 테이블을 수정하여 각 페이지를 다른 물리적 RAM에 할당 할 수 있습니다. 커널 만이 페이지 테이블을 수정할 수 있습니다.

작동하지 않는 방법

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

  1. 프로세스가 calloc()256 MiB를 호출 하고 요청합니다.

  2. 표준 라이브러리는 mmap()256 MiB를 호출 하고 요청합니다.

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

  4. 표준 라이브러리는 RAM을 0으로 memset()만들고에서 반환합니다 calloc().

  5. 프로세스는 결국 종료되고 커널은 RAM을 회수하여 다른 프로세스에서 사용할 수 있습니다.

실제로 작동하는 방식

위의 프로세스는 작동하지만 이런 식으로 발생하지는 않습니다. 세 가지 주요 차이점이 있습니다.

  • 프로세스가 커널에서 새 메모리를 가져 오면 이전에 다른 프로세스에서 해당 메모리를 사용한 것 같습니다. 이것은 보안 위험입니다. 해당 메모리에 암호, 암호화 키 또는 비밀 살사 레시피가있는 경우 어떻게해야합니까? 중요한 데이터 유출을 막기 위해 커널은 메모리를 프로세스에 제공하기 전에 항상 제거합니다. 메모리를 0으로 만들어서 스크러빙 할 수 있으며, 새 메모리가 0으로 설정되면이를 mmap()보장 할 수 있으므로 반환하는 새 메모리가 항상 0이 되도록 보장합니다.

  • 메모리를 할당하지만 메모리를 즉시 사용하지 않는 많은 프로그램이 있습니다. 때때로 메모리는 할당되지만 사용되지는 않습니다. 커널은 이것을 알고 게으르다. 새 메모리를 할당하면 커널은 페이지 테이블을 전혀 건드리지 않으며 프로세스에 RAM을 제공하지 않습니다. 대신 프로세스에서 일부 주소 공간을 찾고 거기에 가야 할 내용을 기록하고 프로그램에서 실제로 사용하는 경우 RAM을 배치 할 것이라고 약속합니다. 프로그램이 해당 주소에서 읽거나 쓰려고하면 프로세서가 페이지 오류를 트리거 하고 커널이 해당 주소에 RAM을 할당하고 프로그램을 다시 시작합니다. 메모리를 사용하지 않으면 페이지 오류가 발생하지 않으며 프로그램에서 실제로 RAM을 얻지 못합니다.

  • 일부 프로세스는 메모리를 할당 한 다음 수정하지 않고 메모리에서 읽습니다. 이는 서로 다른 프로세스에서 메모리의 많은 페이지가에서 반환 된 초기 0으로 채워질 수 있음을 의미 mmap()합니다. 이러한 페이지는 모두 동일하므로 커널은 이러한 모든 가상 주소가 0으로 채워진 단일 공유 4 KiB 페이지의 메모리를 가리 키도록합니다. 해당 메모리에 쓰려고하면 프로세서가 다른 페이지 오류를 트리거하고 커널이 다른 프로그램과 공유되지 않는 새 0 페이지를 제공합니다.

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

  1. 프로세스가 calloc()256 MiB를 호출 하고 요청합니다.

  2. 표준 라이브러리는 mmap()256 MiB를 호출 하고 요청합니다.

  3. 커널은 256MiB의 사용되지 않는 주소 공간을 찾아서 해당 주소 공간이 무엇에 사용되는지 기록하고 반환합니다.

  4. 표준 라이브러리의 결과 것을 알고 mmap()항상 제로로 가득이 (또는 이 메모리에 접촉하지 않도록, 그래서 아무 페이지 오류입니다, 그리고 RAM을 프로세스에 제공되지 않습니다, 실제로 어떤 RAM을 얻으면) .

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

당신이 사용하는 경우 memset()페이지를 제로로, memset()램이 할당되도록 (듯이), 페이지 폴트를 유발하고 이미 제로 가득하더라도 그것을 제로 것입니다. 이것은 엄청난 양의 추가 작업이며 왜 및 calloc()보다 빠릅니다 . 끝 어쨌든 메모리를 사용하여, 경우 여전히 빠르고보다 하고 있지만, 차이는 꽤 말도하지 않습니다.malloc()memset()calloc()malloc()memset()


항상 작동하지는 않습니다

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

이것은 또한 더 작은 할당으로 작동하지 않을 수도 있습니다. 할당량이 적 으면 calloc()커널로 직접 이동하지 않고 공유 풀에서 메모리를 가져옵니다. 일반적으로 공유 풀에는와 함께 사용 및 해제 된 오래된 메모리에서 정크 데이터가 저장 free()되어 calloc()있을 수 있으므로 해당 메모리를 가져 와서 memset()삭제하도록 호출 할 수 있습니다 . 일반적인 구현은 공유 풀의 어느 부분이 깨끗하고 여전히 0으로 채워지는지를 추적하지만 모든 구현이이를 수행하지는 않습니다.

몇 가지 오답 해소

운영 체제에 따라 나중에 비어있는 메모리를 확보해야 할 경우 커널은 사용 가능한 시간에 메모리를 0으로 만들거나 그렇지 않을 수 있습니다. 리눅스는 미리 메모리를 제로화하지 않으며, Dragonfly BSD는 최근 커널에서이 기능을 제거했습니다 . 그러나 일부 다른 커널은 미리 메모리를 0으로 만듭니다. 유휴 상태의 zeroing 페이지는 큰 성능 차이를 설명하기에 충분하지 않습니다.

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()이미 제로되어 메모리를 영점 조정되어 메모리가 두 번 제로가됩니다 것을 의미하지,하지만 그건 단지 배의 성능 차이를 설명합니다. 여기에서의 성능 차이는 훨씬 큽니다 (내 시스템에서 malloc()+memset()와 사이의 크기가 3보다 큰 것을 측정했습니다 calloc()).

파티 트릭

10 회 반복하는 대신 NULL을 반환 malloc()하거나 calloc()NULL을 반환 할 때까지 메모리를 할당하는 프로그램을 작성하십시오 .

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


7
@ Dietrich : calloc에 ​​동일한 0으로 채워진 페이지를 여러 번 할당하는 OS에 대한 Dietrich의 가상 메모리 설명은 쉽게 확인할 수 있습니다. 할당 된 모든 메모리 페이지에 정크 데이터를 쓰는 루프를 추가하십시오 (500 바이트마다 1 바이트를 쓰는 것으로 충분합니다). 시스템이 두 경우 모두 다른 페이지를 실제로 할당해야하므로 전체 결과는 훨씬 가까워 져야합니다.
kriss

1
@kriss : 실제로, 모든 4096 바이트 비록 하나는 시스템의 대부분에 충분하다
디트리히 엡

실제로 calloc()는 종종 malloc구현 스위트의 일부 이므로 에서 메모리를 가져올 때 호출 하지 않도록 최적화됩니다 . bzerommap
mirabilos

1
편집 해 주셔서 감사합니다. 거의 마음에 듭니다. 초기에 malloc + memset 대신 항상 calloc을 사용한다고 언급했습니다. 버퍼의 작은 부분을 0으로 만들어야하는 경우 해당 부분 3을 memset하십시오. 그렇지 않으면 calloc을 사용하십시오. 특히 전체 크기를 malloc + memset하지 말고 (calloc을 사용하십시오) valgrind 및 정적 코드 분석기와 같은 것을 방해하므로 모든 것을 callocing하지 마십시오 (모든 메모리가 갑자기 초기화됩니다). 그 외에는 이것이 괜찮다고 생각합니다.
직원

5
속도와 관련이 없지만 calloc버그가 덜 발생합니다. 즉, large_int * large_int오버플 로 가 발생하는 곳 은을 calloc(large_int, large_int)반환 NULL하지만 반환 malloc(large_int * large_int)되는 메모리 블록의 실제 크기를 알지 못하므로 정의되지 않은 동작입니다.
Dunes

12

많은 시스템에서 여분의 처리 시간에 OS는 여유 메모리를 자체적으로 0으로 설정하고 안전한 것으로 표시하기 calloc()때문에 호출 할 때 calloc()이미 여유 메모리가 0이 될 수 있습니다.


2
확실합니까? 어떤 시스템이이 작업을 수행합니까? 필자는 대부분의 OS가 유휴 상태 일 때 프로세서를 종료하고 메모리에 쓰 자마자 할당 된 프로세스에 대해 필요에 따라 메모리를 제로화한다고 생각했지만 할당 할 때는 그렇지 않았습니다.
Dietrich Epp

@Dietrich-확실하지 않습니다. 나는 그것을 한 번 들었고 calloc()더 효율적 으로 만드는 합리적이고 합리적인 방법처럼 보였다 .
Chris Lutz

@Pierreten- calloc()특정 최적화 에 대한 좋은 정보를 찾을 수 없으며 OP의 libc 소스 코드를 해석하고 싶지 않습니다. 이 최적화가 존재하지 않거나 작동하지 않음을 나타내는 것을 찾을 수 있습니까?
Chris Lutz

13
@Dietrich : FreeBSD는 유휴 시간에 페이지를 0으로 채 웁니다. vm.idlezero_enable 설정을 참조하십시오.
Zan Lynx

1
@DietrichEpp necro에 죄송하지만 예를 들어 Windows 가이 작업을 수행합니다.
Andreas Grapentin

1

일부 모드의 일부 플랫폼에서 malloc은 메모리를 반환하기 전에 일반적으로 0이 아닌 값으로 메모리를 초기화하므로 두 번째 버전은 메모리를 두 번 초기화 할 수 있습니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.