728x90

TypeCast.h

#include <iostream>
using namespace std;

#pragma region TypeList

template<typename... T>
struct TypeList;

template<typename T, typename U>
struct TypeList<T, U>
{
	using Head = T;
	using Tail = U;
};

template<typename T, typename... U>
struct TypeList<T, U...>
{
	using Head = T;
	using Tail = TypeList<U...>;
};

#pragma endregion

#pragma region Length

template<typename... T>
struct Length;

template<>
struct Length<TypeList<>>
{
	enum { value = 0};
};

template<typename T, typename... U>
struct Length<TypeList<T, U...>>
{
	enum { value = 1 + Length< TypeList<U...>>::value };
};

#pragma endregion

#pragma region TypeAt

/*
TypeAt< TypeList<Tail...>, index - 1 >::Result 가 타입을 의미 하는지
값을 의미 하는지 컴파일러는 알지 못 함.

때문에 이런 모호함 때문에 템플릿 인자에 의존적인 이름은 기본적으로 '타입'
이 아닌 '값'으로 컴파일러가 처리하게 되는데,
'타입'을 명시 하려면 typename 붙여 줘야 한다.
*/

template<typename TL, int index>
struct TypeAt;

template<typename Head, typename... Tail>
struct TypeAt<TypeList<Head, Tail...>, 0>
{
	using Result = Head;
};

template<typename Head, typename... Tail, int index>
struct TypeAt<TypeList<Head, Tail...>, index>
{
	using Result = typename TypeAt<TypeList<Tail...>, index - 1>::Result;
};

#pragma endregion



#pragma region IndexOf

template <typename TL, typename T>
struct IndexOf;

template <typename... Tail, typename T>
struct IndexOf<TypeList<T, Tail...>, T>
{
	enum { value = 0 };
};

template <typename T>
struct IndexOf<TypeList<>, T>
{
	enum { value = -1 };
};

template <typename Head, typename... Tail, typename T>
struct IndexOf<TypeList<Head, Tail...>, T>
{
private:
	enum { temp = IndexOf<TypeList<Tail...>, T>::value }; 

public:
	enum { value = (temp == -1) ? -1 : temp + 1 };
};

#pragma endregion




#pragma region Conversion

template<typename From, typename To>
class Conversion
{
private:
	using Small = __int8;
	using Big = __int16;

	///  ... 매개변수 우선순위가 낮음.
	static Small Test(const To&) { return 0; }
	static Big Test(...) { return 0; }
	static From MakeFrom() { return 0; }
public:
	enum { exists = ( sizeof(Test(MakeFrom())) == sizeof(Small) ) };
};

#pragma endregion


#pragma region TypeCast


	template<typename TL>
	class TypeConversionBefore
	{
	public:
		enum
		{
			length = Length<TL>::value
		};

		// 런타임에 정해지는 변수와 컴파일에 정해지는 값이 혼용되어 컴파일 불가능.
		TypeConversionBefore()
		{
			for (int i = 0; i < length; i++;)
			{
				for (int j = 0; j < length; j++;)
				{
					using FromType = typename TypeAt<TL, i>::Result;
					using ToType  = typename TypeAt<TL, j>::Result;

					if (Conversion<const FromType*, const ToType*>::exists)
						s_convert[i][j] = true;
					else
						s_convert[i][j] = false;
				}
			}
		}
	};


// v 값 별로 별도의 클래스
template<int v>
struct Int2Type
{
	enum { value = v};
};

template<typename TL>
class TypeConversion
{
public:
	enum
	{
		length = Length<TL>::value
	};

	TypeConversion()
	{
		MakeTable(Int2Type<0>(), Int2Type<0>());
	}

	template<int i, int j>
	static void MakeTable(Int2Type<i>, Int2Type<j>)
	{
		using FromType = typename TypeAt<TL, i>::Result;
		using ToType = typename TypeAt<TL, j>::Result;

		if (Conversion<const FromType, const ToType>::exists)
		{
			s_convert[i][j] = true;
		}
		else
		{
			s_convert[i][j] = false;
		}

		MakeTable(Int2Type<i>(), Int2Type<j + 1>());
	}

	template<int i>
	static void MakeTable(Int2Type<i>, Int2Type<length>)
	{
		MakeTable(Int2Type<i + 1>(), Int2Type<0>());
	}

	template<int j>
	static void MakeTable(Int2Type<length>, Int2Type<j>)
	{
	}
		
	static bool CanConvert(int from, int to)
	{
		static TypeConversion conversion;

		return s_convert[from][to];
	}

public:
	static bool s_convert[length][length];
};

// s_convert는 멤버 변수 같지만 전역 변수 이기 때문에 클래스 외부에 전역 변수 선언 하듯 선언 해야 한다.
// 다만 해당 클래스 네임스페이스를 이용해서만 접근 가능 하도록 변수를 선언한다.
template<typename TL>
bool TypeConversion<TL>::s_convert[length][length];

#pragma endregion

 

TypeCast.cpp

#include "TypeCast.h"



class Player
{

};

class Knight : public Player
{

};

class Mage : public Player
{

};

class Archer : public Player 
{

};


int main()
{
    // TypeList
    {
        TypeList<Knight>::Head;
        TypeList<Knight>::Tail;

        TypeList<Knight, Mage>::Head;
        TypeList<Knight, Mage>::Tail;

        TypeList<Knight, Mage, Archer>::Head;
        TypeList<Knight, Mage, Archer>::Tail::Head;
        TypeList<Knight, Mage, Archer>::Tail::Tail;
    }

    // Length
    {
        Length<TypeList<Knight>>().value;
        Length<TypeList<Knight, Mage>>().value;
        Length<TypeList<Knight, Mage, Archer>>().value;
    }

    // TypeAt
    {
        using TL = TypeList<Knight, Mage, Archer>;

        TypeAt<TL, 0>::Result;
        TypeAt<TL, 1>::Result;
        TypeAt<TL, 2>::Result;
    }

    // IndexOf
    {
        using TL = TypeList<Knight, Mage, Archer>;

        IndexOf<TL, Knight>().value;
        IndexOf<TL, Mage>().value;
        IndexOf<TL, Archer>().value;
    }

    // Conversion
    {
        Conversion<Knight, Player>().exists;
        Conversion<Player, Knight>().exists;
        Conversion<Mage, Knight>().exists;
    }

    // TypeCast
    {
        using TL = TypeList<Player, Mage, Archer>;


        bool can1 = TypeConversion<TL>().CanConvert(1, 1);
        bool can2 = TypeConversion<TL>().CanConvert(0, 2);
        bool can3 = TypeConversion<TL>().CanConvert(2, 0);

    }
}

 

728x90
728x90

#define은 치환 전처리기

typedef는 새로운 자료형을 저으이

728x90
728x90

병렬 프로그래밍 하다 보면 => 다수 스레드를 관리하게 됨 => fork(쓰레드 다수 생성) - join(쓰레드 소멸 이후 워크 플로우 메인스레드에서만 관리) 방식을 사용하면 관리에 용이.

 

mutex가 정말 필요하다면 그냥 메인스레드에서만 접근하고 연산이 많이 필요한 경우에만 다수 스레드를 사용

 

쓰레드를 생성하고 소멸하는 과정이 비싸다. 때문에 쓰레드 풀을 만드는 방식을 써왔다. 현재 모던 c++ 표준에서는 이마저도 지원 해준다.

 

Amdahl's law

프로그파일링을 통해서 자원을 많이 잡아 먹는 부분을 우선적으로 병렬 프로그래밍을 통해서 최적화를 해야 한다.

 

 

 

 

728x90
728x90

os 를 만들 때는 parallel programing 이 중요하지만

app 위주 개발에서는 고난이도 parallel programming이 중요하지 않아지고 있다.

 

mutex, semaphore => 공유 자원들을 컨트롤 하기 위한 기법들.

하지만 os를 이미 어느 정도 지원하기 때문에 그럴 일이 별로 필요하지 않음.

 

app 공유자원을 관리 한다는 건 heap이나 global 메모리 정도를 관리하는 얘기. 하지만 이마저도 굳이 멀티스레드를 쓸 필요가 없다.

 

임베디드에서도 좋은 os가 들어 가고 있다.

 

web도 내부적으로는 멀티스레드지만 db가 자체적으로 공유자원에 대한 문제를 해결해준다.

 

silocon < engineer. 고급 엔지니어를 이용한 high optimization 보다 clould 기반 기술을 이용하여 virtual pc 등으로 수요를 감당 하는게 더 저렴함.

 

따라서 병렬프로그래밍은 OS를 개발하는 사람이 아니면 점점 수요가 떨어지고 있다.

 

C++이 high performance를 필요로 하기 때문에 멀티코어, 멀티스레드 프로그래밍을 해야 하는 건 사실.

하지만 여전히 edge device(개인이 사용하는)에는 필요하긴 하다.

728x90
728x90

멀티 스레드에서 인접 메모리를 연산하는 경우가 있다고 가정 해보자. cache line이 64byte로 설계되어 있어 64byte보다 작은 데이터를 연산하더라도 인접 메모리까지 포함해서 코어는 64byte를 가져와서 계산하고 이때 현재 계산 대상은 아니지만 다른 코어에서 인접 부분 메모리를 수정하고 있게 된다. 이걸 false sharing 이라고 부르고 이를 해결 하기 위해서 코어간에 sync process를 진행하게 된다.  따라서 오히려 싱글 스레드 연산보다 더 느린 결과 나올 수 있다

 

이런 sync process 과정을 없애기 위한 방법으로 패딩을 넣을 수 있다. 현재 컴퓨터 구조에서는 메모리 자원이 cpu 자원보다 훨씬 저렴하기에 메모리 자원을 낭비하고 CPU 자원을 효율적으로 활용하는 경우가 많다.

 

샘플코드

#include <iostream>
#include <thread>
#include <chrono>
#include <iomanip>

using namespace std;
using namespace chrono;

long long num1 = 0;
long long num2 = 0;
long long num3 = 0;
alignas(64) long long num4 = 0;
alignas(64) long long num5 = 0;

void func1()
{
    for (size_t i = 0; i < 10000000; i++) num1 += 1;
}

void func2()
{
    for (size_t i = 0; i < 10000000; i++) num2 += 1;
}

void func3()
{
    for (size_t i = 0; i < 10000000; i++) num3 += 1;
}

void func4()
{
    for (size_t i = 0; i < 10000000; i++) num4 += 1;
}

void func5()
{
    for (size_t i = 0; i < 10000000; i++) num5 += 1;
}

int main()
{
    const int RANGE = 20000;

    cout.setf(ios::right);
    {
        auto begin = chrono::high_resolution_clock::now();

        func3();
        
        auto end = chrono::high_resolution_clock::now();
        chrono::duration<double> duration = end - begin;

        cout << setw(50) << std::left << "single thread (elapsed time) :: " << setw(10) << duration.count() << endl;
    }

    {
        auto begin = chrono::high_resolution_clock::now();

        thread t1 = thread(func1);
        thread t2 = thread(func2);

        t1.join();
        t2.join();
       
        auto end = chrono::high_resolution_clock::now();
        chrono::duration<double> duration = end - begin;

        cout << setw(50) << "mutli thread (elapsed time) :: " << setw(10) << duration.count() << endl;
    }

    {
        auto begin = chrono::high_resolution_clock::now();

        thread t1 = thread(func4);
        thread t2 = thread(func5);

        t1.join();
        t2.join();

        auto end = chrono::high_resolution_clock::now();
        chrono::duration<double> duration = end - begin;

        cout << setw(50) << "mutli thread + padding (elapsed time) :: " << setw(10) << duration.count() << endl;
    }
}

 

결과

 

728x90
728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

목표

운영체제에 직접 메모리를 할당/해제 요청 함으로써 잘못된 메모리 접근을 막는다.

 

구현

void* StompAllocator::Alloc(int32 size)
{
	// 페이지 사이즈 단위로 반올림.
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE; 
	const int64 reqSize = pageCount * size;

	return ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);;
}

void StompAllocator::Release(void* ptr)
{
	::VirtualFree(ptr, 0, MEM_RELEASE);
}

애초에 작은 공간을 요청해도 페이지 단위로 할당하기 때문에 페이지 사이즈 단위로 요청하는 코드이다.

 

 

적용

template<typename Type, typename... Args>
Type* xnew(Args&&... args)
{
	Type* memory = static_cast<Type*>( StompAllocator::Alloc(sizeof(Type)) );

	new(memory)Type(::forward<Args>(args)...);

	return memory;
}

template<typename Type>
void xdelete(Type* obj)
{
	obj->~Type();
	StompAllocator::Alloc(obj);
}

이전 단계에서 만든 xnew / xdelete의 메모리 할당 해제 부분을 stomp allocator로 대체 했다.

 

개선점

지금까지의 stomp allocator로 할당된 메모리에 접근하는 잘못된 메모리를 접근을 방지할 수 있지만 메모리가 해제 되지 않은 상태에서 잘못된 메모리 참조는 여전히 가능하다. 메모리 할당이 페이지 단위로 이루어 지기 때문에 4byte 객체를 생성해도 64byte가 할당되고 사용하지 않는 메모리 참조가 가능하게 되는 오버플로우 문제가 발생한다. 이를 해결하기 위한 방법을 알아보자.

 

해결 idea

사용할 메모리를 우측 정렬 한다.

 

기존 메모리 구조 => [o][o][o][x][x][x][x][x][x][x]

메모리를 우측 정렬 =>  [x][x][x][x][x][x][x][o][o][o]

 

구현

void* StompAllocator::Alloc(int32 size)
{
	// 페이지 사이즈 단위로 반올림.
	const int64 pageCount = (size + PAGE_SIZE - 1) / PAGE_SIZE; 
	const int64 dataOffset = pageCount * PAGE_SIZE - size;
	const int64 reqSize = pageCount * size;

	void* base = ::VirtualAlloc(NULL, pageCount * PAGE_SIZE, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);;

	return static_cast<void*> ( static_cast<char*>(base) + dataOffset );
}

void StompAllocator::Release(void* ptr)
{
	// 메모리는 프로세스에 메모리가 64kb로 할당 되고
	// 메모리 요청은 페이지 단위 4kb 할당 되기 떄문에
	// 4kb로 나머지 연산한 값이 offset 값임을 알 수 있다.
	const int64 address = reinterpret_cast<int64>(ptr);
	const int64 baseAddress = address - (address % PAGE_SIZE);

	::VirtualFree(reinterpret_cast<void*>(baseAddress), 0, MEM_RELEASE);
}

offset을 구해서 char* 타입 포인터 값과 포인터 연산을 한 모습이다. 이렇게 되면 반대로 언더플로우 문제가 생길 수 도 있으나 오버플로우 처럼 일반적인 경우는 아니기 때문에 신경 쓰지 않기로 한다.

 

결론

해제된 메모리의 잘못된 접근을 막는다.

할당 메모리의 오버플로우 문제를 막는다.(언더플로우는 못 막음)

 

728x90
728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

이번 단계는 앞서 만든 메모리 할당기 stomp allocator를 컨테이너에도 적용되게 하는 실습이다.

 

컨테이너는 기본적으로 할당자를 템플릿 변수로 받는다. 이를 채워 줄 StlAllocator를 정의 해보자.

 

template<typename T>
class StlAllocator
{
public:
	using value_type = T;

	StlAllocator() {}

	template<typename other>
	StlAllocator(const StlAllocator<other>&) {}

	T* allocate(size_t count)
	{
		const int32 size = count * sizeof(T);

		return static_cast<T*>(StompAllocator::Alloc(size));
	}

	void deallocate(T* ptr, size_t count)
	{
		StompAllocator::Release(ptr);
	}
};

할당자로 쓰기 위해 정의 해야하는 부분은 vector 라이브러 버전에 따라 달라 질 수 있으니 참고하자. 중요한 건 allocate, deallocate 함수의 구현부다.

 

커스텀 할당기를 편하게 사용하기 위해 template using 문을 작성 해보자.

template<typename T>
using Vector = vector < T, StlAllocator<T>>;

 

결과

int main()
{	
	Vector<Knight> v(100);
}

 

 

이로서 일단 메모리를 xnew를 사용하는 버전, 컨테이너를 생성하는 버전 모두 stomp allocator를 통해서 메모리를 할당 받을 수 있다!

728x90
728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

 

필요한 이유

메모리 이슈를 디버깅 시점에 수월하게 잡기 위함.

 

메모리 이슈 사례 몇 가지

- dangling pointer  =>스마트 포인터로 어느 정도 해결 가능

 

- user after free  =>스마트 포인터로 어느 정도 해결 가능

 

- 컨테이너 순회 중에 컨테이너 요소를 삽입/삭제하거나 컨테이너를 clear하는 등의 동작을하고 반복문을 빠져 나오지 않는 경우 => 컴파일러가 주기도 하지만 모든 상황에 그럴거란 보장은 없음

 

- casting 문제 상속관계 객체를 잘못 캐스팅 한 경우(일종의 overflow) => dynamic 캐스팅을 하면 좀더 안전하지만 속도 문제로 dynamic 캐스팅을 안 쓰기도 함

 

 

페이지 확인 하는 함수

	SYSTEM_INFO info;

	::GetSystemInfo(&info);

	// 페이지 사이즈. (4kb, 0x1000)
	// 페이지 보다 더 작은 사이즈의 메모리를 운영체제에게 요구해도
	// 운영체제는 페이지 사이즈를 예약합니다.
	info.dwPageSize; 

	// 프로세스 주소 공간에서 특정 영역을 예약할 때 사용하는
	// 단위의 크기, 대부분 65536 값을 가지고 있다.
	info.dwAllocationGranularity;

 

운영체에게 직접 메모리 할당/해제를 요청 하는 법

	int* test = (int*)::VirtualAlloc(NULL, 4, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	*test = 100;
	::VirtualFree(test, 0, MEM_RELEASE);

 

 

이렇게 했을 때 malloc, free를 이용해서 메모리 할당/해제를 했을 때 무슨 차이가 있을까? VirutualFree()가 호출되고 다시 '*test=200'과 같은 코드가 있으면 크래쉬가 난다. 이전에 new, delete를 사용할 때 발생하는 dangling pointer 이슈 생기지 않는다. 이 말은 new / delete 키워드를 쓰면 운영체제에게 직접 메모리 할당 / 해제 요청을 하는 게 아니라 컴파일러 자체적으로 최적화가 이루어 지고 있을 수 추측할 수 있다. 이런 최적화가 있기 때문에 앞으로 다룰 메모리풀링이 어쩌면 의미가 없을 수도 있다. 하지만 반대로 이런 최적화는 디버깅 단계, 개발 단계에서는 오히려 버그를 찾기 힘들게 한다. 따라서 stomp allocator의 핵심은 직접 운영체제에게 메모리 할당/해제 요청을 함으로써 잘못된 메모리 참조로 인한 이슈를 찾기 위함이다. 3,4단계 포스팅을 통해 stomp allocator의 필요성과 구현을 위한 사전지식을 학습 했으므로 다음 단계에서 직접 구현 해보도록 하자.

728x90

+ Recent posts