일단 내적을 구하는 다음과 같은 코드를 보자.
double inner_product(const std::vector<double>& x, const std::vector<double>& y)
{
double ret = 0;
const auto size = x.size();
for(std::size_t i = 0; i <= size; ++i)
ret += x[i] * y[i];
return ret;
}
일부러 코드에 버그를 넣어놨어. off by 1(링크)이라는 기초적이지만 흔히 발생할 수 있는 버그인데, 프로그램 규모가 커지거나 짜는 사람이 피곤하면 찾기가 쉽지 않은 버그야. c/c++은 바운더리 체크를 안해주기 땜에 더더욱 그렇지.
std::vector를 좀 써본 사람이라면 .at() 이라는 함수를 본 적이 있을텐데, 이 함수는 인덱스 체크를 해서 벡터의 범위를 벗어나면 익셉션을 던져주는 함수야. 이 함수를 쓰면 다음과 같이 짤 수 있고, 실행해보면 실제로 익셉션이 던져져.
for(std::size_t i = 0; i <= size; ++i)
ret += x.at(i) * y.at(i);
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 2) >= this->size() (which is 2)
Aborted
그런데 이 코드의 단점은, at() 함수가 호출될때마다 인자를 검사하기 때문에 오버헤드가 좀 크고, 그래서 성능 저하가 발생할 수 있다는거야.
디버그할 때는 검사를 하지만, 릴리즈할 때는 검사를 하지 않는 그런 코드가 있으면 좋은데, 그게 바로 assert야.
일단 assert가 어떻게 생겨먹었는지 보자. (링크)
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
NDEBUG가 define되어 있으면 아무 일도 하지 않고, define되어 있지 않으면 관련 메시지를 출력하고 프로그램을 종료시키는데,
개인적으로는 이런 매크로야말로 제대로 매크로를 활용한 우아한 방법이라고 생각해.
#define T_T template <class T> 이따위 개족보에도 없을 매크로는 그저 T_T
C에서는 #include <assert.h>, c++에서는 #include <cassert>를 사용하면 assert를 쓸 수 있어.
assert를 써서 위의 코드를 바꿔보자.
for(std::size_t i = 0; i <= size; ++i) {
assert( (i < x.size()) && (i < y.size()) );
ret += x[i] * y[i];
}
NDEBUG를 정의하지 않고 수행하면 다음과 같이 assertion 메시지가 뜨는 것을 볼 수가 있어.
a.out: assertion.cpp:12: double inner_product(const std::vector<double>&, const std::vector<double>&): Assertion `(i < x.size()) && (i < y.size())' failed.
Aborted
그럼 딱 봐도 assertion.cpp의 12번째 줄에 i가 잘못됐구나 라는 걸 알 수가 있고, 디버깅하기도 쉽겠지? 거기다 NDEBUG만 정의하면 릴리즈모드에서 오버헤드가 전혀 없고.
NDEBUG를 정의하는 방법은 컴파일러마다 다르지만 대충 다음과 같아.
gcc : gcc -DNDEBUG
clang : clang -DNDEBUG
icc : icc - DNDEBUG
msvc : 프로젝트 속성 - C/C++ - 전처리기 - 전처리기 정의 : NDEBUG 추가.
그럼, 언제 익셉션을 던지고 언제 assert를 사용해야하는가? 이게 좀 고민해볼만한 문제인데, 이건 딱히 정답은 없는 것 같지만 대략 내 경험으로는
유저 입출력이나 파일 입출력에서 발생하는 외부적 요인 : 에러 코드를 리턴하거나 익셉션
구현 내부에서 발생가능한 내부적 요인 : assert로 지뢰밭을 깔아둠
정도가 적합하지 않는가 싶어.
다시 위의 inner_product 함수에서, 저 함수의 사용자가 벡터 두개를 입력으로 주는데 두 벡터의 크기가 다르면 내적이 정의가 안되잖아? 이런 경우에는 에러코드를 리턴하거나 익셉션을 던지는게 더 적합하다고 보는거야.
그럼 요 기준으로 inner_product를 짜면 다음과 같이 짤 수 있어.
double inner_product(const std::vector<double>& x, const std::vector<double>& y)
{
double ret = 0;
if(x.size() != y.size())
throw std::invalid_argument("x.size() != y.size()");
const auto size = x.size();
for(std::size_t i = 0; i < size; ++i) {
assert( (i < x.size()) && (i < y.size()) );
ret += x[i] * y[i];
}
return ret;
}
assert는 잘 쓰면 그 자체로 코드 내부에서 지켜져야할 불변식을 나타낼 수 있는 게 큰 장점이야. 릴리즈 모드에서 NDEBUG만 정의하면 비용도 0이고.
assert 사용할 때는 주의사항이 좀 있는데, 절대로 assert 내부에서 값을 변경하지 말아야해. 다음과 같은 코드를 보자.
int a = 1;
assert(++a != 0);
std::cout << a << std::endl;
이런 코드는 NDEBUG를 정의할 때와 정의하지 않을 때의 동작이 달라지겠지? 디버깅 쉽게 하려고 assert를 썼는데 오히려 거기서 버그가 발생할 수 있어. 그러니까 값을 바꾸는 코드와 무조건 assert문은 분리하는게 좋아.
int a = 1;
++a;
assert(a != 0); // 분리함
std::cout << a << std::endl;
그리고 또 주의사항은ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
프로그래밍 30년 넘게 했다는 새끼갘ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
성능 비교하는데 assert문도 안끄고 비교를ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
assert문 안끄고 돌려봤떠니 그 븅신새끼랑 결과가 비슷함ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
자칭 고수지만 assert문 끄는 방법을 모르거낰ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
아님 일부러 안끈 개새끼라는 건뎈ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
둘 다 개 웃겨섴ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
내가 오늘 아침에 좆나 웃겨서 깸ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
오늘 밤이나 내일은 잘못된 최적화의 예를 써볼까 한다.
3줄 요약:
디버거도 좋지만 assert 지뢰밭은 버그 찾기 더 쉬움.
예외 처리할 때와 assert 쓸 떄를 구별하자.
assert 내부에선 절대로 값을 변경하지 말자.
========== 내용 추가 ==========
#define assert(x) (void(0)) 이 문장은 x를 (void(0))으로 치환한다는거야.
assert(a == 0);
이 문장이 있다고 치면,
NDEBUG가 정의가 안되어 있으면 일반적으로
if(!(a == 0)) {
// __FILE__과 __LINE__을 출력하고
// 프로그램을 강제종료시킨다.
}
요런식으로 바뀌게 돼.
gcc -DNDEBUG로 NDEBUG를 정의해서 빌드하면
assert(a == 0); 는 전처리기에 의해 (void(0)); 으로 치환되고, 그럼 컴파일러는 아무 의미없는 이 문장을 그냥 버려. 그러니까 assert 문장이 아무것도 없는 것처럼 처리되는거지.
assert()는 기본적으로 런타임 검사야. 실행시에 주어진 조건을 검사하는데, 컴파일 타임 assertion을 원하면 C11의 _Static_assert(링크)나 c++11의 static_assert(링크)를 쓰면 돼.
내가 짠 코드가 CHAR_BIT 이 8인 시스템에서만 정상적으로 돌아가는 코드라면,
_Static_assert(CHAR_BIT == 8, "It doesn't work with CHAR_BIT!=8");
이런식으로 컴파일 타임에 assertion을 수행할 수 있어. 물론 이 compile time assertion은 생성되는 코드에 전혀 영향이 없으니까 켜고 끌 것도 없지.
========== 내용 또 추가 ==========
음 내가 내부적 요인 외부적 요인이라고 써놨는데 이건 공식용어가 아니라 그냥 내가 표현한것 뿐이야. 이것 땜에 좀 헷갈리는것 같은데 내가 설명을 못한다는걸 새삼 느낀다 ㅋㅋ
doubly linked list를 짠다고 생각해보자.
node* insert(T value, node* pos)
{
node* new_node = malloc(sizeof(node));
if(new_node == NULL) {
// 내가 잘못 짜서 생기는 현상이 아니므로 익셉션을 던지든 에러코드를 리턴하든 한다.
}
node->value = value;
node* prev = pos->prev;
// prev와 new_node를 연결
// new_node와 pos를 연결
// 내가 잘못 짜서 버그가 생길 수 있으니 assertion을 수행한다.
assert(prev->next->prev == prev);
assert(pos->prev->next == pos);
assert(new_node->prev->next == new_node);
assert(new_node->next->prev == new_node);
}
메모리 할당이 안되는 경우는 외부적 요인이니까 에러코드를 리턴하든 익셉션을 던지는게 좋을거고,
내가 잘못 짜서 생기는건 assertion으로 디버그 모드에서 검사하고 릴리즈모드에선 비활성화하는게 좋겠지.
역시 쿠캬캬형님 오늘도 감사합니다. 에버노트에 저장했습니다.
오 정성 개추
"유저 입출력이나 파일 입출력에서 발생하는 외부적 요인 "이게 for문 밖을 의미하는거 같은데.. 쉽게 설명해주실수 있나요?..
off by 1 라는건 두 벡터의 크기가 다르다는걸 말하는거죠?
#ifdef NDEBUG #define [...] ((void)0) #else #define [...] /*implementation defined*/ #endif 이부분도 이해가 잘 안되는데.. assert 함수가 저렇게 구현 되있다는 거죠?
assert가 함수가 아니라 매크로로 되어 있는 건가요?
아아 확인해보니 매크로 맞네여
#define [...] ((void)0) 이게 무슨 의미인지 알려주세요 ㅠㅠ
ㅋㅋ마지막존나웃기네
외부적인 요인이라는게 유저한테 숫자를 받는 경우이고 내부적인 요인은 for문 같은 것들이라는 말씀이죠?
제가 너무 섞어서 질문하네요.. 죄송합니다.
1. #define [...] ((void)0) 이게 무슨 의미인지 알려주세요 ㅠㅠ121.187.*.*
2. 외부적인 요인이라는게 유저한테 숫자를 받는 상황 같은 경우이고 call같은?// 내부적인 요인은 for문 같은 것들이라는 말씀이죠?121.187.*.*
흠 외부적요인 내부적요인은 사실 내 주관적인 판단이라 뭐 어떻게 딱 쉽게 설명하기가 쉽지가 않네. 일단은 그냥 assert 쓰고 나중에 규모가 좀 큰 프로그램을 짜게 되거든 그 때 다시 생각해보자.
아 c의 static assertion은 아예 새로운 사양이구나. 어떻게 만들었나 했네
외부/내부는 그러니까 런타임에 안 바뀌는걸 내부라고 보면 될거같은데염 근데 컴파일 타임에는 잡을 수 없는 그런 미묘한 구역?
병신이 내가 어디 assert 를 쓰든? 예제코드엔 올려 놓은게 없는데, NDEBUG 는 프로젝트 release definition 에서 정의하지 보통.
소설쓰고 나자빠졌네 ㅋㄷ
ㅅㅅㅅ가 그림자 공격이라고 하던 말이 딱 맞다. 편집증 환자 같으니 ㅋㄷ
아 니꺼 assert 걸어놨다고? 거야 니문제지. 글구 원래 C++ 코드에서 이미 확인 다 했을거 아님? memcpy 든 unaligned 든 12~15GB/s 가 나오는게 정상이야.
글구 인트린직 비교는 첨부터 말했지만 의미없음. 어차피 하드웨어 가속임.
C++ 레벨에서 unaligned 와 aligned 를 정의했을때 컴파일러가 어떻게 최적화 해주냐의 이슈일 뿐이야.
이 새끼 또 while(true)돌리고 있네.
http://gall.dcinside.com/board/view/?id=programming&no=573217