Computer Science
전공이 Software engineering이면서 Blog에 관련된 이야기를 하지 않으려고 외면해 왔다. 관심을 가지고 하는 일이며, 하루의 대부분을 차지하는 일인데 애써 거부하는 것이 웃기지 않은 가? 매우 뛰어나신 분들은 항상 겸손히 조용히 계시니 없는 지식이라도 조금 풀어놓고자 한다. 빈수레가 요란하다고 하지 않았던 가?
break, continue - C/C++ 과 Java에서의 차이
Y군과 L군이 프로그래밍 언어 개념이라는 시험을 마치고나서 내게 와서는 물었다. Java에서의 break와 continue는 C/C++에서의 break와 continue와의 차이점이 있다면서 차이점을 서술하라고 문제가 나왔다고 한다. 당연히 풀지 못했으니 물었으리라. 그리하여 대답을 대신하여 이 글을 기록한다.
break 하나를 설명하면 continue까지 설명하는 것이 되니 break만으로 설명을 하기로 한다. break는 반복문 내에서 해당 반복문을 정지하기 위하여 사용된다. 그런데 예를 들어 C/C++에서 for()가 여러개 중첩되어 있는 경우, 가장 안쪽 반복문에서 특정 조건을 만족했을 때 현재 중첩되어 있는 모든 for()을 벗어나고자 한다면 어떻게 해야할 까?
in C/C++
for(int i= 0; i<size; i++) {
for(int j= 0; j<size; j++) {
for(int k= 0; k<size; k++) {
…
if(expressionA) {
break;
}
…
}
}
}
(C에서는 for() 안에 int i= 0; 과 같은 사용은 허용되지 않는다고 이야기하지 말자.[1])
위 code에서 expressionA가 True라면 k를 increment로 사용하고 있는 가장 안쪽 for()를 중지할 수는 있으나 i, j를 increment로 사용하는 for()는 중지시킬 수가 없다. 이 문제를 해결하기 위한 방법으로 3가지 정도를 들어보기로 한다.
1. break; 를 작성하기 전에 그 위 줄에 i= size; j= size; 와 같은 문장을 추가한다면 해결되지 않을 까? 물론 중첩된 for()를 벗어날 수 있으나 i, j, k의 scope가 for() 내에서만이 아니라 위 문장을 작성하기 이전에 위에 선언되었다면, 그리고 중첩된 for()가 종료된 시점에서 i, j, k의 값을 참조해야한다면 문제가 될 수도 있다. 이런 경우에서라면 좋은 방법이라고 이야기할 수가 없다.
2. 꽤 많이 사용되는 방법으로 flag를 하나 두고 break; 위에서 flag를 set한 후 각 for()에서도 flag의 set/unset 여부에 따라 break를 결정하는 방법이 있다. 이 방법은 for()가 늘어날 때마다 기록해야 하는 if()가 늘어날 것은 분명한 사실이며 for() 중첩이 많으면 많을수록 종료를 위한 overhead가 커짐을 의미한다.
3. goto를 사용하는 방법으로 아래의 코드와 같이 처리하면 매우 간단히 해결된다.
in C/C++
for(int i= 0; i<size; i++) {
for(int j= 0; j<size; j++) {
for(int k= 0; k<size; k++) {
…
if(expressionA) {
goto endOfLoop;
}
…
}
}
}
endOfLoop:
다만 이 방식은 goto를 사용했다는 것이 문제인데, goto에 대한 사용에 대해서는 지금까지 많은 논쟁이 되어왔으며[2], 처음 C/C++를 접하는 학생에게 goto에 대해서 알려주지만 사용치 못하게 하거나 아예 가르쳐주지 않는 경우도 존재한다. goto가 지역성, 구조적인 흐름을 깨부수기 때문에 사용하지 않는 것이 좋다는 주장이 많기 때문이다.[2] 따라서 이 방법은 매우 간단히 해결할 수 있지만 spaghetti code가 될 가능성이 높은 goto를 사용했으므로 권장하기 어렵다고 할 수 있다.[2]
Java에서는 이러한 문제를 해결하기 위하여 반복문 내에서의 goto의 효과를 살리는 방법을 제공하는데 break/continue가 goto적인 요소를 가지고 있다.
in Java
NestedLoop:
for(int i= 0; i<size; i++) {
for(int j= 0; j<size; j++) {
for(int k= 0; k<size; k++) {
…
if(expressionA) {
break NestedLoop;
}
…
}
}
}
위 code에서 볼 수 있는 것 처럼 break 뒤에 Label name을 적음으로써 해당 Label이 위차하고 있는 Level까지의 loop를 종료할 수 있다. (쉽게 이해하자면 '이 Loop의 이름은 NestedLoop이다. NestedLoop를 종료해라' 라고 이해할 수 있다.) 지금까지 break에 대해서만 설명했지만 continue의 경우에도 동일하게 Label name을 continue 뒤에 적어줌으로써 해당 Level의 Loop를 다음 상태로 넘어갈 수 있게된다.
[1] C99에서는 변수의 선언이 C++와 마찬가지로 어디에서나 가능하다. http://eriny.net/n/40
[2] Goto의 사용은 구조적 프로그래밍으로 프로그래밍언어의 패러다임이 옮겨간 후부터 많은 논쟁이 되어왔던 문제이다. 대표적으로 Goto가 사용되고 있는 예를 들자면 Linux Kernel을 들 수 있다. 이 이야기에 대해서는 다음의 Link를 확인하도록 하자. http://erin.is/lTx3j4QL / http://erin.is/lTx3j4Qr
C99
C1X[1]가 거들먹 거리고 있는 이 때에, 아직 C90[1]이후 C99[1]에 추가된 것들에 대해 정확히 모르고 있다는 사실이 스스로에게도 이상하기도 해서 찾아보고 정확히 이해하기로 했다. 아무리 3번씩 개정이 되었다고 하더라도 무려 1999년의 표준인데 이제야 정확히 알아야겠다고 마음 먹은 것 그 자체가 스스로에게 너무나 웃길 따름이다. 여기저기 뒤져볼 필요없이 알아서 잘 정리해둔 wikipedia 내용을 잠시 보도록 하자. (아!! C1X에 대해서도 봐둬야할텐대……)
아래는 wikipedia의 내용을 그대로 가지고 왔다.
reference link: http://en.wikipedia.org/wiki/C99
위 내용 말고도 형식 지정자가 빠진 선언을 int type으로 처리해주거나 하는 것들이 사라졌다. 이런 내용 정도는 다들 잘 알고 있으리라 생각되는데, 아무래도 다들 알고 있을 것 같은데 여전히 나만 모르고 있었던 기능이 4가지 있다.
1. Intermingled declarations and code C99이전 C90에서는 변수의 선언은 반드시 File scope 또는 code block의 처음 부분에서만 할 수 있었는데 C++와 같이 어디에서든 가능하다. 변수선언 장소에 대해 구애받지 않게 되었다.
예전에는
for(int i=0; i<max; i++)와 같은 사용이 불가능 했는데 지금은 가능하다는 이야기이다. 그런데 왜 난 이상하게 주워들어서 for()에만 가능하다고만 알고 있었을 까?
2. Variable-length arrays 이런게 가능한 줄 알았다면, 얼마나 좋았을 까? 이따금 이렇게 되면 좋겠다는 생각을 정말 많이 했었는데, 이미 가능했는데 사용치 못하고 있었다는 사실이 너무 웃기다. 애당초 C99로 complie할 생각을 잘 안 하니 이런 문제도 생기는 거다.
void printSortedElements(int array[], int size) {
int tmpArray[size];
for(int i= 0; i<size; i++) {
tmpArray[i]= array[i];
}
for(int i= 0; i<size; i++) {
for(int j= 0; j<size; j++) {
if(tmpArray[i] < tmpArray[j]) {
int tmp= tmpArray[i];
tmpArray[i]= tmpArray[j];
tmpArray[j]= tmp;
}
}
}
for(int i=0; i<size; i++) {
printf("%d\n", tmpArray[i]);
}
}
3. support for variadic macros (macros with a variable number of arguments) #define을 이용한 매크로 사용시 고정적인 인자의 사용으로 인하여 인자가 늘어나면 동일한 기능임에도 불구하고 다른 이름으로 작성했어야 했는데 이 부분에 대한 문제를 간단히 건너뛰게 되었다. 이해하는데 무리가 없었으니 그냥 넘어가기로 한다.
4. restrict qualification allows more aggressive code optimization 코드 최적화를 좀 더 적극적으로 할 수 있게 된다는데 restrict가 무엇을 하길레 그런걸까? restrict는 pointer에 사용되는 keyword로 한정자이다. 자세한 이야기를 시작하기 전에 pointer에 대한 이야기가 필요하다. c언어에서의 pointer는 매우 큰 강점, 장점이 되는데 그 이유는 주소의 접근으로 인하여 low level을 다룰 수 있게 해주기 때문이다. 다만 이 것이 compiler입장에서는 매우 큰 약점이 된다. 그 이유는 다음과 같다. pointer가 아닌 expression이 5개가 연속적으로 code에 존재한다고 하자. 각 expression이 다른 expression에 영향을 주지 않는다면, complier는 각각의 expression의 수행속도에 따라 수행순서를 바꾸거나 병렬성을 고려하거나 하는 식의 최적화를 할 수 있다. 각 expression이 다른 expression에 영향을 주는 가? 주지 않는 가?를 판단하는 가장 쉬운 방법으로 변수의 이름으로 확인할 수 있다. 다른 expression의 결과를 담게되는 변수가 다른 expression에 포함되어 있다면 영향을 주게되는 것은 자명한 사실이다. 그런데 pointer는 단순히 변수의 이름으로 확인할 수가 없다. 왜냐하면 pointer는 임의의 주소를 가지고 있고, 동적으로 언제든 주소가 변경될 수 있으므로 각 expression끼리의 의존성 파악이 어려워진다. 아래의 code를 보면서 이해해보자.
*result1= *number1 + *number2; *result2= *number3 + *number4;두 expression이 서로 영향을 주지 않는다고 단언할 수 있는 가? 아니다. 절대할 수 없다. 예를 들어 result1과 number4가 같은 주소를 가지고 있다면 당연히 *result2에 영향을 주게 되는 것이다. 그런데 만약 compiler가 명확하게 각 pointer 변수가 서로 다른 주소만을 가지고 있을 경우에 그 사실을 알 수 있다면 최적화할 수 있게 된다. 아래의 code를 통해서 확인하자.
void add(int restrict *arr1, const int restrict *arr2, int size)
{
for(int i=0; i<size; i++) {
arr1[i]+= arr2[i];
}
}
restrict는 pointer 변수가 가지는 주소가 겹치거나 같은 주소를 사용하지 않는다는 것을 complier에게 알린다. 그러면 왜 complier가 최적화를 할 수 있는 것이 아니라 적극적으로 최적화할 수 있다고 이야기를 하고 있느냐하면, complier는 pointer의 주소가 겹칠 것인 가, 겹치지 않을 것인가에 대한 예측을 하고 예측에 따라 최적화를 수행하는데 해당 최적화가 잘못 수행되었을 경우 되돌릴 수 있는 code 또한 포함한다.[2] 따라서 복구를 위한 추가적인 code를 생성할 필요 없이 restrict를 통해서 좀 더 적극적인 최적화를 수행할 수 있게 된다.
[1] C언어에 대한 표준으로 최초의 표준 C89(1989년 ANSI C) 그리고 이어서 C90(1990년 ISO), C99(1999년 ISO)를 말한다. C1X는 C99를 개선한 것으로 GCC version 4.6에 몇몇 특징이 추가되었으며 아직 완료되지 않은 표준안이다.
[2] 이 부분에 대해서는 정확한 지식을 가지고 있지 않으므로 해당 내용이 잘못 되었을 수 있다.
* 위 code는 gcc -o test -std=c99 test.c 와 같은 방식으로 GCC만으로 검증하였습니다.