노력과 삽질 퇴적물
코드 최적화 -JAVA중점으로- 본문
![]() [이미지 출처: jkfran.com] |
|
▼ 파트1
|
|
1) String vs. StringBuilder 2) for문 |
|
1) final 활용 2) 콜렉션과 자료형 3) 연산관련 4) 마치며 | |
① 콘솔에서 한글깨짐등의 이유로 추가세팅이 없는 JDK17로
② 가급적 검증이 된 방법 위주로 구성. Maven등으로 [🔗JMH (the Java Microbenchmark Harness)를 활용한 벤치마크]도 있다고 하는데... 코딩테스트라던가 제한된 환경에 가능한 기법을 우선적으로.
③ 주요 체크항목: CPU 사용량, 실행시간
→ 메모리도 체크해보면 좋긴해도... 위의 2가지가 투입하는 시간 대비 효과가 더 두드러지는편이다보니.
→ 메모리도 체크해보면 좋긴해도... 위의 2가지가 투입하는 시간 대비 효과가 더 두드러지는편이다보니.
윈도우10
- CPU: AMD Ryzen 5 2600 Six-Core Processor 3.40 GHz
- 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 |
특히나 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."
(baeldung, "The Java final Keyword – Impact on Performance")
① 코드 및 실행 |
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
예.... ... ...@Configurationpublic 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"
(geeksforgeeks.org, "12 Tips to Optimize Java Code Performance")
② 숫자형에서 범위가 그리 크지 않으면 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 Documentation | java 성능 지침 (갱신: 2025-01-16) → AIX는 IBM의 유닉스고 자바는 JVM의 호환성상. |
geeksforgeeks | 12 Tips to Optimize Java Code Performance (갱신: 2025-01-16) |
2) 블로그 |
Java Performance Optimization: Tips and Techniques (접속: 2025-02-27)
자바의 반복문에 대한 아주 얕은 고찰 (접속: 2025-02-27)
3) 웹사이트 |
sharedit, "AIX jdk 와 리눅스 jdk 의 차이점에 대해서 자세히 아시는분 계신가요.." (접속: 2025-02-27), https://www.sharedit.co.kr/qnaboards/21793.
▼ 기타. 변경 내력 |
일자
|
변경 내력 |
2025-02-28 | 초안 및 일반 공개. [🔗blogger] [🔗티스토리]. 포스팅 신규 양식 적용(ver.202408) |
'📂게임개발 note > 미분류' 카테고리의 다른 글
단편적인 IT개발 샘플A - AWS 자격증. (1) | 2024.10.22 |
---|---|
컨퍼런스 참관 기록 (0) | 2024.10.03 |
토이 프로젝트로 소프트스킬 맛보기 -외부 협력이면 피할 수 없다- (0) | 2024.07.06 |
현재 실무에서 주로 쓰는 개발툴들 (0) | 2022.07.30 |
신입 게임개발자, 포트폴리오에서 퇴사까지 (2) | 2018.05.07 |