About Tuning

튜닝은 코드에 대한 자기 반성의 시간이다.

내가 원래 짰던 코드들이

그 때는 잘 돌아갔지만

시간에 지남에 따라 코드가 추가 되고

수정되고 하면서 여러가지 현상이 나타나게 되는데.

그것이 현재 필요한 상황에 엄청난 부담을 주게 된 경우 보통 튜닝을 하게 된다.

그러므로 튜닝은 자기 코드를 보고 하나의 반성의 시간을 가지게 될 수 있다.

구조적으로 아주 잘 짜여진 코드들도 실제로 성능은 좋지 않을 수 있다.

CPU 를 많이 쓴다던가 메모리를 많이 차지한다던가 네트워크 트래픽을 엄청 먹는 다던가

하는 여러가지 상황으로 현재 상황이 흘러갈 수 있다.

오늘은 jui 를 만들면서 생각 했었던 튜닝에 대한 몇가지를 적을까 한다. 여기서는 실제적인 퍼포먼스 측정을 하지 않는다. 다만 논리적인 로직을 보고 할 수 있는 일에 대한 간략한 정보를 제시한다.

튜닝의 기본 전략은 작게 작게

튜닝에서 가장 포인트가 되는 부분은 현재 로직에 영향을 주지 않고 어떻게 하면 cpu와 memory 등의 리소스를 작게 쓸까이다.

예를 들어 Chart 가 여러개 모여 있는 Dashboard 가 있다고 치자. 실시간으로 Dashboard 에 엄청나게 많은 데이타를 변경해야하면 cpu, memory 등 관련 리소스들이 엄청난 부담으로 다가온다. 심지어 데이타 구조를 어떻게 설계해야하는지부터 고민해야할 수도 있다.

1개의 차트를 그리는 동안 cpu 100 이라고 치면 잘 튜닝해서 50으로 만들면 2개의 차트를 그릴 수 있고. 만약에 1개만 그리더라도 전체적인 시스템에 영향을 주는 부분이 반으로 줄어든다.

코드를 만들어 놓고 자신의 코드를 다시 보는게 쉽지는 않다. 튜닝도 어떻게 보면 리팩토링의 범주에 들어가지만 사실상 리팩토링을 하는 많은 경우는 튜닝보다 구조적인 설계 오류를 재설정 하기 위한 것이 많다.

그렇지만 튜닝을 하지 않으면 진짜로 해결해야할 문제를 해결하지 못한채 리소스만 엄청 낭비할 수 있다.

Javascript 는 생각보다 빠르지만 우리는 느리게 코딩한다.

JUI 는 순수하게 javascript로 되어 있다. UI 를 만들때는 table 형태의 UI 를 제외하고는 성능에 대한 고민을 할 필요가 없었다.

Chart 기능이 들어 갔을 때도 처음에는 로직에 집중하였다. 일단 기능이 완성이 되어야(정확히는 눈에 보여야) 다음을 진행 할 수 있었기 때문에 성능에 대한 고민을 접어두고 로직에 대한 고민만 한다.

하지만 Chart 기능이 점차 추가됨에 따라 여러개의 차트를 동시에 보여줄 수 있는 기능도 추가 되고 이에 따라 많은 양의 데이타를 그릴 수 있는 기반이 완성이 되어야 했다.

거기다 실시간 Chart 도 그려야 하는 이슈가 생겼다.

처음에는 실시간으로 그리는 차트를 만들었는데 상당히 많은 양의 데이타(대략 90000건)를 매초마다 뿌려 주고 있었기 때문에 cpu 사용률과 화면에 계속 껌뻑이는 현상이 지속 되었다.

만든다고 다가 아닌 것이다.

상황에 따라 계속 변화하는 로직에 대하 기본적인 성능이 받쳐주질 못하면 다음단계를 갈 수가 없었다.

그래서 시작했다. 튜닝을.....

튜닝의 시작은 loop 문으로 부터

많은 양의 데이타를 처리 하는 경우는 loop 형태가 필수이고 (for, while 등등) 그런 것들을 어떻게 사용하느냐에 따라 퍼포먼스가 나뉘어 진다.

일반적인 loop

var arr = new Array(10000);
for(var i = 0; i < arr.length; i++) { 
    // 10000 개에 대한 루프 
}

여기에 문제점이 하나 있다. 무엇일까? (이미 대부분 눈치 챘을 수도 있다.)

i < arr.length;

이 부분은 루프를 계속 할 것인지에 대한 체크를 하는 곳이다. 즉, 10000개를 돌릴려면 10000번에 대한 체크를 한다.

다만 arr.length 형태로 배열의 length 속성을 계속 접근 하는 것은 성능에 좋지 않다. .(dot) 도 연산자이기 때문에 연산을 따로 수행하는 과정이 생긴다.

이럴 때는 arr.length 에 대한 값을 하나의 변수에 담아두고 쓰는게 좋다.

for(var i = 0, len = arr.length; i < len; i++) {
   // 10000개에 대한 루프 
}

자 , len 이라는 변수에 arr.length 을 넣어뒀다. 그럼 이제 .(dot) 연산자로 length 변수에 접근하는 연산은 빠진다.

i 가 유효한지 체크 하는 로직 (10000번) arr.length 를 얻어오는 로직 (10000번) 에서

arr.length 를 얻어오는 로직 (10000번) 이 빠지는 것이다.

캐쉬를 쓰자.

위에서 이야기한 arr.length 를 len 변수에 담아두는 것이 캐쉬이다. 중복적으로 쓰이고 변하지 않는 것에 대해서는 모두 캐슁을 해두는 것이 좋다.

JUI 관련된 차트를 만들면서 로직에 우선을 둔 코드들을 많이 만들었었는데 그중에 scale 담당하는 코드를 예제로 볼까 한다.


// 내부 변수를 설정한다. 
var domain = []; 

// domain 메소드는 domain 변수를 설정한다. 
this.domain = function(arr) {
   global.domain = arr;
}

this.max = function() {
    return Math.max.apply(Math, domain);
}

this.min = function() {
    return Math.min.apply(Math, domain);
}

this.get = function(x) {
    var max = this.max();
    var min = this.min(); 

    // 내부 값을 가지고 오는 코드 실행 
    ... 
}

간단한 예제이다.
여기서는 성능에 대한 것이 전혀 고려 되어 있지 않고 실행시 유연한 구성에 대한 것만 고려가 되어 있다. 예를 들어 min, max 값을 들어온 배열에 따라 실행시간 기준으로 유연하게 구할 수 있다. 중간에 domain 값이 바뀌어도 상관이 없다.

단순히 성능 면에서 생각해보면

this.get 이라는 메소드를 호출 할 때 매번 this.min, this.max 를 호출 하게 되어 있다.
this.min 은 Math.min, this.max 는 Math.max 를 호출하게 된다.

즉, 메소드 하나에 실행하는 실제 메소드가 4개가 된다.

그런데 과연 이 메소드들을 매번 실행할 필요가 있을까?

// 내부 변수를 설정한다. 
var domain = []; 
var domainMax = null;
var domainMin = null; 

// domain 메소드는 domain 변수를 설정한다. 
this.domain = function(arr) {
   domain = arr;
   domainMax = Math.max.apply(Math, arr);
   domainMin = Math.min.apply(Math, arr);
}

this.get = function(x) {
    // 내부 값을 가지고 오는 코드 실행 
    // domainMax, domainMin 을 사용 
    ... 
}

자 이제 domainMax, domainMin 값이 캐쉬 되었다.

그럼 this.get 실행 시점에 실행되던 4번의 메소드 호출은 없어진 셈이다.

domain 이 바뀔 때 마다 min, max 정의도 같이 해주기 때문에 유연성도 가진다.

차트를 그릴 때 scatter 라는 차트를 그릴 수 있는데 10000개의 포인트를 찍어야 한다고 치자.

for(var i = 0; i < 10000; i++) { 
  this.get(i);   // 10000번 실행 
}

this.get 을 10000번실행하는 동안 내부 메소드가 4번씩 실행되니깐 10000 x 4 해서 40000 번 실행 되던 메소드가

캐쉬를 통해서 0 으로 줄어들었다.

물론 성능에 치명적이지 않을 수는 있다. 하지만 작연 연산이 모여 사용하는 리소스(cpu, memory)를 최대한 줄일 수 있다.

loop 에 불필요한 로직은 삭제

말 그대로 loop 랑 전혀 연관성이 없는 로직을 삭제하는 것이다.

코드를 한번 보자.


// a, b 타겟에 대한 x,y 데이타를 수집하는 예제 
var targetList = ['a', 'b'];
var target = { }; 

for(var i = 0; i < 10000; i++) {

   for(var targetIndex = 0; i < targetList.length; targetIndex++) {

      // 해당 target 에 대한 정보가 초기화 되어 있지 않으면 초기화 한다. 
      if (targetIndex == 0 && !target[targetList[targetIndex]]) { 
        target[targetList[targetIndex]] = {
           x : [],
           y : []
        }; 
      } 

      target[targetList[targetIndex]].x.push(targetIndex);
      target[targetList[targetIndex]].y.push(this.get(i, targetIndex));
   }      

}

간략히 보면 사실상 딱히 문제 되는 부분은 없다.
루프에 대한 변수도 나름 캐슁을 했고 targetList 가 2 개 밖에 안되기 때문에 그 안에서 루프를 다시 도는 것도 괜찮다.(어짜피 돌아야 하는 부분이기도 하고)

다만 그 안에서 객체를 초기화 하고 있는게 문제가 된다.

10000 의 루프 밑에 2 번의 루프가 중첩으로 돌아가서 최소 실행 횟수가 10000 x 2 가 된다.

그런데 여기서 if 체크를 매번한다. 즉, if 가 20000 번 돌고 있다. 과연 그럴만한 가치가 있을까?

다음 코드를 보자.


// a, b 타겟에 대한 x,y 데이타를 수집하는 예제 
var targetList = ['a', 'b'];
var target = { }; 

// 객체를 초기화 한다. 
for(var targetIndex = 0; i < targetList.length; targetIndex++) {
    target[targetList[targetIndex]] = {
       x : [],
        y : []
    }; 
}      


for(var i = 0; i < 10000; i++) {

   for(var targetIndex = 0; i < targetList.length; targetIndex++) {
      target[targetList[targetIndex]].x.push(targetIndex);
      target[targetList[targetIndex]].y.push(this.get(i, targetIndex));
   }      

}

루프 안에서 매번 if 로 체크해서 초기화 하던 구문을 밖으로 분리하였다.

몇가지가 달라졌는지 보자.

  1. 20000 번을 실행하던 if 체크 로직이 빠졌다.
  2. 20000번 대신 targetList 만큼의 초기화 로직이 생겼다. (20000 번에 비하면 2는 아주 사소한 것이다)
  3. 전체 loop 에 대한 로직이 간단해졌다. (루프 자체의 기능에 집중 할 수 있게 된다.)

결국 20000번 실행 하던 로직이 2번으로 바뀌었다.

실행횟수만 줄여도 cpu 가 날아 갈 것이다.

results matching ""

    No results matching ""