cpp 언어로 메모리 직접 관리하기

임베디드 특정 칩에서 한정된 메모리로 관리해야하는 상황이 생길 수 있다.
stdlib.h 의 malloc, memcpy 사용하지 않는 상황에서 배열 관련으로 코드 가독성도 높이고 메모리 관리가 필요하여 별도의 라이브러리를 구축하게 되었다.
동작 코드는 사실상 c 언어인데, IDE에서 불필요한 함수와 변수 호출을 최소화하려고 cpp 클래스로 캡슐화하였다.


ArrayMap.h

#ifndef ARRAY_MAP_H
#define ARRAY_MAP_H

class ArrayMap {
private:
	struct Row {
		int* data;
		int length;
	};
	// maximum space 
	// POOL_SIZE / Row = 
	// 1024 / 4 = 
	// 256 = 
	// full capacity
	static constexpr int MAX_POOL_SIZE = 256 * 4;
	static constexpr int MAX_DEPTH_SIZE = 64;
	unsigned char mem[MAX_POOL_SIZE];
	int** di[MAX_DEPTH_SIZE];
	Row rows[MAX_DEPTH_SIZE];
	int offset;
	int capacity;
	int size;

	void reset();
	

public:
	int alloc(int size);
	int* getArray(int rowAddr);
	int getArrayLength(int rowAddr);
	int* setReverse(int rowAddr);
	int** getMap();
	int length();
};

#endif

구형 호환성으로 #progma once 대신 ifndef, define 매크로로 중복으로 호출하는 것을 방지하였다.

호출자에게 불필요한 변수 변경 방지 및 내부용 구조체(struct Row{}) 호출되지 않도록 private 으로 선언하였다.

메모리 크기는 mem으로 선언한 것으로 볼 수 있다. 힙 공간의 1024byte 사용한다.
마찬가지로 rows 변수로 힙메모리 공간을 선언한 것과 같이 64byte * Row(12~16byte) 768~1024byte 사용한다.

MAX_POOL_SIZE, MAX_DEPTH_SIZE 상수 값 수정으로 크기를 늘릴 수 있다.

Row 구조체의 메모리 공간은 다음과 같다.

struct Row {
    int* data; // 4byte. 64bit 칩셋이면 8byte
    int length; // 4byte
    // 구조체 padding 4byte
}

그리고 사용하는 멤버함수 기능은 심플하다.

public:

  • int alloc(int size): 힙메모리인 mem[MAX_POOL_SIZE] 매개변수 공간만큼 Row 구조체 data 배열과 length 값을 할당한다.
  • int* getArray(int rowAddr): 매개변수로 인덱스를 전달되면 자신의 Row 구조체 data 배열을 반환한다.
  • int* setReverse(int rowAddr): 매개변수로 인덱스를 전달되면 자신의 Row 구조체 data 배열을 역순으로 변경하여 반환한다.
  • int getArrayLength(int rowAddr): 매개변수로 인덱스를 전달되면 자신의 Row 구조체의 length 값을 반환한다.
  • int** getMap(): 자신의 Row 구조체 data 전체를 반환한다.
  • int getMapLength(): Row 구조체 크기를 반환한다.

코드를 살펴보면 알겠지만, 메모리 직접 관리로 아슬아슬한 줄타기를 타고 있다. 이러한 라이브러리를 사용하면 all, any, fill 과 같은 부가기능 멤버함수를 추가하기도 수월하고 유지보수도 쉽다.


ArrayMap.cpp

#include "ArrayMap.h"
#include <stdio.h>

void ArrayMap::reset() {
	capacity = 0;
	offset = 0;
	size = 0;
}

int ArrayMap::alloc(int currentSize) {
	if (offset < 0) reset();

	if (offset + currentSize > (MAX_POOL_SIZE + sizeof(int))) return -1;
	void* ptr = mem + offset;
	offset = offset + (currentSize * sizeof(int));

	di[size] = (int**)ptr;
	rows[size].data = (int*)ptr;
	rows[size].length = currentSize;

	return size++;
}

int* ArrayMap::getArray(int rowAddr) {
	if (rows[rowAddr].length < 0) return nullptr;
	
	return (int*)rows[rowAddr].data;
}

int ArrayMap::getArrayLength(int rowAddr) {
	if (rows[rowAddr].length < 0) return -1;
	return rows[rowAddr].length;
}

int* ArrayMap::setReverse(int rowAddr) {
	// TODO: 인메모리 변경으로 멀티스레드 환경에서 보장 불가능. 뮤텍스 락 필요 및 싱글스레드로만 할당
	if (rows[rowAddr].length < 0) return nullptr;

	int len = rows[rowAddr].length;
	int* arr = rows[rowAddr].data;
	for (int i = 0; i < len / 2; i++) {
		int tmp = arr[i];
		arr[i] = arr[len - 1 -i];
		arr[len - 1 - i] = tmp;
	}

	return arr;
}

int** ArrayMap::getMap() {
	if (di == nullptr) return nullptr;

	return (int**)di;
}

int ArrayMap::length() {
	return size;
}

장점과 단점을 살펴보자

장점

  • 클래스 접근제어자 사용으로 불필요한 변수, 함수를 호출하지 않는다.
  • 유지보수성이 올라간다.
  • 재사용이 높다.

단점

  • 호출부에서 배열의 크기 이상으로 할당할 수 있어서 배열 침범이 일어난다. (데이터유실 발생, 크래쉬 발생 가능. Todo: 스택 멤버 함수 추가해서 개선하기)
  • 힙공간을 전역변수로 사용하고 있다. (반환이 없다. stdlib 라이브러리를 불려와 malloc 인스턴스로 사용하는 것도 고려해볼만 하다.)
  • 힙공간을 임의의 크기로 다시 재정의할 수 없다. (Todo: realloc 멤버 함수 추가하도록 하자.)
  • 배열이 초기화되지 않음. 쓰레기값 출력함.

단점이 매우 많은데, 숨겨진 위험 요소와 정의되지 않은 동작(undefined behavior)이 많으므로 오동작/크래시/데이터 손상이 발생될 수 있다는 것이 문제다. 앞으로의 c / cpp 메모리 안전성이 떨어진다는 것을 다시 한번 바라볼 계기가 되었다.

가능하다면 vector 라이브러리를 사용한다.


테스트 케이스

사용한 라이브러리는 테스트케이스를 만들어 기능 추가나 코드 변경한 경우 영향이 없는지 검증이 필요하다.

assert 라이브러리 기능을 사용하면 컴파일 단계에서만 검증하고 런타임에는 동작하지 않으므로 유용하다.

ArrayMap map;

// 조건
int arrTen = map.alloc(10); // 첫번 째 배열 10개 메모리 할당
int arrFive = map.alloc(5); // 두번 째 배열 5개 메모리 할당
int arrReverse = map.alloc(4); // 세번 째 배열 4개 메모리 할당

int* arr = map.getArray(arrTen); // 첫번 째 인덱스 배열 불려오기
arr[0] = 500; // 첫번 째 배열 메모리에 500 대입
arr[9] = 60000; // 첫번 째 배열 메모리에 60000 대입

arr = map.getArray(arrFive); // 두번 째 인덱스 배열 불려오기
arr[0] = 10000; // 두번 째 배열 메모리에 10000 대입
arr[4] = 50; // 두번 째 배열 메모리에 50 대입

arr = map.getArray(arrReverse); // 세번 째 인덱스 배열 불려오기
arr[0] = 100; // 세번 째 배열 메모리에 100 대입
arr[3] = 900; // 세번 째 배열 메모리에 900 대입

// 세번 째 배열 모든 공간을 역순으로 치환함
assert(map.setReverse(arrReverse) != nullptr);
assert(map.setReverse(-1) == nullptr); // 비정상 케이스 확인

// 첫번째 getLength 결과
assert(map.getArrayLength(arrTen) == 10);
// 두번째 getLength 결과
assert(map.getArrayLength(arrFive) == 5);
// 첫번째 배열 대입 확인
assert(map.getArray(arrTen)[0] == 500);
assert(map.getArray(arrTen)[9] == 60000);
// 두번째 배열 대입 확인
assert(map.getArray(arrFive)[0] == 10000);
assert(map.getArray(arrFive)[4] == 50);
// 세번째 배열 역순 치환 확인
assert(map.getArray(arrReverse)[0] == 900);
assert(map.getArray(arrReverse)[3] == 100);

// 전체 ArrayMap 2차원으로 펼쳐서 불려오기
assert(map.getMap()[0][0] == 500);
assert(map.getMap()[1][0] == 10000);
assert(map.getMap()[2][0] == 900);

// length 추가한 배열의 Dimention 전체 크기 확인하기
assert(map.length() == 3);