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 메모리 안전성이 떨어진다는 것을 다시 한번 바라볼 계기가 되었다.

가능하다면 std의 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);