programming

std::unique_ptr을 사용하자

07/03/2018 c++, programming No comments

최신의 c++에서 동적메모리 할당에 raw pointer를 사용하는 것보다는 std::unique_ptr을 사용해야 할 이유는 충분히 많이 있다.

std::unique_ptr의 사용해야 할 이유가 생소한 사람은 “스콧 마이어스의 Effective Modern C++” 부터 읽어보길 추천한다.

여기서는 생각해볼 문제로 2가지 관점에서 std::unique_ptr을 써야 하는 이유를 이야기해보고자 한다.

 

1. 예외로 부터 보호하기

c++에서는 RAII를 활용하는 것이 정말 좋다. 즉, raw pointer보다 std::unique_ptr이 더 좋다.

만약 CQueue::Add 함수가 예외를 던진다면 어떻게 될까?

UseRawPointer(); 함수는 메모리릭이 발생하지만, UseUniquePointer(); 함수는 메모리릭이 발생하지 않는다.

 

2. 잘못 쓰기 어렵게 만들기

예를 들어서 CStream이라는 클래스가 있고, 생성자에서 CBuffer*를 입력해줄 것을 요구한다고 하자.

CBuffer* 를 할당을 해서 넣어주어야 하는 것일까? 그렇다면 해제는 CStream이 하는것인가? 외부에서 따로 해야 하는 것인가?

new를 안해도 되는 것일까?

 

실제로 개발을 하다 보면, 반드시 new를 요구하는 케이스가 있고, 그렇지 않은 케이스가 있다.

개발하면서는 기존에 사용한 코드를 참조하거나, 내부의 구현을 보고 적당히 잘 쓰는게 현실이다.

나같은 경우는 c++에 move 연산자가 추가되면서 부터는 new를 요구하는 곳에서는 std::unique_ptr을 사용하려고 하고 있다.

(인터페이스의 일관성 문제 등도 있고, 협업의 관점도 있고, 이미 진행된 코드에서는 그렇게 하지 못하지만…)

 

Spin-Loop

05/31/2018 c++, programming No comments

멀티쓰레딩 프로그래밍에서 락을 잡는 타이밍이 짧다면 이론적으로 spin lock을 사용하여 busy waiting을 하는 것이 성능에 좋다고 알고 있다.

하지만, 실제로 spin lock으로 효과를 보기 쉽지 않은데, CPU에 이를 위해 pause라는 명령어가 존재한다.

spin lock이라는게 결국 변수의 값이 원하는 값이 될때까지 계속 loop를 돌리는 것인데, pause라는 명령어를 통해 CPU에게 지금 spin loop를 돌고 있다는 힌트를 주고, CPU가 메모리버스 속도 등을 감안해서 변수가 바뀌는데 필요한 최소한의 시간만큼 다음 명령어를 실행하지 않는 것이다.

– 참고 https://software.intel.com/sites/default/files/m/d/4/1/d/8/17689_w_spinlock.pdf (w_spinlock.pdf)

이를 테스트 해보았다.

인텔계열의 CPU에서는 _mm_pause();라는 함수로 pause를 사용 할 수 있다.

  • Visual Studio에서는 YieldProcessor(); 라는 매크로 함수도 동일하다.

간단하게 코드를 보면

실제로 pause 명령어를 사용했을 때와 안했을 때를 테스트 해봤을 때….

어마어마한 차이가 났다.

 

테스트 케이스는 I7-6700K CPU (8코어) 에서 8개의 쓰레드에서 스핀락을 걸고, 스핀락이 성공하면 Sleep(0)로 잠깐 휴지기를 주었으며, 시간 측정은 spin-loop 구간만 측정하였다.

결과는…

실행할때마다 결과가 다르긴 한데… 10배이상의 차이가….

std::vector로 배우는 레벨업 #2

01/18/2018 c++, programming No comments , ,

std::vector로 배우는 레벨업 #1에 이어서…

resize를 구현해 보자

아래 테스트 코드를 통해 동작을 확인해보자.

 

std::vector로 배우는 레벨업 #1

01/17/2018 c++, programming No comments , ,

선행학습: std::vector의 size와 capacity에 대해서 제대로 알고있자

std::vector의 push_back과 reserve를 한번 직접 구현해보자..

언뜻 생각해보면 template에 대해서 조금만 알고 있으면 다음과 같이 쉽게 구현 할 수 있을 거라는 생각이 들 것이다.

한번 다음의 테스트 코드로 std::vector과 MyVector의 출력결과가 어떻게 다른지 확인해 보면….

push_back을 했을때 복사생성자가 호출되느냐 대입연산자가 호출되느냐는 c++ 버젼에 따라 차이가 있을 수 있으니, 동일하다고 치고,

stl버젼은 reserve를 했을때 생성자가 호출되지 않지만, MyVector는 생성자가 2번 호출되는 차이점을 볼 수 있다.

이 차이가 의미하는 것은 무엇일까?

 

눈치가 빠른분들은 이미 감을 잡으셨을 것이다.

그렇다. vector에서 reserve함수를 통하여 capacity의 크기를 늘린다는 것은 메모리공간만 할당하는 것이지, 실제 객체가 생성되는 것은 아니다.

혹시, 아직 c++에서 객체 생성의 의미를 정확히 모르고 있었다면, 이제는 정확히 이해 했을 것이다.

(잘 모르겠으면 ‘c++에서 malloc과 new의 차이점‘을 읽어보자)

 

그럼 한번 요구사항에 맞게 코드를 고쳐보자

이제 std::vector와 동일한 결과가 나옴을 확인 할 수 있을 것이다.

c++에서 malloc과 new의 차이점

01/17/2018 c++, programming No comments

제일 중요한 포인트는 생성자의 호출 유무이다.

  • malloc
    • 동적 메모리를 할당한다.
  • new
    • 동적 메모리를 할당한다.
    • 생성자를 호출한다.

마찬가지로, free와 delete의 핵심 차이점은 소멸자의 호출 여부

기타 차이점으로는

  • 호출 하는 방법이 다르다.
  • new 의 경우에 메모리 할당에 실패하면 예외(std::bad_alloc)가 발생한다.

참고

https://msdn.microsoft.com/library/kftdy56f.aspx

http://en.cppreference.com/w/cpp/memory/new/operator_new

std::vector의 size와 capacity에 대해서 제대로 알고있자

12/11/2016 c++, programming 1 comment ,

std::vector를 제대로 알고, 제대로 쓰고 있는가를 파악할 때 내가 자주 하는 질문은

reserve와 resize에 대해 설명해 보세요.

의외로 많은 프로그래머가 제대로 대답하지 못한다.

저 질문을 대답하기 위해서는 size와 capacity에 대해서 잘 알고 있어야 하는데..

사실, std::vector을 사용하기에 앞서 어떻게 구현이 되어있을까? 라고 고민을 해보면 추측할 수 있는 문제가 아닌가 하는 안타까움이 들때가 많다.

이제 막 입문하는 사람은 그렇다 치고, 어느정도 사용하다보면, 당연히 가지게 되는 의문과 고민, 추측이 아닌가?

그것은 나만 기대하는 욕심인 것인가?

위 그림을 보면 대부분은 이해할 것이라고 생각한다.

capacity는 vector의 요소(element)들을 담을 수 있는 메모리가 할당되어 있는 공간의 용량이다.

size는 실제 유효한 요소(element)들의 갯수이다.

예를 들어, push_back함수를 통해 새로운 요소(element)를 맨뒤에 한개 추가할 때,

capacity > size보다 큰 상황이면 그냥 맨뒤의 공간에 요소를 복사해 넣고 ++size만 일어나며,

capacity == size 이면 capacity가 증가한 새로운 연속된 공간을 할당하고… 기존의 모든 요소들을 복사하여(기존 정보는 지우고..) capacity > size인 상황을 만들어 놓고, 맨뒤에 요소를 복사해 놓고, ++size를 하는 과정이 일어나는 것이다.

reserve는 이 capacity를 프로그래머가 원하는 크기로 할당을 해준다.

여기까지 size, capacity, reserve에 대해서 이해했으리라 믿고, Effective STL의 항목 14. reserve는 필요 없이 메모리가 재할당되는 것을 막아 준다.(Use reserve to avoid unnecessary reallocations)를 공부해 보길 바란다.

여기서는 resize에 대해서 조금 더 이야기를 해보려고 한다.

resize에 대해서 다음과 같이 설명하는 사람에게는 나는 70점을 줄 것이다.

지정한 수 만큼 요소를 만들어 준다. 내부에서 capacity가 작으면 재할당이 일어나며…

resize에서 한가지 더 중요한 것이 있는데, 증가되는 size만큼 복사생성자가 호출된다는 점이다.

(c++11까지는 복사생성자가 호출 됬었는데, 최신은 생성자가 호출된다.)

resize의 원형을 보자

C++ 11 이전 버젼이든 C++ 11 이후 버젼이든, 기본값을 따로 지정하지 않으면, default 생성자에 의해서 모든 요소가 초기화 된다는 사실은 동일하다.

과거에 이렇게 코딩한 사람을 본적이 있는데… 이는 resize의 스펙을 잘 모르고 한 바보같은….


참고

http://en.cppreference.com/w/cpp/container/vector/size

http://en.cppreference.com/w/cpp/container/vector/capacity

http://en.cppreference.com/w/cpp/container/vector/reserve

http://en.cppreference.com/w/cpp/container/vector/resize

Effective STL, 2001, Scott Meyers

 

가상 소멸자(virtual destructor)에 대한 정확한 이해

12/11/2016 c++, programming No comments

C++입문자들이 가상 소멸자에대해서 잘 모르는 것을 많이 봤다.

또, 어느정도 했다는 프로그래머중에서도 정확히는 모르는 사람들이 꽤 있드라…

“Effective C++ 항목7” 에 이 부분에 대한 설명이 있지만, 초보자들이 이해하기에는 다소 어려움이 있어 보여, 이부분에 대해 이해도를 높여 보고자 한다.

(다형성을 가진 virtual class에서) 가상 소멸자(virtual destructor)를 사용하지 않으면 문제는 무엇인가?

라는 질문을 했을때 다음과 같은 대답을 한다면 70점을 줄 것이다.

메모리 릭이 발생합니다

소멸자의 호출여부를 제대로 알고 있어야 100점이라고 할 수 있다. 대략

(파생 클래스의) 소멸자가 제대로 호출되지 않을 수 있어, 메모리릭을 포함해서 부분 소멸될 수 밖에 없는 문제점이 발생할 수 있습니다

C++은 생성자, 소멸자가 굉장히 중요하다는 것을 잊어서는 안된다.

생성자에서 락일 걸고, 소멸자에서 락을 해제하는 방법을 사용하며, 생성자에서 파일을 열고, 소멸자에서 파일을 닫는 방법을 사용하기도 한다.

위의 예에서 소멸자가 호출되지 않는다면 일어나는 문제점은 잘 알 수 있을 것이다.

메모리 릭이 발생한다고 대답하는 것은 왜 70점짜리 대답인지 자세히 알아보면,

Abstract의 소멸자에는 virtual 키워드가 없다.

이를 상속받은 Foo, Bar 2개의 class가 있는데,

Foo 클래스는 메모리릭 문제가 존재하지 않는다.

왜냐하면, Foo가 가지고 있는 데이터는 순전히 HeapManager에 의해서 할당/해제가 되기 때문에 소멸자의 호출여부와는 무관하기 때문

소멸자에서 하는 일이라고는 “~Foo()”를 출력하는 것 뿐

하지만, Bar 클래스는 메모리릭 문제가 발생 할 수 있다.

왜냐하면, char[]에대한 해제는 소멸자가 호출되어야만 발생 할 수 있기 때문

VS2015의 Heap Profile을 이용해서 확인해 보면,

~Foo()의 소멸자 호출과는 관계없이 메모리가 해제되는 2번째 파동을 볼 수 있다.

하지만, ~Bar()의 소멸자가 호출되지 않고 메모리가 해제된다면 생기는 4번째 파동은 볼 수 없다.

정상적으로 Abstract의 소멸자에 virtual을 붙인다면 어떨까?


참고

Effective C++, Third Edition, 2005. Scott Meyers

tuple의 모든 타입이 arithmetic type 인지 알아내는 템플릿 클래스

09/13/2016 c++, programming No comments ,

최근에 C#으로 코딩좀 하고 있는데…

역시 c++의 강력한 template이 그립다..

개인적으로 OOP형태의 언어에서는 c++의 template 때문에 c++을 버릴 수 없을 것 같다. ㅠ

필요할 것 같아서 작성했는데…

std::is_arithmetic 은 integral type(short, int, __int64 …) 이나 floating-point type(float, double) 이면 true이다.

이걸 tuple의 모든 항목이 true인지 검사하는 템플릿 클래스를 만들어 보았다.

참고로, std::is_arithmetic< std::tuple<…> >::value = false 이다.

응용하면 std::is_integral, std::is_floating_point, std::is_same 등도 만들 수 있다.

VS2015기준으로 작성되어있다.

swap을 사용하면 최적화 할 수 있다.

05/05/2016 c++, programming No comments , , , ,

Lock을 잡는 scope는 최소화 해야 하는 것은 잘 알고 있을 것이다.

swap을 활용하면 더 좋다는 것에 대해서 이야기 해보려고 한다.

개발을 하다보면 map을 만들어 놓고 객체를 등록하고, 찾고, 제거하는 일을 많이 하게 된다.

예를 들어 다음과 같은 Manager class를 만들어 사용한다고 하자

 

만약 Manager가 멀티쓰레드 환경에서 thread safe해져야 한다면,

lock free container를 도입하거나,

lock을 사용 해야 한다.

만약 lock을 사용한다면 …

보통 이렇게 할 것이다.

하지만, Remove함수에서 좀 더 최적화를 할 수 있는 여지가 남아있는데…

Remove 함수에서 lock을 잡은 후에 일어나는 일을 먼저 살펴 보면

1. _cont에서 id에 해당하는 node를 찾아서 지운다.
2. Foo의 소멸자가 호출되고, 메모리 해제가 된다.

1번의 과정은 정확히 lock이 필요하지만, 2번의 과정은 lock이 과정이 없어도 된다.
(여기서 Manager class의 lock은 map을 보호하는 것이지 class Foo 를 보호하는 것이 아님)

이 문제는 다음과 같이 swap을 활용하면 해결 할 수 있다.

뭔가 코드의 줄 수는 늘어나서 좋지 않아보이지만, 분명히 최적화는 되었다.

만약 Clear함수를 만든다면….

Clear() 보다 Clear2()가 코드 줄수는 조금 더 길지만, 최적화 되었다.

std::map(set) insert 정확하게 사용하기

04/17/2016 c++, programming No comments , , ,

꽤나 많은 프로그래머들이 std::map insert 의 사용을 잘 못 하고 있는 경우가 있어서, 오늘은 그것에 대해 이야기 하려고 한다.

우선 std::map의 insert의 기본형을 보면 다음과 같다.

pair<iterator,bool> insert (const pair<const Key, Type>& val);

여기서 중요한점은 리턴값으로 pair를 돌려주고 있는데, 이 리턴값의 의미를 정확히 이해 하고 있어야 한다.

insert 결과 첫번째(iterator) 두번째(bool) 언제 발생
성공 insert되어진 element의 iterator true 중복된 Key가 존재 하지 않을때
실패 중복된 Key의 element의 iterator false 중복된 Key가 존재 할때

insert를 한다는 것도 내부적으로 insert할 위치를 찾아가며, (이는 Red-black tree 를 구성 하려면 당연하다.)

std::map은 이를 활용 할 수 있도록 리턴값으로 제공하고 있다. (잘 활용하라는 의도)

많은 프로그래머들이 다음과 같이 사용하는 것을 종종 봤는데….

위의 리턴값의 의미를 정확히 이해하고 있다면, 이렇게 사용하면 안될 것이다.

operator[]의 경우 insert한 결과의 iterator->second를 돌려준다(second의 성공, 실패 여부와 관계없이)는 사실을 알고 있다면,

operator[]를 호출하는 순간 insert가 발생한 다는 것을 알고 있어야 한다.

종종 뒤에 insert하는 Value의 메모리를 할당하는 것이 부담되어(insert를 성공 할지 실패 할지 않수 없을때)

역시, 잘못된 방법으로 사용하는 경우가 있는데…

이 경우는 두가지 해결책을 제시 할 수 있는데,

STL에는 대비책이 다 마련되어 있다.(insert with hint)

lower_bound로 찾은 다음에 operator==으로 비교 하는 것보다. std::less로 비교하는 것이 더 정확하다.

(만약 std::map template의 세번째 파라미터로 std::greater로 정의 하였다면, 당연히 std::greater로 비교)


참고

http://www.cplusplus.com/reference/map/map/insert/