노력과 삽질 퇴적물

코드 최적화 -JAVA중점으로- 본문

📂게임개발 note/미분류

코드 최적화 -JAVA중점으로-

MTG 2025. 2. 28. 19:39

[이미지 출처: jkfran.com]
파트1  
 1) String vs. StringBuilder
 2) for문
 1) final 활용
 
2) 콜렉션과 자료형
 3) 연산관련
 4) 마치며
① 콘솔에서 한글깨짐등의 이유로 추가세팅이 없는 JDK17로

② 가급적 검증이 된 방법 위주로 구성. Maven등으로 [🔗JMH (the Java Microbenchmark Harness)를 활용한 벤치마크]도 있다고 하는데... 코딩테스트라던가 제한된 환경에 가능한 기법을 우선적으로.

③ 주요 체크항목: CPU 사용량, 실행시간
→ 메모리도 체크해보면 좋긴해도... 위의 2가지가 투입하는 시간 대비 효과가 더 두드러지는편이다보니. 
1. 약간의 차이가 얼마나?
* 테스트 환경
윈도우10
- CPU: AMD Ryzen 5 2600 Six-Core Processor 3.40 GHz
- 램 32 GB 
- VsCode 1.94.2
도커 4.37.1 (178610)
- 컨테이너 이미지: openjdk:17-ea-28-slim
→ 컨테이너 원격 접속을 통한 코드 실행


1) String vs. StringBuilder
① 코드 및 실행
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{
compare1 = ""; //String compare1 = "";
startTime[0][sampleIdx] = System.nanoTime();
for(int idx=0; idx<loopCnt; idx++)
{
compare1 += "_";
}
costTime[0][sampleIdx] = System.nanoTime() - startTime[0][sampleIdx];
}
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{
compare2.setLength(0); //StringBuilder compare2 = new StringBuilder("");
startTime[1][sampleIdx] = System.nanoTime();
for(int idx=0; idx<loopCnt; idx++)
{
compare2.append("_");
}
costTime[1][sampleIdx] = System.nanoTime() - startTime[1][sampleIdx];
}
... ... ...
... ... ...
optimize.compareString(10, 10000);
optimize.compareString(10, 100000);
optimize.compareString(10, 200000);
optimize.compareString(10, 500000);
→ 샘플은 10개로 고정, 샘플별 연산(=loopCnt)은 1만/10만/20만/50만으로
→ 성능 비교를 위해 nano초 단위 측정.

② 결과 및 분석
→ 전문적인 벤치마크가 아니며, 대략적인 성능차이로 참조바랍니다.

String Stringbuilder
10,000회 CPU: 0% 상승 (35→35)
costTime
 avg(ns): 11,650,658
 avg(㎲): 11,650
 avg(ms): 11
 avg(sec): 0
CPU: 0% 상승 (35→35)
costTime
 avg(ns): 491,338
 avg(㎲): 491
 avg(ms): 0
 avg(sec): 0
100,000회 CPU: 5% 상승 (35→40)
costTime
 avg(ns): 685,022,347
 avg(㎲): 685,022
 avg(ms): 685
 avg(sec): 0
CPU: 0% 상승 (40→40)
costTime
 avg(ns): 412,250
 avg(㎲): 412
 avg(ms): 0
 avg(sec): 0
200,000회 CPU: 21% 상승 (40→61)
costTime
 avg(ns): 2,632,230,097
 avg(㎲): 2,632,230
 avg(ms): 2632
 avg(sec): 2
CPU: 0% 상승 (61→61)
costTime
 avg(ns): 880,590
 avg(㎲): 880
 avg(ms): 0
 avg(sec): 0
500,000회 CPU: 42% 상승 (61→103)
costTime
 avg(ns): 14,695,133,992
 avg(㎲): 14,695,133
 avg(ms): 14695
 avg(sec): 14
CPU: 0% 상승 (103→103)
costTime
 avg(ns): 2,266,064
 avg(㎲): 2,266
 avg(ms): 2
 avg(sec): 0
→ 게임에서 프레임 단위로 타이밍 잡아서 조작시 초(sec)보다 좀 더 빠른 ms단위로 해본 경험상, 체감 성능은 최소 ms단위로 놓고보니 단순 계산으로는 10만 급부터 체감되는 성능.
 실제 서비스에서 DB연동 등등 내부에 다른 연산들이 많은거나 마케팅등에서 100만 다운로드부터 성공적으로 보는걸 참조하면, 100만 급에서도 느리지 않게 하려면 문자열 append처리에서 String말고 Stringbuilder쓰는 습관으로.
//서버 인스턴스의 머신 성능으로만 해결하기에는 고사양 머신 요금은 비싸니.
→ 게다가 String은 immutable / stringbuilder는 mutable이라서 전자쪽에 문자열 변경마다 새 객체를 만들다보니.



2) for문
① 코드 및 실행
... ... ...
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{//int[] dataArr = new int[loopCnt];
startTime[CodeCase.CASE_03.ordinal()][sampleIdx] = System.nanoTime();
for(int tmpDatas : dataArr)   
{
tmp = tmpDatas;
}
costTime[CodeCase.CASE_03.ordinal()][sampleIdx] = System.nanoTime() - startTime[CodeCase.CASE_03.ordinal()][sampleIdx];
}
... ... ...
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{//List<Integer> dataList = Arrays.stream(dataArr).boxed().collect(Collectors.toList());
startTime[CodeCase.CASE_04.ordinal()][sampleIdx] = System.nanoTime();
for(int idx=0; idx<loopCnt; idx++)
{
tmp = dataList.get(idx);
}
costTime[CodeCase.CASE_04.ordinal()][sampleIdx] = System.nanoTime() - startTime[CodeCase.CASE_04.ordinal()][sampleIdx];
}
... ... ...
... ... ...
optimize.compareFor(10, 1000);
optimize.compareFor(20, 1000);
optimize.compareFor(50, 1000);
optimize.compareFor(10, 10000);
optimize.compareFor(10, 100000);
optimize.compareFor(10, 500000);
optimize.compareFor(10, 1000000);
optimize.compareFor(10, Integer.MAX_VALUE/1000);
optimize.compareFor(10, Integer.MAX_VALUE/100);
optimize.compareFor(10, Integer.MAX_VALUE/10);
→ for문 횟수쪽 / 배열, ArrayList 조합으로 간단한 성능 비교
→ 성능 비교를 위해 nano초 단위 측정.

② 결과 및 분석
(연산 1000회 고정) int[] dataArr List<Integer> dataList
sampleCnt: 10
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 12,254
 avg(㎲): 12
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 11,468
 avg(㎲): 11
 avg(ms): 0
 
for(int tmpDatas : dataArr)
 avg(ns): 16,174
 avg(㎲): 16
 avg(ms): 0
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 169,719
 avg(㎲): 169
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 73,414
 avg(㎲): 73
 avg(ms): 0
 
for(int tmpDatas : dataList)
 avg(ns): 207,102
 avg(㎲): 207
 avg(ms): 0
sampleCnt: 20
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 16,477
 avg(㎲): 16
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 16,219
 avg(㎲): 16
 avg(ms): 0
 
for(int tmpDatas : dataArr)
 avg(ns): 20,115
 avg(㎲): 20
 avg(ms): 0
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 133,429
 avg(㎲): 133
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 80,808
 avg(㎲): 80
 avg(ms): 0
 
for(int tmpDatas : dataList)
 avg(ns): 116,919
 avg(㎲): 116
 avg(ms): 0
sampleCnt: 50
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 12,673
 avg(㎲): 12
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 13,804
 avg(㎲): 13
 avg(ms): 0
 
for(int tmpDatas : dataArr)
 avg(ns): 18,761
 avg(㎲): 18
 avg(ms): 0
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 371,611
 avg(㎲): 371
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 8,431
 avg(㎲): 8
 avg(ms): 0
 
for(int tmpDatas : dataList)
 avg(ns): 136,052
 avg(㎲): 136
 avg(ms): 0
→ 같은 사이즈라도 ArrayList보다 배열이 빠르다. 새삼스럽지만... (1)
→ 향상된 for문은 원소값 읽기만 할 때는 더 빠르다로 배웠는데 실제로는 딱히?
  //괜히 사용해온거 같은데?

(샘플 10개 고정) int[] dataArr List<Integer> dataList
10,000회
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 49,316
 avg(㎲): 49
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 30,055
 avg(㎲): 30
 avg(ms): 0
 
for(int tmpDatas : dataArr)
 avg(ns): 28,190
 avg(㎲): 28
 avg(ms): 0
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 189,892
 avg(㎲): 189
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 136,439
 avg(㎲): 136
 avg(ms): 0
 
for(int tmpDatas : dataList)
 avg(ns): 192,425
 avg(㎲): 192
 avg(ms): 0
100,000회
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 370,708
 avg(㎲): 370
 avg(ms): 0
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 331,132
 avg(㎲): 331
 avg(ms): 0
 
for(int tmpDatas : dataArr)
 avg(ns): 249,731
 avg(㎲): 249
 avg(ms): 0
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 1,344,518
 avg(㎲): 1,344
 avg(ms): 1
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 815,296
 avg(㎲): 815
 avg(ms): 0
 
for(int tmpDatas : dataList)
 avg(ns): 1,329,307
 avg(㎲): 1,329
 avg(ms): 1
500,000회
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 1,256,024
 avg(㎲): 1,256
 avg(ms): 1
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 1,604,311
 avg(㎲): 1,604
 avg(ms): 1
 
for(int tmpDatas : dataArr)
 avg(ns): 1,235,389
 avg(㎲): 1,235
 avg(ms): 1
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 9,499,419
 avg(㎲): 9,499
 avg(ms): 9
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 4,128,204
 avg(㎲): 4,128
 avg(ms): 4
 
for(int tmpDatas : dataList)
 avg(ns): 6,076,615
 avg(㎲): 6,076
 avg(ms): 6
Integer.MAX_VALUE
/1000

(=2,147,483)
//200백만급
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 5,441,878
 avg(㎲): 5,441
 avg(ms): 5
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 6,604,420
 avg(㎲): 6,604
 avg(ms): 6
 
for(int tmpDatas : dataArr)
 avg(ns): 5,608,300
 avg(㎲): 5,608
 avg(ms): 5
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 27,771,322
 avg(㎲): 27,771
 avg(ms): 27
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 18,529,921
 avg(㎲): 18,529
 avg(ms): 18
 
for(int tmpDatas : dataList)
 avg(ns): 12,160,953
 avg(㎲): 12,160
 avg(ms): 12
Integer.MAX_VALUE
/100

(=21,474,836)
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 7,855,170
 avg(㎲): 7,855
 avg(ms): 7
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 3,384,040
 avg(㎲): 3,384
 avg(ms): 3
 
for(int tmpDatas : dataArr)
 avg(ns): 3,632,626
 avg(㎲): 3,632
 avg(ms): 3
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 15,432,402
 avg(㎲): 15,432
 avg(ms): 15
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 15,184,087
 avg(㎲): 15,184
 avg(ms): 15
 
for(int tmpDatas : dataList)
 avg(ns): 75,211,436
 avg(㎲): 75,211
 avg(ms): 75
Integer.MAX_VALUE
/10
(=214,748,364)
for(int idx=0; idx<dataArr.length; idx++)
 avg(ns): 80,721,729
 avg(㎲): 80,721
 avg(ms): 80
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): ?
 avg(㎲): ?
 avg(ms): ?
 
for(int tmpDatas : dataArr)
 avg(ns): ?
 avg(㎲): ?
 avg(ms): ?
→ 로그 날라갔다...
for(int idx=0; idx<dataList.size(); idx++)
 avg(ns): 113,640,305
 avg(㎲): 113,640
 avg(ms): 113
 
for(int idx=0; idx<loopCnt; idx++)
 avg(ns): 106,033,928
 avg(㎲): 106,033
 avg(ms): 106
 
for(int tmpDatas : dataList)
 avg(ns): 124,312,755
 avg(㎲): 124,312
 avg(ms): 124
→ 같은 사이즈라도 ArrayList보다 배열이 빠르다. 새삼스럽지만... (2)
    특히나 for문 횟수 50만~2천만 범위에서 배열문의 성능이 돋보인다.
→ 조건식에 크기값 변수로 쓰는건 그냥 필수다. 필수. 전에도 for문 최적화를 이걸로 하긴 했는데 그때는 액션스크립트 자체의 처리 속도자체가 느린거였는데 서버처럼 대단위로 해보니 이건 또 규모가 다르군요.
 영문권 자료에도 'Getting the Size of the Collection in the Loop'로 나오는 빈번한 기초이기도 하고.
→ for문에서 컬렉션말고 배열을 사용하면, 못해도 30%정도 단축되는 개선을 기대해볼만?


배열  ==> Arrays.stream().boxed().collect(Collectors.toList()) ==> List류
List류 ==> List형_변수.stream().mapToXxxx(....).toArray()          ==> 배열
변환을 더 알차게 써먹자.

예.
int[] dataArr = new int[loopCnt];
Arrays.fill(dataArr, 1);
List<Integer> dataList = Arrays.stream(dataArr).boxed().collect(Collectors.toList());
int[] convertTest = dataList.stream().mapToInt(Integer::intValue).toArray();
2. 소문난 기법들
1) final 활용
 "Declare methods as final whenever possible. Final methods are handled better by the JVM. ...(중략)... Use the static final key word when creating constants in order to reduce the number of times the variables need to be initialized."
(IBM Documentation, "Java performance guidelines")
 "performance benefits of using the final keyword. In the examples, we showed that applying the final keyword to variables can have a minor positive impact on performance. Nevertheless, applying the final keyword to classes and methods will not result in any performance benefits."

① 코드 및 실행
final int sampleCnt = Integer.MAX_VALUE/100;
... ... ...
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{
mStartTime[CodeCase.CASE_01.ordinal()][sampleIdx] = System.nanoTime();
noneFinalFunc1(); //일반 함수
mCostTime[CodeCase.CASE_01.ordinal()][sampleIdx] = System.nanoTime() - mStartTime[CodeCase.CASE_01.ordinal()][sampleIdx];
}
for(int sampleIdx=0; sampleIdx<sampleCnt; sampleIdx++)
{
mStartTime[CodeCase.CASE_02.ordinal()][sampleIdx] = System.nanoTime();
finalFunc1(); //final로 지정된 함수
mCostTime[CodeCase.CASE_02.ordinal()][sampleIdx] = System.nanoTime() - mStartTime[CodeCase.CASE_02.ordinal()][sampleIdx];
}
... ... ...
 CPU 사용량에서 뭔가 특이사항이 보인다 싶었으나 일시적인 현상으로 판단되서 함수에서는 효과 없어 보입니다. IBM 문서상 해당 머신에는 유효해보이지만, 그리 범용적이진 않은거 같군요.
 baeldung 자료에서는 static함수내 final 변수에서 효과를 봤다는거랑 IBM문서에도 static final 변수를 쓰는 방법이 언급된걸 보면 상수쪽에는 쓸만하다로?



2) 콜렉션과 자료형
2-1) 캐싱과 Pooling
예.
... ... ...
@Configuration
public class TaskConfiguration
{
@Bean(name = "executor")
public ThreadPoolTaskExecutor executor()
{
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("TaskThread-");
executor.setCorePoolSize(... ... ...);
executor.setMaxPoolSize(... ... ...);
... ... ...
게임 엔진 다룰때보다는 서버쪽에서 빈번히 접하는 그거.
객체들을 필요할 때마다 새로 만들기보다는 미리 만들어서 대기시켜두는쪽이.


2-2) 자료형
① Integer/Long/Double 보다는 int/long/double로
 "Usage of primitive types over objects is beneficial since the primitive type data is stored on stack memory and the objects are stored on heap memory. If possible, we can use primitive types instead of objects since data access from stack memory is faster than heap memory"
② 숫자형에서 범위가 그리 크지 않으면 int지만... DB에서 auto increment인건 long이 속편함.
→ 약간 비슷한 원인들로 BigDecimal 남용X



3) 연산관련
3-1) 조건문
→ if-else조건문을 쓸때 가급적 삼항연산자나 switch문으로.
→ 그런데 이중 조건문이랑 이중 switch문으로 테스트를 해본 결과... 결과가 오락가락해서 애매;;;
 PHP서버 맡을때는 조건문 쓰지말고, 꼭 switch문 쓰라고 당부하시길래 그리 했지만.



4) 마치며
 의외로 뻔한 방법들만 있는거 같지만, 생각보다 다들 넘겨버리는것이 기초적인 방법이기도 하죠. 가령 예전에 업무를 맡았던 비공개 프로젝트(언어: 액션스크립트)도 기본적인 최적화 가이드 라인에 있던
- 스프라이트 시트 포맷 선택 및 변환
- (좌표 값) 곱셉/나눗셈 대신 시프트 연산자
- for문 조건식
로 뜯어고치니깐 속도가 약 10배쯤 빨라졌을 정도니...
1) 튜터링 사이트 
사이트명
baeldung The Java final Keyword – Impact on Performance (접속: 2025-02-27)
IBM Documentationjava 성능 지침 (갱신: 2025-01-16)
→ AIX는 IBM의 유닉스고 자바는 JVM의 호환성상.
geeksforgeeks 12 Tips to Optimize Java Code Performance (갱신: 2025-01-16)



2) 블로그 
Java Performance Optimization: Mastering Techniques to Boost Your Applications (접속: 2025-02-27)




3) 웹사이트 
sharedit, "AIX jdk 와 리눅스 jdk 의 차이점에 대해서 자세히 아시는분 계신가요.." (접속: 2025-02-27),  https://www.sharedit.co.kr/qnaboards/21793.

기타. 변경 내력
일자
변경 내력
2025-02-28 초안 및 일반 공개. [🔗blogger] [🔗티스토리].
포스팅 신규 양식 적용(ver.202408)