Search
Duplicate
🥕

메모리 풀, malloc()보다 100배 빠르다!

간단소개
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
C++
Scrap
태그
9 more properties

개요

new/delete, malloc/free 는 매우 비싼 비용이 든다. syscall인 것도 문제고, 적절한 공간을 찾고 해제 후 다른 공간과 병합하는 복잡한 과정이 있기 때문이다.
이를 개선하자는 아이디어가 메모리 풀이다. 메모리 풀은 큰 사이즈를 한 번에 malloc한 뒤, 거기서 한 칸씩 나눠주는 식으로 구현한다.
내가 node라는 구조체를 자주 할당/해제 한다고 치면, node[1000] 짜리 배열을 한 번에 malloc한 뒤 할당 요청이 오면 한 칸씩 나눠주는 간단한 원리다. 실제 구현도 굉장히 간단하다.

성능 향상은?

기존 malloc/free에 비해 2배 ~ 100배 정도 성능이 향상된다. 구현과 사이즈에 따라 편차가 클 수 있다.

C 간단 구현 FixedMemoryPool

정말 간단하다. 보고 이해하자.

Assert를 모르는 사람을 위해

그 전에 잠깐. Assert (x == 0); 이러한 코드는 디버깅 빌드에서 x == 0 이 아니면 에러를 내주는 코드이다. 조건이 맞지 않으면 에러를 내주는 디버깅용 함수다.
말한대로 max_size만큼의 큰 배열 하나와 free 상태인 칸들의 인덱스를 저장하는 index_table이 들어간다.
alloc과 free가 한 줄만에 끝나니 당연히 엄청 빠를 것이다. assert는 디버깅 빌드에서 버그를 잡기 위한 것이니 release 빌드에서는 포함되지 않음.

C++ Generic(Template) 구현

위에서 작성한 메모리 풀은 한 가지 자료형만 사용할 수 있으니 c++의 템플릿을 사용하여 모든 자료형에서 사용할 수 있도록 만들어보자.
필자는 ft_irc에서 메시지를 관리하기 위해 메모리 풀을 작성했다. 각 클라이언트가 각자의 버퍼를 가지고 있는 것이 아닌 모든 클라이언트가 메모리 버퍼를 공유하도록 해서 메모리 지역성을 높인다.
아까 간단한 c 구현과 별반 다를 바가 없다.
다만 PAGE_SIZE = 4KB 단위에 맞춰서 공간을 구성하도록 클래스를 짰다.
소켓 통신에서는 페이지에 락을 걸어서 패킷 데이터를 커널로 복사하지 않고 바로 유저에서 다루는 것이 가능하다. 그렇다면 데이터를 send/recv 하는 버퍼들을 연속적인 메모리 공간에 묶어버리면 page locking을 불필요하게 더 많은 페이지에 걸지 않고 하나의 페이지로 끝낼 수 있다.
프로세스 마다 locked page의 한계가 있으니 대규모 클라이언트를 처리하기 위해선 이런식으로 처리해야한다.
과제에선 멀티스레딩을 허용하지 않았지만 멀티스레딩에서는 이런식으로 메모리 풀을 구성하면 락을 아예 사용하지 않거나 더블 버퍼링을 손쉽게 구현해버릴 수 있다.

Fixed Memory Pool의 문제점

이러한 메모리 풀도 문제점이 있는데, 바로 Capacity가 정해져 있다는 것이다.
맨 처음에 할당한 큰 버퍼를 다 사용하면 추가로 할당할 수 없다.
물론 최대로 malloc할 크기가 정해져있는 프로그램들이 대다수이므로 여전히 유효하다.

Variable Memory Pool

그렇다면 그냥 한계점에 도달하면 추가로 malloc하도록 짜면 되는거 아닌가? 가변 메모리 풀을 만들어보자.
컨셉은 여러가지 fixedMemoryPool을 청크로 사용해서 청크를 여러개 묶어 써보자~ 이다.
그러므로 이제는 배열을 만들어서 간단히 떼주는게 아닌 free 할 때를 위해서 데이터 바로 앞에 어떤 Chunk에서 왔는지에 대한 chunk Index를 붙인다. free가 될 때 주소의 몇 바이트 앞에서 Chunk Index를 보고 어느 청크에 반환해야 하는지 알아낸다.
그래서 Chunk_Index + Data 를 붙여서 Block 이라는 구조를 만들었다. (VariableMemoryPoolBlock_t)
VariableMemoryPool은 Block을 관리하는 FixedMemoryPool들을 벡터로 여러 개 연결해서 관리하게 한다.
가지고 있는 FixedMemoryPool 청크들을 순서대로 보면서 할당 가능한 공간이 있는지 본다. 할당 가능하다면 해당 청크에서 곧바로 할당하고 할당 가능한 풀이 없다면 새로운 청크를 만들어 청크 리스트에 추가한다.
이제 가변 메모리 풀도 완성했다! Non-POD 타입도 지원한다.

new/delete 오버로딩

이대로 사용해도 괜찮지만 메모리 풀에 직접 할당/해제 하면 귀찮고 실수할 가능성이 꽤 있다.
그러므로 직접 메모리풀에 alloc/free를 호출하지 않아도 기존 new/delete만으로 메모리풀을 사용하게 new/delete 키워드를 오버로딩 해버리자.
필자는 메시지 버퍼 구조체를 오버로딩 시키겠다. https://github.com/Ria9993/IRC_server/blob/main/Source/Server/MsgBlock.hpp
끝. 이제 MsgBlock 구조체를 new/delete 하면 자동으로 메모리 풀을 사용한다.
굳이 이렇게 메모리 풀로 new/delete를 오버로딩 하는게 아니더라도
클래스마다 각각 개별 힙을 만들어서 new/delete를 오버로딩 해버리면 락 비용을 없애거나 어떤 클래스에서 메모리 누수나 corruption이 생겼는지 디버깅을 용이하게 할 수 있다.

스마트 포인터 (shared_ptr, weak_ptr)

Send 버퍼를 관리할 때 shared_ptr이 필요해서 하는 김에 스마트 포인터도 구현했다.