본문으로 바로가기

반복문 비교 (2)

category Algorithm/기타 정보 2019. 3. 15. 08:24




반복문을 비교하는 포스트에 조금 문제가 있었다. 바로...


 long startTime = new Date().getTime();
long endTime = new Date().getTime();

long elapsed = endTime - startTime;


윗 코드들, 즉 수행 시간을 재기 위해 사용한 getTime() 메소드들이

순수한 수행시간을 알 수 있는 메소드가 아니란 것이었다.


Date date = new Date();

System.out.println(date);


// 혹은

System.out.println(new Date());


Date 객체는 ( new 생성자를 통하여 ) 호출되는 시점을 저장한다. 따라서 

위의 코드를 실행해보면 어떤 포맷에 맞춰진 시간이 출력된다.

(보통은 프로그램 실행하자마자 좌라락 처리되므로 사용자가 보기엔

Date() 가 호출될 시점의 시간이 아니라, 그냥 현재 시간이 출력되는 것처럼 보인다.)


getTime() 메소드는, Date 객체에 저장되어 있는 시간을 밀리초로 변환한다.

즉 getTime()을 사용하면 시간 타입을 순수한 정수로 바꿀수 있다.


따라서 endTime - startTime 이라는 간단한 수식을 사용할 수 있고...

그 값을 long elapsed 에 담으면,

startTime과 endTime 사이에 존재했던 순간의 시간을 알 수 있다.






말이 주저리주저리 길었는데...


(어떤 메소드를 써야되는데? 라고 생각했다면,

전(2)를 먼저 보세요.)


문제를 최대한 풀어서 정리해보겠다

아래의 코드를 보자.. 


public static void main(String[] args) { 	
   
     WhichLoopIsFaster evaluate = new WhichLoopIsFaster();
     
      evaluate.For();
      evaluate.While();
      evaluate.Iterator();
}


main 함수 실행시

For 반복문의 시간을 알기 위해 만든 For가 제일 먼저 호출되고,

그다음 While, 이터레이터가 그 뒤를 잇는다.


표면적으로는 누가 먼저 실행되더라도 상관이 없다. 

왜냐하면 정말이지 단순하게

종료시점 ㅡ 시작시점 하는 로직이기 때문이다.


하지만... 


public static void main(String[] args) { 	
   
     WhichLoopIsFaster evaluate = new WhichLoopIsFaster();
      
      evaluate.Iterator();
      evaluate.While();
      evaluate.For();
}

바뀐 점이 보이나요?


이렇게 순서를 바꾸면

항상 제일 느렸던 Iterator의 속도가 눈에 띌 정도로 빨라진다.

심지어 다른 두 메소드보다 빠른 경우도 나온다.


첫 반복문 포스트를 쓸 때는 전혀 생각지 못했다..

처음엔 문제의 원인을 제대로 파악하기가 어려웠다.






For(), While(), Iterator() 세가지 메소드를

각각 개별 클래스를 따로 만들어 실행을 따로따로 해보니


다행스럽게도(?) 테스트를 3번이나 했었는데 우연히도... 


전 포스트의 결과와 동일하게

for와 while 이 속도가 비슷하고

Iterator가 제일 느리게 나왔다.


어쨌든, 이유를 알아보자.


맨 처음 떠올렸던 생각은

가비지컬렉터에 뭔가 문제가 있는게 아닐까? 였다.

그리고 나의 사랑 stackoverflow 에서 정확히 맞아떨어지지는 않지만 생각할 수 있는 단서를 찾을 수 있었다.

(링크: https://stackoverflow.com/questions/15632734/can-we-call-the-garbage-collector-explicitly)


최초의 가비지컬렉터가 할 일이라고는, 실행할 메소드가 담긴 객체의 내용들을

ㅡ그렇다.. 가비지 컬렉터가 쓰레기라고 생각하는 것은 바로 객체다ㅡ


담는 것만 하면 된다.


하지만 두번째 메소드부터는 상황이 좀 달라진다.


정말 무식하게 1500만 개의 배열에 값을 하나씩 넣고 

그 값들을 하나하나 세아리는 일을 시켰기 때문에

Gabage Collector가 엄청난 데이터를 수거해야하고

힙메모리가 꽉 찰때마다 비워주는 처리과정이 하나 더 늘어나게 된 것이다.


따라서 맨 처음 호출되는 메소드는 항상 제일 빠를 수 밖에 없게 된 것이다.


두번째 메소드를 처리하는 도중

힙메모리가 가득차면 Garbage Collector가 호출되어 메모리를 비우고.. 또 차면 비우고..


또한 확실하지 않지만 수십번 테스트해본 결과

가비지컬렉터가 메소드 하나 끝낼 때마다

모든 힙메모리를 날려버리는 것이 아닌 듯하다.


최초로 호출하는 객체의 일부분은 힙메모리에 내비둔채 (안비우고) 

두번째 호출하는 객체를 담은뒤 그 것만을 계속 비우고.. 두번째 호출된 객체의 일부분도 힙메모리에 그대로 둔채

세번째 호출하는 객체를 담은 뒤 또다시 그 객체만을 비우는 ....


이렇게 하다 결국 어느 순간에 판단하여 모든 가비지를 날려버리는 것 같은데,


그 순간이 언제일지는 나같은 우매한 인간은 판단이 힘들고

GC(Gabage Collector)님만이 아시는 일인 듯하다 (...)




전(2)



두번째 문제는 Date().getTime()의 사용이었다.

Date().getTime()은 내부적으로 System.currentTimeMillis() 을 호출한다. 즉,


Date().getTime() == System.currentTimeMillis()

둘은 동일하다.


System.currentTimeMillis() 는

1970년 1월 1일 자정부터 현재까지 카운트된 시간을 ms(milliseconds) 단위로 표시한다.


레퍼런스에서는 currentTimeMillis()를 아래와 같은 상황에서 사용한다고 한다.

================================================================

  • 날짜와 관련한 시각 계산을 위해 사용(밀리초 단위: 1밀리초는 0.001초.)
  • 시스템 시간을 참조함 (외부데이터)
  • 일반적인 업데이트 간격은 인터럽트의 타이머(0.00001초, 10만분의 1초)에 따라 결정됨
  • GetSystemTimeAsFileTime 메서드로 구현되어 있으며, OS가 관리하는 시간값을 가져옴.

================================================================



따라서 경과된 시간을 측정하는데 좀 더 적합한 System.nanoTime() 을 사용해야했다.


참고로 nanoTime() 메소드는 레퍼런스에서 이렇게 설명된다고 한다.
================================================================
  • 특정 시점들의 간격을 측정하는데 사용 (나노초 단위: 1나노초는 0.000001밀리초.)
  • 시스템 시간과 관련없음 (무관함)
  • QueryPerformanceCounter/QueryPerformanceFrequency API 로 구현됨.
================================================================

(출처: https://hashcode.co.kr/questions/569/systemcurrenttimemillis-%EC%99%80-systemnanotime%EC%9D%98-%EC%B0%A8%EC%9D%B4%EA%B0%80-%EB%AD%94%EA%B0%80%EC%9A%94)




결(2)


객관적인 시간의 간격을 알려면

따로 따로 여러번 돌려보고,

그 평균값을 구하는 것이 합리적인 것 같다..

컴퓨터야 미안해.. 힘내 파이팅!!


그리고 앞으로 처리시간을 알고 싶을 땐 System.nanoTime() 을 사용해야겠다.