요즘 대부분의 경우 compiler의 optimization만으로도 충분하다.

 그렇지만 몇몇 특수한 경우, compiler는 알 수 없지만, programmer는 알고 있는 것들을 적극적으로 이

용할 필요가 있다. 이 글에서는 다음의 내용들을 소개하고자 한다.


* memory barrier
 compile은 file단위로 이루어진다. 그러므로, 비록 programmer가 thread-safety에 대해 고려 하고

coding을 했더라도, compiler가 optimization 과정에서 이를 깨는 경우가 많다.
    가장 대표적인 예가 아래의 경우이다.

    ...
    struct stA {
        int a;
        int b;
        int c;
    };
    ...
    replace (const struct stA* p) {
        struct stA* np = malloc(sizeof(struct stA));
        np->a = 10;
        np->b = 20;
        np->c = 30;
        // <-- A --->
        p = np; // assumes that assign-operation is atomic.
    }
    ...


 programmer는 multi-threading환경에서의 race condition을 고려해서 replace함수를 구현했지만 -

"p = np;"가 가장 아래에 위치해 있다. - compiler의 Out Of Order Optimization으로 인해 programmer의

이러한 의도가 지켜지지 못할 수도 있다.
 (단, 여기서 CPU의 Hardware적인 Out Of Order Execution과 거기에 대응하는, Hardware적인 memory

fence/barrier는 논외로 한다.)
 이런 경우, mutex를 이용해서 해결할 수도 있지만 mutex는 상당히 cost가 비싼 operation이니 만큼

되도록이면 이용하지 않는것이 좋다. 이런 경우 memory barrier를 이용하면 된다.
 memory barrier (membar, memory fence 혹은 fence instruction)에 대한 자세한 내용은 다른 여러

곳에서 잘 설명되어 있으니 따로 언급하지 않겠다.
 다만, 위의 경우 'A'의 위치에 barrier를 두면 mutex를 사용하는 overhead없이 위 문제를 해결할 수

있다.


* compiler hint

 이미 앞서 이야기한대로, compiler는 알지 못하지만, programmer는 알고 있는 내용들을 이용하는 또 다른 방법이 'compiler hint'이다.

 compiler hint는 branch문 - 특히 if/else - 에서 많이 쓰이는데, programmer가, runtime에 분기하는 빈도수가 높은 쪽을 compiler에게 미리 hint를 주는 것이다. 예를 들면, "if (<cond>) { A } else { B }" 이고, runtime에 대다수의 경우 B쪽으로 분기하게 된다는 것을 programmer가 안다면, <cond>에 이러한 힌트를 함께 언급해 주는 것이다.

 그럼 왜 branch문에 이런 compiler hint가 많이 쓰이는 것일까? 그리고 어떠한 잇점이 있는 걸까?

 대부분의 Architecture - 특히 RISC - 에서, 성능향상을 위해, pipeline을 사용한다. 그리고 일반적으로 이 pipeline의 단계를 늘림으로서 성능향상을 기대할 수 있다.

 그런데, pipelining을 위해서, 다음에 수행할 instruction을 미리 읽어오는 방법을 쓰게 되는데 - instruction prefetch - branch문에서는 다음에 어떤 쪽으로 수행될지가 runtime에 결정되기 때문에 - 즉 compiler가 알지 못한다 -  compiler는 자체적으로 판단하여 prefetch를 수행할 쪽을 결정하고, 이를 반영해서 compile하게 된다. 이런 예측이 옳은 경우는 pipeline이 깨어지지 않고 잘 수행되어 별 문제가 없는데, branch가 예측한 곳과 다른 곳으로 일어나게 되면, 미리 prefetch했던 instruction이 무효한 것이 되므로 이 순간 pipelining이 깨어지게 된다. (앞의 내용들이 이해가 되지 않는다면, pipeline에 대해서 wikipedia등을 이용하길 바란다.) 즉, performance측면에서 손해를 보게 되는 것이다.

 이런 손해를 최소화 하기 위해서, programmer가 어느 쪽으로 분기하는 경우가 많은지를 compiler에게 hint를 줄 필요가 있는 것이다.

 아래의 예는 Linux kernel에서 볼 수 있는 'likely/unlikely'를 사용한 예이다.


#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0) if (likely(n > 1)) { // 자주 수행되는 routine } else {

// 상대적으로 적은 빈도로 수해오디는 routne }

if (unlikely(n > 1)) { // 상대적으로 적은 빈도로 수해오디는 routne

} else {

// 자주 수행되는 routine }


ㅇㅇㅇㅇ


 Variable(변수)와 Function(함수)는 programming을 하는 사람이라면, 누구에게나 익숙한 개념이다.

 그렇지만, 조금 더 살펴보면 흥미로운 요소들을 많이 찾을 수 있다.


 먼저, Variable에 대해서 이야기해 보자.

 거의 모든 programming language에서, variable은 type과 name 두 가지로 구성된다.

 비록 Static/Dynamic typing, 혹은 Strong/Week typing 등의 차이는 있으나, 기본적으로 type과 name이 존재한다는 점에서는 차이가 없다.

 그럼, 혹시 "variable은 왜 필요하지?", "왜 거의 모든 programming language에서 변수는 type과 name으로 구성되어 있지?"등의 의문을 가져본 적이 있는가?

 이는 분명 한번 쯤은 생각해 볼 문제이다.


 사실 이런 의문들에 대한 대답은 programming 발전의 역사와 함께한다고 할 수 있다.

 따라서, 이런 부분에 대한 자세한 내용은 넘어가고, 간략하게, "왜 type과 name"으로 나뉘어지는 지를 고민해보는 것으로 대체하기로 하자.

 

이런저런 자세한 내용은 차치하고, 기본적으로 Programming이란 MCU(혹은 CPU)가 메모리에서 data를 읽어서 그것으로 연산을 수행하고 다시 그 결과를 메모리에 저장하는 일련의 과정이다. 이때 메모리에서 데이타를 읽고 그것을 가공하기 위해서는, 두 가지가 필수적으로 필요하다. "어디서 data를 읽고, 그 data가 어떻게 해석되어야 할 것인가?"가 그것이고, 그것이 바로, "메모리 주소" 와 "메모리 data type"이다.

그 결과, "메모리 주소 = Symbol Address"이고, "data type = Symbol Type"으로 해석되는 것이다.
 또한, 변수, 함수 모두 memory address의 alias 역할을 하는 symbol + memory data의 해석 방법을 알려주는 type, 두 가지로 이루어져 있다는 측면에서 보면 근본적으로 같은 개념이다. 단, variable은 data이고, function은 execution code라는 측면이 다를 뿐인데, 특정언어 - ex. LISP(data와 code가 구분되지 않는다.) - 에서는 이것 마저도 구분하지 않는 경우도 있다.

 즉, 함수라는 것 역시 일련의 데이터를 execution code로 해석하는 것! 그 이상도 그 이하도 아니다 (일반적인 Programming lanuage에서는 함수의 type이란, 함수의 signature로 표현된다.)

 

요점은 다음과 같다.

오늘날 사용되는 거의 모든 programming이 CPU, memory구조를 가지는 (넓은 의미의)폰-노이만(Von-Neumann)방식에 근간을 둔 하드웨어 구조위에서 이루어지고 있다. 그리고, 이를 전제한 programming language는 memory address의 alias인 'symbol'과 memory data의 해석 방식인 'type' 두 가지에 기반한 형태로 자연스럽게 발전하게 되었다...라는 개인적인 의견을 피력해본다. ^_^


 소프트웨어쪽 업무의 대부분은, 바쁜 일정 속에서, 로직(logic)구현 및 디버깅을 반복하는 일이다.

 그러다 보니, 입사초기에, 실무적으로 필요한 내용을 최대한 빨리 습득하고, 이를 바탕으로 업무에서 성과를 내는 것을 우선시 하게 된다.

 그렇지만, 이런식으로 프로그래밍을 배운 사람들은, 마치 언어에 대한 깊은 이해나 지식, 이론적은 배경 없이 "우리나라 말"을 통해서 의사소통을 하는 초등학생과 같이, 당장 필요한 몇가지 일들은 해낼 수 있을지 모르겠으나 어느 순간 한계에 부딫히게 되기 마련이다.


 이 강좌는, 프로그래밍에 대한 기초적인 지식을 보유한 사람들을 대상으로 하며, 프로그래밍의 바탕에 깔린 깊은 내용을, 구체적인 예를 통해서 case-by-case로 짚어보는 방식으로 기술될 것이다.


혹시 부정확한 부분이나, 잘못 기술된 내용이 있다면... 부담없이 태클 걸어 주세요~~

+ Recent posts