며칠 전에 zelon님과 함께 코드리뷰를 하다가 vector의 capacity에 대한 이야기가 나왔는데, 재미있는 일이 있었다. 지난 수년간 나는 vector의 capacity를 줄이기 위해서 clear() 함수 대신 resize(0)를 사용하고 있었는데, 내가 잘 못 알고 있었던 것.

나는 clear()는 데이터만 삭제할 뿐 내부 버퍼로 사용하는 메모리는 유지되는 반면 resize(0)는 버퍼까지 모두 정리하는 것이라고 말했는데, zelon님은 내가 반대로 알고 있다고 말했다. 그래서 누가 맞는지를 검색해보자고 찾아봤더니… 둘 다 틀렸음 ㅋㅋㅋ 결론적으로는 두 함수 모두 버퍼를 줄이지 않는다.

ESTL의 챕터 17에서 이에 대한 설명을 찾아볼 수 있다. 그리고 capacity를 사이즈에 맞게 줄일 수 있는 트릭을 제시하고 있는데,

vector<Contestant>(contestants).swap(contestants);

그리 쉽게 눈에 읽히지는 않는다.

헌데, C++11에서 capacity를 줄일 수 있는 새로운 인터페이스 shrink_to_fit() 이 추가됐다. vs2010에서는 ESTL과 동일하게 임시객체를 써서 구현되었다고 하는데, vs2013은 구현도 약간 다르다.

void shrink_to_fit()
{	// reduce capacity
	if (_Has_unused_capacity())
	{	// worth shrinking, do it
		if (empty())
			_Tidy();
		else
			_Reallocate(size());
	}
}

늘어난 버퍼를 줄이고 싶다면 resize()로 먼저 데이터 크기를 줄인 후, (혹은 clear()로 데이터를 비운 후) shrink_to_fit()을 호출해준다.

int _tmain(int argc, _TCHAR* argv[])
{
	std::vector<int> vecData;

	const int max_size = 10000;
	vecData.reserve(10000);

	for (int i = 0; i < max_size; i++)
	{
		vecData.push_back(i);
	}

	vecData.resize(200);
	vecData.shrink_to_fit();

	return 0;
}

'프로그래밍 팁 > C++' 카테고리의 다른 글

vector::shrink_to_fit  (0) 2013.12.24
Reconstructing parameters from x64 crash dumps  (0) 2013.07.17
x64 Stack Frame layout  (0) 2013.07.04
x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

발번역 입니다. 원본 : http://analyze-v.com/?p=482

Note : 만약 크래시 발생 시점의 풀 메모리 덤프를 가지고 있고, 스택 프레임 등 디버깅 데이터를 적당히 유지하도록 빌드한 상태였다면 WinDbg의 dv 명령을 먼저 확인할 것. 덤프에 정보가 풍성하게 유지된 경우는 WinDbg가 웬만하면 알아서 지역변수들을 모두 분석해 보기 쉽게 출력해준다. 아래는 덤프분석 연습 삼아 번역해둔 것.

영문은 의역이 많고, 퇴고시 원본 대조 없이 내가 이해하기 쉬운 말투로 재조정 했으니 말 그대로 발번역.

 

(번역 시작)

우리는 x64 환경의 크래시와 관련된 이야기들을 이어나가고 있고, 실제 시스템상에서 발생한 크래시 덤프를 어느 정도 분석할 수 있는 수준에 도달했다. 이번 글에서는 x64 크래시에서 함수에 전달된 실제 인자값을 찾아내는 데모를 설명해 보고자 한다. 앞에서 언급했던 바와 같이, x64 컴파일러가 인자를 전달할 때 값이 유지되지 않는(volatile) 레지스터들을 사용하기 때문에 조금 까다로운 부분이 있다. 그래서 다른 레지스터들에 저장된 값이나 스택에 적힌 값들을 참고해야만 한다.

실제로 문제가 발생한 서브루틴으로 전달된 인자값을 읽어낼 덤프를 분석해 보도록 하자. 아래에는 크래시가 발생한 모듈에서 문제가 되는 명령어를 보여주고 있다.

faultinginst

문제가 발생한 명령어는 두 번째 화살표가 가리키는 위치이고, 첫 번째 화살표는 RDX 레지스터에서 RBX레지스터로 잘못된 포인터 값을 로딩하는 부분을 가리키고 있다. 내용을 분석해보면 RBX 레지스터에 있는 잘못된 포인터 값을 읽다가(dereference) 문제가 발생했음을 알 수 있다. 우리가 x64 호출규약에 대해서 정리했던 것처럼, RDX는 함수로 전달되는 두 번째 인자값이니까 아마 이 함수는 두 번째 인자값으로 잘못된 포인터의 주소를 전달받았을 것이라고 짐작할 수 있다.

구글에서 함수 이름을 검색해보면 RtlDeleteNoSplay 함수의 원형을 확인할 수 있다.

VOID RtlDeleteNoSplay(
     
IN PRTL_SPLAY_LINKS Links,
      IN OUT PRTL_SPLAY_LINKS *Root
);

이제 보다 많은 정보들을 분석하기 위해 일단 이 함수로 전달된 두 개의 인자값들을 읽어내 보자. 이 작업은 아주 간단히 끝날 수도 있고 무척이나 짜증스러운 과정이 될 수도 있는데, 이 함수의 첫 번째 인자값을 알아내는 과정을 세 가지 다른 난이도의 방법으로 설명하려고 한다. 이 방법들은 모두 특정 상황에만 해당되는 극히 한정적인 해법일 수 있긴 하지만, 여기에서 설명하는 기법들이 실제로 쓸모가 있기를 기대해 본다.

 

 

EASY – 쉬운 방법

이 경우에는 아주 운 좋게도 첫 번째 인자를 쉽게 확인할 방법이 있다. 크래시가 발생하기 전에 RCX의 값이 R11레지스터에도 로딩 되었기 때문이다.

rcxr11

R11 레지스터는 값이 보존된 레지스터?이기 때문에 크래시 시점의 프레임에서 R11레지스터의 값이 유지되고 있다고 말할 수 있고, R11의 값을 확인함으로써 첫 번째 인자값을 알아낼 수 있다.
(역주: 원본 글에서는 R11을 non-volatile이라고 적었는데, 원본글 아래에 이에 관련된 댓글이 달려있다. R11은 volatile 레지스터이므로, 무작정 R11의 값을 신임할 수는 없다. 하지만 위의 경우 R11값에 대한 추가적인 조작이 없으므로, 이 경우에 한해서는 값이 유지되는 (non-volatile)레지스터라고 여겨도 무방할 뿐이다.)

r11param

하지만, 크래시가 발생하기 전에 RCX의 값을 R11에 저장하지 않았다면 어떻게 할 것인가? 그런 경우엔 이전의 프레임으로 좀 더 깊이 파고들어가 분석을 계속 해야 한다. 나는 가능하다면 다음에 설명할 방법을 종종 연습 삼아 해보는데, 인자값을 이미 파악 했다 하더라도 좀 더 난이도 있는 다음의 방법으로 알아낸 값과 비교해보면 값을 제대로 된 값을 읽었는지 확인해 볼 수 있기 때문이다.

 

MEDIUM – 조금 더 어렵게.

그럼 R11레지스터를 사용하지 않고 어떻게 첫 번째 인자값을 알아낼 수 있을까? 콜스택에서 바로 이전에 실행된 함수(즉 caller;호출자)는, 바로 다음 프레임에서 사용할 수 있도록 RCX레지스터에 값을 올려 두어야만 한다. 이점을 이용해서, 스택 상에서 앞 단계에 위치한 함수부터 분석을 시작해보자. 지금 예로 든 덤프의 경우에는 TreeUnlinkNoBalance 함수가 될 것이다.

treeunlink1

RtlDeleteNoSplay 함수를 호출하기 전에 RCX의 값이 RBX에 저장된다는 사실을 확인할 수 있다. 그럼 이제 RtlDeleteNoSplay 함수 시작점에서 RCX의 값과 RBX의 값이 같다는 사실을 알 았고, RtlDeleteNoSplay 함수 안쪽에서 인자값 확인에 참고할 수 있는 레지스터가 한 가지 더 늘어난 셈이다. 그럼 RtlDeleteNoSplay 함수를 다시 한 번 살펴보자.

rtldeleteprolog

RtlDeleteNoSplay 의 첫 번째 명령어를 주목해보자 – RBX를 무려 스택에 집어넣고 있다! 이 루틴으로 전달된 첫 번째 인자는 실행 스택에 보존되어 있다. 크래시가 발생한 프레임(trap frame)에서 RSP값이 곧 크래시가 발생한 시점의 스택 포인터 값이다. 스택에 저장된 값을 다시 읽어내려면 값을 스택에 저장한 시점 이후 처리된 스택의 조작을 역으로 다시 풀어줄(unwinding) 필요가 있다. 이 예제의 경우에라면 스택 포인터에 0x20만 더해주면 된다. push 연산 수행 후 RSP에서 0x20을 빼 주었던 명령만을 되돌리면 되기 때문이다.

r11match

 

HARD – 어려운 방법

그런데 콜스택이 인자값을 알고 싶어하는 함수를 지나 위쪽으로 더 진행되어 버렸다면 어떻게 해야 할까? 함수가 호출된 시점의 인자를 알아내는 것이 가능할까? 이런 경우를 한 번 알아보자.

이번 데모에서는 이전과 다른 함수의 다른 인자값을 알아내 보자. 콜스택이 추가 진행한 상황에 대한 좀 더 난이도 있는 분석방법을 이야기 해야 하기 때문이다.

아래의 스샷은 PurgeStreamNameCache 함수의 어셈블을 보여준다. 이 함수는 진행 중 DeleteNameCacheNodes를 호출한다.

deletenamecachecall

이 함수는 RSI의 값을 RCX에 로딩시켜서 함수 호출을 준비하고 있음을 주목하라. 이 동작으로 인해서 DeleteNameCacheNodes함수의 내부에서 첫 번째 인자값을 구할 때 RCX뿐만 아니라 RSI의 값도 함께 조사할 수 있게 됐다. 이 두 레지스터 중 아무 값이라도 다른 레지스터에 넣는다든지, 스택에 저장한다든지 하는 명령이 있는지 알아보면 될 것이다.

rsihomed

DeleteNameCacheNodes 함수가 바로 시작하자마자 RSI의 값을 R9의 홈 공간에 해당하는 스택 위치에 저장하는 것을 볼 수 있다. 이 명령어는 엄청난 희소식이다. 이제 알고 싶어하는 인자값은 스택에 저장된 올바른 위치를 찾아 읽어내기만 하면 구할 수 있다. 하지만, 이 때 사용해야 할 스택 포인터의 값을 어떻게 알아낼 수 있을까?

이 부분은 x64에서 인자를 재구성하는데 아주 중요한 트릭이다. 사실 아주 간단하다. 우리가 조사하고 있는 프레임의 Child-SP값을 확인하기 위해 k 명령어를 실행해 주기만 하면 된다. (역주: WinDbg의 k 명령은 콜스택을 덤프해 보여준다. 명령어 설명은 이 곳의 15. Call stack 부분 참조.)

childsp

이 값은 서브루틴이 만들어지는 시점에, 서브루틴의 호출이 끝나고 되돌아올 위치값을 저장한 스택 포인터이다. (역주: 이 부분을 이해하는 것이 중요하다. 원문은 That is the value the stack pointer will be upon return from the current call that subroutine is making.) 이 부분이 많이 헷갈릴 텐데, 이 예제의 경우 DeleteNameCacheNodesTreeUnlinkMulti 함수를 호출하는 시점을 주목해야 한다. 이 때의 Child-SP는 함수 호출이 끝나고 되돌아올 곳인 RSP를 가리킨다. 우리는 이 값을 이용해 DeleteNameCacheNodes 함수의 실행시점 안에서 스택에 담겨 있는 다른 정보들을 쉽게 찾아낼 수 있다. TreeUnlinkMulti 진입점의 prolog에서 계산한 RSP값에서, 우리가 알아내고자 하는 값에 도달할 때까지 스택의 변경사항을 역으로 풀어내기만 하면(unwinding)된다.

offsets

(Note: 위의 이미지는 1/15에 익명의 유저가 올린 댓글에 맞추어 수정되었다. 본문 아래의 댓글 란을 참고할 것)

이제 값이 저장된 위치를 알았으니, dq 명령어를 이용해 해당 메모리 위치의 값을 조회해 볼 수 있다.

paramback

 

마지막 팁으로, kv 명령어가 스택 상의 네 개의 홈 공간을 출력으로 보여준다는 사실을 이용하면 이 값을 더 빨리 알아낼 수도 있다. RSI의 값은 R9 홈 공간에 저장되었으므로 (RSP+0x20), kv 명령어를 실행하고 여기에 출력된 DeleteNameCacheNodes 항목의 네 번째 값을 확인하면 된다.

kveasy

(번역 끝)

 

이로서 http://analyze-v.com/에 있는 글 번역의 시리즈는 일단락 되었다. 원문이 영어인지라, 지금 읽었던 글을 정리해두지 않으면 금방 까먹을 것 같아서.. 미래의 날 위해 날림이지만 번역해 올려둠.

위의 글들도 좋지만 사실 x64 명령어 call의 동작 방식을 이해하는 것이 콜스택을 분석하는데 큰 도움이 되는데, 이 부분은 따로 다시 한 번 더 정리해야겠다.

'프로그래밍 팁 > C++' 카테고리의 다른 글

vector::shrink_to_fit  (0) 2013.12.24
Reconstructing parameters from x64 crash dumps  (0) 2013.07.17
x64 Stack Frame layout  (0) 2013.07.04
x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

발번역 합니다. 원본 : http://analyze-v.com/?p=468

이전 글(의 발번역)을 보려면 http://devnote.tistory.com/249

 

이 글을 적고 있는 시점은 뉴잉글랜드 패트리어츠 경기의 하프타임이다. x64 컴파일러가 스택을 다루는 방법에 대해 이야기하기 딱 좋은 시간이다.

윈도우즈의 x64 컴파일러는 함수의 prolog 영역을 처리하는 동안 스택을 조작하는 모든 동작을 수행한다. (그렇지는 않다는 댓글도 달려 있음. 아래쪽 Update 단에 적혀있다.) 이 과정에는 값을 유지해야할(non-volatile) 레지스터들의 값을 스택에 저장하고, 지역 변수들을 위한 공간을 할당하고, 호출한 함수쪽으로 전달해야 할 인자들을 위한 공간도 할당하게 된다. (처음 네 개의 인자들은 레지스터를 통해 전달되고, 나머지 인자들이 스택을 통해 전달된다는 사실을 기억할 것.)

이러한 동작 중에는 x64 컴파일러가 스택을 사용하는, 또하나의 흥미로운 특성을 포함한다. 모든 서브루틴들은 호출자쪽이 서브루틴을 위한 용도의 ‘홈’ 공간을 마련해주어 이를 사용할 수 있다는 점이다. 이 공간은 레지스터들 (RCX, RDX, R8, R9)를 통해서 전달되는 처음 네 개의 인자들을 저장하기에도 충분한 공간이다. 그리고 이 공간은 다섯 번째 인자가 저장되는 공간의 앞부분에 마련된다.

아래의 그림을 보면 보다 분명하게 이해될 것이다. 서브루틴으로 진입할 때, RSP는 루틴이 되돌아가야 할 번지를 담고 있는 곳의 위치를 가리킨다. RSP+8h는 RCX 홈 공간을, RSP+10h는 RDX홈 공간을 가리킨다.

x64-frame1

 

하지만 여기서 또다시 중요한 점은, 스택에 마련되는 이 공간은 단지 RCX, RDX, R8, R9 레지스터 값을 유지하기 위한 용도로만 사용되는 것은 아니라는 점이다. 어떤 경우는 함수의 prolog 코드가 이 공간에 레지스터행 인자들을 저장하기도 하지만, 또 다른 경우에는 여기에 subset을 저장하기도 한다. 또 어떨 때는 전혀 상관없는 다른 레지스터 값들을 이곳에 저장할 수도 있고, 이 공간을 무시한 채 아무 것도 저장하지 않을 수도 있다. 사실상 이 공간은 서브루틴의 입장에서 적합한 용도로 사용할 수 있는 여분의 공간(scratch space)이라고 보아도 무방하다.

이제 우리는 임의의 x64 크래시를 분석할 때 이 여분의 공간을 좀 더 광범위하게 활용할 수 있는 방법에 대해 이야기 해보려 한다. 이 공간은 함수로 전달된 인자들과 데이터 유지용(non-volatile) 레지스터들의 값이 덤프로 붙잡은 현재 프레임에서는 보이지 않을 때, 이를 알아내기 위한 최고의 장소로 쓰일 수 있다. 다음 포스팅에선 이 공간을 이용해 함수로 전달된 파라미터 값을 손쉽게 재구성하는 방법에 대해 이야기 해 보자.

(막 경기 후반전이 시작되는 시점에 타이핑을 끝냈다!)

업데이트

익명의 댓글에서 흥미로운 사실이 달렸다.

중요하게 짚고 넘어가야 할 부분이 있습니다. 스택의 조작은 prolog 코드가 수행되는 동안에 이루어지는 것은 맞지만, RSP 레지스터는 함수의 본체가 실행되는 동안에 변경되기도 합니다. 예를 들면, 함수 _alloca()를 썼다든가 할 때에요. (누구 다른 케이스를 알고 계신 분?)

이러한 동작은 커널모드의 프로그래밍 동작에서는 흔치 않은 일이라, 위 글에 답글을 적지는 못했다. 이런 케이스에 대해 알고 있는 사람은 우리에게도 알려주었으면 한다.

(번역 끝)

본문은 여기까지고, 달린 댓글 중에 참고할 만한 게 하나 더 있다면

FYI, the stack grows towards low addresses. So, the picture is kinda upside down if you`re trying to see a ‘stack’

참고로 덧붙이자면 스택은 주소 번지가 낮은 쪽으로 자라납니다. 그러니 위 이미지에서 스택을 나타내려는 것이라면 거꾸로 표현되어야겠지요.

I tend to like to show these diagrams with the highest address on the top instead of the bottom. As you mentioned, the stacks grown down so starting at the top and working down makes sense to me. I can see the argument for going the other way as well and if you do a Google image search there’s a pretty decent mix of both.

(글쓴이의 답변글) 나는 의도적으로 이 다이어그램에서 높은 주소번지를 아래쪽이 아닌 위쪽으로 보이도록 하였습니다. 님께서 언급하신 것 같이, 스택은 아래쪽으로 자라나기 때문에 위쪽에서 시작해서 아래쪽으로 쌓여가도록 표현하는 것이 제가 생각하기에도 상식적입니다. 하지만 스택 방향을 꼭 그렇게 맞춰 표현하지 않으면 어떻느냐는 의견도 많이 있고요, 구글에서 이미지 검색을 해보면 두 가지 방식의 표현 모두가 적절히 사용되고 있는 걸 확인할 수 있습니다.

(답변 번역도 끝)

 

Note : x64에서 스택은 거꾸로 자란다(down-growing). 나같은 초보들은 이미지만 보고 넘어가다가는 오해할 수도 있겠군. 첨부된 이미지를 보면, low / high 주소공간 위치를 거꾸로 그린 것은 둘 째치고, 스택이 분명 거꾸로 자란다고 되어있으면서, 설명하는 공간들은 RSP를 기준으로 높은 주소번지로 자라 나가는데, 이것은 서브루틴이 사용할 스택의 공간 방향이 아니다. 위의 그림 기준에서라면 함수의 로컬변수 등은 아래쪽에 넣게 된다.

image

실제로 테스트 코드를 만들어 확인해보면 바로 알 수 있는데, 위에 빨간 동그라미를 보면 첫 번째 인자로 받은 int 변수 b의 값을 [rsp+8]에 넣고 있다. 하지만 아래 push rdi 명령을 보면 rsp보다 낮은 주소번지로 값이 들어간다.

일단 첨언은 여기까지만. 한동안은 x64 asm 글을 계속 올릴 듯 한데 좀 더 자세히 정리할 기회가 있겠지.

'프로그래밍 팁 > C++' 카테고리의 다른 글

vector::shrink_to_fit  (0) 2013.12.24
Reconstructing parameters from x64 crash dumps  (0) 2013.07.17
x64 Stack Frame layout  (0) 2013.07.04
x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

발번역 합니다. 원본은 : http://analyze-v.com/?p=458

x64 덤프를 다루기 위해서는 호출규약(Calling Convention)에 대해 이해할 필요가 있다. 이를 알고 있어야 특정 함수로 전달하는 인자(parameter)들을 확인한다던지 하는 일들이 가능하다.

가장 기본적인 룰은 함수로 전달하는 처음 네 개의 인자들은 레지스터를 이용하며, 나머지 추가 인자들은 스택을 통해 전달된다는 것이다. 보통 사용되는 레지스터들은 아래와 같다.

  • Parameter 1 – RCX
  • Parameter 2 – RDX
  • Parameter 3 – R8
  • Parameter 4 – R9

(주의: 실수형 인자를 사용할 때 같은 특정 케이스에서는 별도의 규칙이 적용된다. 이에 대한 전체적인 세부설명은 이 곳에서 확인할 수 있다.)

예를 들어, 만약 어떤 루틴이 시작점에서 RDX 레지스터에 접근한다면, 이 루틴은 두 번째 인자에 접근하고 있음을 확인할 수 있다. 또한 이를 통해 유추할 수 있는 것은, 루틴을 호출하는 곳에서는 이보다 더 앞선 프레임(frame)에서,  RDX 레지스터에 올바른 두 번째 인자 값을 채워 넣었어야 한다는 점이다. (역자 주 – 함수가 시작하자 마자 인자값에 접근을 하니까, 함수가 시작되기 전에 이미 레지스터에 값이 쓰여져 있어야 하고, 이는 호출자(caller)의 역할이라는 뜻)

rdx

이러한 호출 규약에는 한 가지 중요한 이슈가 있다. 컴파일러가 함수의 처음 네 개의 인자들을 휘발적으로(volatile) 처리한다는 점인데, 다시 말하자면 서브루틴 호출이 진행되는 동안 이 값들이 유지되지 않는다는 말이다. 서브루틴이 처리되는 과정에서 컴파일러는 이 네 개의 레지스터에 아무 연관 없는 다른 값들을 덮어써버릴 수 있다. 이러한 동작 방식은 x64에서 인자들의 값을 다시 복원하기 어렵게 만들어 버린다.

다음의 포스팅에서는 x64 컴파일러가 스택을 활용하는 방법에 대해 이야기할 것이다. 인자로 전달된 값들을 알고 싶을 때, 스택을 이용해서 원하는 정보를 얻을 수 있는 자세한 방법에 대해 논의해 보자.

'프로그래밍 팁 > C++' 카테고리의 다른 글

Reconstructing parameters from x64 crash dumps  (0) 2013.07.17
x64 Stack Frame layout  (0) 2013.07.04
x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Direct2D 레퍼런스 모음  (0) 2011.06.03
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

Faux variadics

2013. 6. 21. 15:18 from 프로그래밍 팁/C++


원본 출처 : http://msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx

아래는 발번역.

vs2012의 C++은 가변 인자 템플릿을 흉내내는 새로운 구문을 가진다. vs2008 SP1과 vs2010에서는, 매크로 정의를 바꿔가며 하위 헤더파일을 반복적으로 인클루드하는 방식으로 인자 개수가 다른 오버로드를 찍어내는 식이었다. 예를 들어, <memory> 헤더는 하위 헤더인 <xxshared>를 반복적으로 인클루드하며 make_shared<T>(args, args, args)를 찍어냈다. vs2012에서는 이런 하위 헤더파일이 사라졌다. 이제는 가변 인자 템플릿이 매크로를 이용해 정의되고, 주 매크로(master macro)를 이용해 확장된다. 이러한 내부 구현의 변화는 다음과 같은 변화를 만든다.

  • 코드의 유지/보수성이 좋아지고, 사용하기 쉬워졌다. (하위 헤더 파일을 인클루드 하는 것은 제법 가볍지 않은 작업이다) 또한 코드의 가독성도 높아졌다.
  • 디버거에서 코드 안을 확인하기가 더 어려워졌다 – 미안하게 생각한다! (헐…)
  • std::pair의 생성자인 pair(piecewise_construct_t, tuple<Args1…>, tuple<Args2…>) 에 흥미로운 현상이 있는데, 이 생성자는 N2개의 오버로드를 가진다. (10개의 인자를 가지는 tuple를 지원할 경우 총 121개의 오버로드를 가지는데, 인자를 받지 않는 생성자를 포함해 계산하기 때문이다.) 11 * 11 = 121.

다량의 pair-tuple 객체 오버로드 생성과, 위치 변경 오버로드(emplacement overload)까지 더해져서 컴파일 시점에 많은 양의 메모리를 필요로 하게 되었다. 그래서 우리는 인자 개수 제한을 줄였다. vs 2008 SP1과 vs 2010에서는 10개 까지의 인자 개수가 지원되었다 (가변 템플릿 인자가 0개에서 10개까지 제공된다는 의미). 하지만 vs2012에서는 디폴트로 인자 개수 제한을 5개로 낮추었다. 이렇게 해서 컴파일러의 메모리 사용량을 vs2010때와 유사하게 조정해 두었다.

만약 더 많은 수의 인자를 사용해야 한다면 (6개 타입을 갖는 tuple이라든지) 이를 위한 탈출구도 마련되어 있다. 프로젝트 전역으로 _VARIADIC_MAX 매크로를 5~10 사이의 값으로 정의하면 된다. 이렇게 하면 더 많은 메모리를 필요하게 되기 때문에, 컴파일러 옵션에 /Zm을 사용해 pre-compiled header를 위한 메모리 공간을 더 확보해야 할 수도 있다.

(번역 끝.)

 

지금 우리 프로젝트가 _VARIADIC_MAX = 10을 사용하고 있는 상황. 이 때문에 pch 사이즈가 엄청 커져서 컴파일에 file I/O도 엄청 늘었고, 빌드 시간도 많이 느려졌다. 지금도 빌드 걸어놓고 기다리다가 심심해서 발번역이나 한 번 해 봄.

야 이 M$ 잡것들아 가뜩이나 욕먹는 default STL에 이상한 짓좀 하지 말어 ㅜㅠ… 그리고 이런 요상한 것 할 시간 있으면 차라리 variadic template 문법적으로 제대로 추가하는 작업이나 서둘러 해라 ㅜㅠ…

'프로그래밍 팁 > C++' 카테고리의 다른 글

x64 Stack Frame layout  (0) 2013.07.04
x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Direct2D 레퍼런스 모음  (0) 2011.06.03
[vs2005] __assume 키워드  (0) 2011.05.25
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

type traits

2012. 2. 14. 15:18 from 프로그래밍 팁/C++

traits를 네이버 영한사전으로 검색해보면 특질, 자질 등의 의미를 가진다. EC++ 3판 #47에서는 특성정보라고 번역되어 있고, The C++ 표준 라이브러리 확장 튜토리얼 및 레퍼런스 11장에서는 type traits 전체를 ‘형식 특질’이라고 번역하고 있다.

템플릿 코드를 읽다 보면 traits를 아주 잦은 빈도로 만나게 된다. STL의 컨테이너와 반복자들의 구현에도 많이 사용되었다. 템플릿의 인자로 전달되는 타입을 컴파일 할 때 알아내고 싶을 때 사용하는데, 인자의 타입 뿐 아니라 다른 부가적인 정보들도 더 확인할 수 있다. 간단하게는 포인터형인지(is_pointer) 혹은 배열인지(is_array) 하는 것도 확인할 수 있고, 가상 소멸자를 가지고 있는지 여부(is_virtual_destructor)나 POD인지 여부(is_pod)도 확인할 수 있다.

pod(plain old data)라는 단어 정의를 오늘 처음 접했다. 그리고 컴파일 시점에 pod 여부를 확인하는 것이 가능 하다는 사실에 놀랐다. 이게 가능하다면 네트워크 프로그래밍에서 패킷을 serialize할 때 메모리 복사를 잘못 시행하는 코드를 미리 잡아낼 수 있다. 좀 더 미리 알았더라면 예전에 삽질을 많이 줄일 수 있었을 텐데 아쉽다 ㅠ

아무튼 다시 traits 이야기로 돌아와서..

기본적으로 traits가 무엇이고, 어떤 유익이 있는가 감을 잡기 위해서는 EC++의 챕터 7 템플릿과 일반화 프로그래밍 부분을 읽어보는 것이 좋다. (#41~#48) 여기 설명된 대로 traits는 은근슬쩍(?) 암묵적인 관례들이 하나 둘씩 모이면서 다듬어 졌는데, 그래서 예전부터 traits를 구현한 라이브러리를 여러 곳에서 볼 수 있다. 내가 알고 있는 것만 적어봐도

이중에 boost의 traits는 Technical Report1을 거쳐 현재는 표준 라이브러리에 들어가 있으므로, 이제 vs2008 sp1 이상이라면 #include <type_traits>만 추가하면 별도 라이브러리 없이 바로 사용 가능하다. 컴파일러 자체 제공 traits도 있지만 표준 라이브러리를 쓰는 게 좀 더 마음이 편안하달까 +_+).. 관련된 설명은 msdn에도 있지만 C++ 표준 라이브러리 확장 책에도 잘 설명되어 있다.

템플릿은 역시 만만하지 않다. 지금보다 많이 초보였던 시절 제대로 다 이해 못하고 팽개쳐 두었던 책들을 지금 다시 읽으니 이젠 대충 이해가 가기는 한데, 실제로 코딩할 때 적극적으로 사용해 보려고 하면 많이 헷갈린다. 다음 번 토이 프로젝트에는 메타 프로그래밍을 좀 더 익힐 수 있도록 해야겠다.

예전에 진택이형이 사용한 int2type이라는 템플릿 구조체를 처음 보고 정말 신기해했던 기억이 난다. 공부하다 보니 Modern C++의 저자 알렉산드라쿠스가 소개한 기법이라고 한다. 이제는 동일한 기법이 std::true_type, std::false_type으로 깔끔하게 정리되어 표준 라이브러리에 들어가있다. 이걸 활용해 실행시간의 조건비교를 컴파일 타임 비교로 교체하는 예제 코드를 만들어봤다.

아래 코드는 Windows 2008부터 제공하는 슬림 리더라이터락 (SRW)을 scoped lock 형식으로 사용할 수 있게 해주는 헬퍼 클래스다. 락의 경우가 읽기와 쓰기 두 종류여서, lock을 걸 때 어떤 타입인지를 별도의 인자로 받게 작성되어 있다.

class Locker
{
public:
    Locker( SWRLOCK& lock, bool bExclusive )
        : m_lock( &lock )
        , m_bExclusive( bExclusive )
    {
        if( m_bExclusive )
        {
            while( !::TryAcquireSWRLockExclusive( m_lock ) )
                Sleep( 0 );
        }
        else
        {
            while( !::TryAcquireSRWLockShared( m_lock ) )
                Sleep( 0 );
        }
    }
 
    ~Locker()
    {
        if( m_bExclusive )
            ::ReleaseSRWLockExclusive( m_lock );
        else
            ::ReleaseSRWLockShared( m_lock );
 
        m_lock = NULL;
    }
 
protected:
    PSRWLOCK    m_lock;
    bool        m_bExclusive;
};
 
SRWLOCK g_rwLock;
 
Locker lock( g_rwLock, true );    // 쓰기 락을 걸 때
Locker lock( g_rwLock, false );   // 읽기 락을 걸 때

공유 리소스에 접근할 때 읽기 락이 필요한지 쓰기 락이 필요한지는 코드에서 이미 결정되어 있다. 하지만 위에 작성된 것처럼 코드를 짜면 실행시간에 매번 잠금의 종류를 확인하기 위해 조건문을 수행해야 한다.

type traits를 이용해 간단히 고쳐보면 아래와 같이 된다.

#include <type_traits>
 
template<bool bExclusive> class Locker
{
public:
    Locker( SWRLOCK& lock ) : m_lock( &lock )
    {
        Lock( std::integral_constant<bool,bExclusive>() );
    }
 
    ~Locker()
    {
        Unlock( std::integral_constant<bool,bExclusive>() );
    }
 
protected:
 
    void Lock( std::true_type )
    {
        while( !::TryAcquireSWRLockExclusive( m_lock ) )
            Sleep( 0 );
    }
 
    void Lock( std::false_type )
    {
        while( !::TryAcquireSRWLockShared( m_lock ) )
            Sleep( 0 );
    }
 
    void Unlock( std::true_type )
    {
        ::ReleaseSRWLockExclusive( m_lock );
        m_lock = NULL;
    }
 
    void Unlock( std::false_type )
    {
        ::ReleaseSRWLockShared( m_lock );
        m_lock = NULL;
    }
 
protected:
    PSRWLOCK    m_lock;
};
 
SRWLOCK g_rwLock;
 
Locker lock<true>( g_rwLock );    // 쓰기 락을 걸 때
Locker lock<false>( g_rwLock );   // 읽기 락을 걸 때

막상 다 해놓고 읽어보면 별로 대수로울 거 없는데 직접 작성할 때는 한참 헤맸음. 읽어서 이해되는 거랑 직접 써보는 거랑 차이가 많이 난다.

TMP 코드들이 예전에 봤을 때보단 쉽게 읽히는 걸 보니 그래도 조금씩 성장하고 있는 것 같아 기분이 좋다.  팽개쳐 두었던 템플릿 책들 다시 읽어봐야겠다.

See Also:

'프로그래밍 팁 > C++' 카테고리의 다른 글

x64 Calling Convention  (0) 2013.07.04
Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Direct2D 레퍼런스 모음  (0) 2011.06.03
[vs2005] __assume 키워드  (0) 2011.05.25
[visual studio camp #1] C++0x와 Windows7  (0) 2011.02.13
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

[0602 박민근] Direct2D
View more presentations from jacking

어지간한 내용은 MSDN 페이지에 다 링크되있다. (http://msdn.microsoft.com/en-us/library/dd370990(v=VS.85).aspx)

'프로그래밍 팁 > C++' 카테고리의 다른 글

Faux variadics  (0) 2013.06.21
type traits  (0) 2012.02.14
Direct2D 레퍼런스 모음  (0) 2011.06.03
[vs2005] __assume 키워드  (0) 2011.05.25
[visual studio camp #1] C++0x와 Windows7  (0) 2011.02.13
[C++0x] R-value reference 때문에 인터페이스 설계가 바뀌는 중~  (2) 2011.01.27
Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

http://msdn.microsoft.com/en-us/library/1b3fsfxw(VS.80).aspx

 

컴파일러의 optimizer에게 런타임의 상태에 대한 힌트를 제공해주는 키워드. 단순히 제안일 뿐이며 (it is only a suggestion) 컴파일러가 무시할 수도 있지만 (so the compiler is free to ignore it.) 잘만 사용하면 적잖은 성능 향상을 얻을 수 있다.

vs2005 변경사항을 뒤늦게 많이 보게 되는군. 아마 조사해보면 더 나오겠지 +_+
역시 세상에 공부할 건 너무 많아 ~

Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

100828 [visual studio camp #1] C++0x와 Windows7
View more presentations from sung ki choi.

Posted by leafbird 트랙백 0 : 댓글 0

댓글을 달아 주세요

현재 작업중인 프로젝트에서 얼마 전부터 vs2010을 사용하고 있다.
 
처음에는 그냥 컴파일러 버전만 좀 올라가고… 코딩에는 고작해야 auto 키워드를 쓰는 것 정도가 다였는데
이제 슬슬 vs2010에서 코딩 하는 양이 많아지면서 C++0x의 새로운 문법들을 자연스럽게 활용해가고 있다.
 
그 중에 요즘 조금씩 느끼고 있는 것 중에 한가지가, r-value reference를 이용한 이동 생성자(move constructor)를 사용하게 되면서, 내가 작성하는 모듈 인터페이스의 설계가 바뀌고 있다는 사실이다.
그 동안 C++에서 객체의 불필요한 복사를 피하기 위해 다년간 체득했던 방식들이 조금씩 변화하는 중.
 
코드로 예를 들어보면 아래와 같은 경우다.
 
// GetInfo 함수는 DWORD로 표현된 ip 주소와 port 정보를 받아
// 문자열로 된 x.x.x.x:xx 방식의 ipv4 주소를 반환합니다.
std::string GetInfo( DWORD dwIP, unsigned short usPort )
{
    SOCKADDR_IN addr;
    addr.sin_addr.s_addr = dwIP;
 
    char szBuff[16] = {0};
 
    // RVO 최적화를 피하기 위해 아래와 같이 코딩했습니다. 
    // 물론 더 효율적인 코딩도 가능합니다. 아래는 예시를 위한 샘플 입니다.
    std::string strBuff;
    strBuff = inet_ntoa( addr.sin_addr );
    strBuff += ":";
    strBuff += itoa( usPort, szBuff, 10 );
 
    return strBuff;
}
 
void main()
{
    std::cout << GetInfo( 0x164e8b, 20 ) << std::endl;
}
 
GetInfo() 함수는 내부에서 뭔가 의미심장한 일을 한 후 그로 인한 string 타입 결과값을 함수의 리턴 값으로 반환한다.
main 함수에서는 이 함수를 스트림 형식으로 출력하는 std::cout의 << 연산자에 그대로 호출하고 있다.
호출하는 곳에선 이렇게 하는 게 훨씬 깔끔하다.
하지만 문제는 GetInfo가 std::string을 반환할 때 복사 생성자가 호출되고, 이때 문자열 데이터가 메모리상에서 불필요하게 복사된다는 점이다.
 
그래서 대부분 어느 정도 숙련된 C++ 개발자라면 오버헤드를 없애기 위해 GetInfo의 인터페이스를 아래와 같이 수정할 것이다.
 
// 반환할 스트링 변수를 레퍼런스로 받아 이곳에 값을 채우도록 변경.
void GetInfo2( std::string& strOut, DWORD dwIP, unsigned short usPort )
{
    SOCKADDR_IN addr;
    addr.sin_addr.s_addr = dwIP;
 
    char szBuff[16] = {0};
 
    strOut = inet_ntoa( addr.sin_addr );
    strOut += ":";
    strOut += itoa( usPort, szBuff, 10 );
 
}
 
void main()
{
    std::string strInfo;
    GetInfo2( strInfo, 0x164e8b, 20 );
    std::cout << strInfo << std::endl;
}
 
그렇죠? 아마 이 글을 찬찬히 보고 있으신 C++ 프로그래머라면 머릿속에 이런 코드를 생각하고 있으셨을 겁니다.
 
그런데 visual studio 2010 부터는, 처음의 인터페이스인 std::string GetInfo(…) 스타일로 작성하고 RVO나 NRVO같은 컴파일 최적화의 도움을 받지 않아도 객체복사가 일어나지 않는다. 그 이유는 std::string 객체의 이동생성자 때문이다.
 
불필요한 메모리 복사의 문제만 아니라면 첫 번째 인터페이스가 사용하기에도 편하고 작성하기도 좀 더 편하다.
컴파일러 처음 갈아타고 나서는 이동생성자를 명시적으로 호출해주기 위해서 반환시점에 return std::move( strBuff ); 와 같은 식으로 처리했는데, 나중에 테스트코드를 돌려보니 std::move()를 사용하지 않아도 이동 생성자가 불린다.
 
근데 아직 함께 일하는 다른 프로그래머들도 새로운 문법에 적응하기 위한 시간을 거치는 중일 테니, 기능상으로는 쓸모 없더라도 문서화의 목적으로 return std::move( 리턴할 객체 ); 식으로 코딩해주는 것도 하나의 센스일 듯.
이 부분은 수정이 조금 필요하다. 실제로 나는 팀에서 이동 생성자를 의도한 코드에서는 std::move(...)를 문서화의 의미로 좀 남발하였는데, 이것은 std::move의 올바른 사용방식이 아니며, r-value의 동작 방식을 제대로 알고 있는 동료들에게는 오히려 혼동을 줄 수도 있겠다는 피드백을 받았다. 
  • 함수의 반환값은 무조건 r-value로 처리된다. 이것으로 인해 리턴값에 std::move를 쓰는 것은 이미 어색한 코드가 된다. 
  • std::move는 l-value를 r-value로 넘기기 위한 경우에만 쓰면 된다. 
  • std::move를 안 쓴다고 해서 move가 가능한 곳이 copy 되어버리는 케이스는 없다. 무슨 말이냐 하면, 객체를 이동 생성 하느냐, 복사 생성 하느냐 하는 이슈는 std::move를 적거나 빼먹었거나 하는 것에 달려있지 않다는 말이다.
  • 따라서, std::move는 처음에 썼던 것처럼 여기저기 막 붙이는 것이 아니라, 사용하지 않고는 컴파일 에러를 해결할 수 없는 케이스에만 사용하면 된다. 
 
어쨌거나 요즘은, 객체 복사의 불안함 때문에 반환값 객체를 레퍼런스로 받도록 하는 함수 인터페이스를 점점 안 쓰게 됐다. 적다가 보니 r-value reference에 대한 공부도 좀 더 깊이 있게 해둬야겠다 싶구나.


ps. r-value의 간단한 개념은 흥배형님의 핸드북에 친절히 설명되어 있다.


Posted by leafbird 트랙백 0 : 댓글 2

댓글을 달아 주세요

  1. addr | edit/del | reply Favicon of https://devnote.tistory.com BlogIcon leafbird 2013.10.03 00:06 신고

    2013.10.13 - 본문 내용 중 std::move(...)의 사용에 대한 부분을 수정.

  2. addr | edit/del | reply Favicon of http://www.npteam.net BlogIcon TTF 2013.10.03 13:42

    좋은 정보 감사합니다.