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

할당 된 메모리를 초기화한다는 점과 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있습니까?



답변

짧은 버전 : 항상 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()?


답변

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


답변

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


답변