실행 방법입니다. 외부 jar 는 필요 없습니다.
cd java-src\extension\22_performance
javac -encoding UTF-8 *.java
java -Dstdout.encoding=UTF-8 MainBench — 워밍업·죽은 코드 제거 방지 하네스// Bench.measure: 워밍업 후 반복마다 나노초를 모아 분위수 계산
static void measure(String name, int warmupIters, int iters, Task task) {
for (int i = 0; i < warmupIters; i++) {
sink += task.run(); // 결과를 더해 죽은 코드 제거를 막는다
}
long[] samples = new long[iters];
for (int i = 0; i < iters; i++) {
long start = System.nanoTime();
sink += task.run();
samples[i] = System.nanoTime() - start;
}
Arrays.sort(samples);
// p50·p95·p99·평균을 마이크로초 단위로 출력
}sink 는 static long 필드로, 벤치마크 결과를 계속 더합니다. JIT 가 "아무도 쓰지 않는 계산"이라 판단해 지워버리는 것을 막는 최소한의 장치입니다.
=== 1. String + vs StringBuilder (10만 회 누적) ===
String plus p50=3686671.1us ... ← 약 3.7초
StringBuilder p50= 5971.7us ... ← 약 6ms
=== 2. ArrayList.contains vs HashSet.contains (1만 건 조회) ===
ArrayList.contains p50= 536169.6us ... ← 약 536ms
HashSet.contains p50= 5348.5us ... ← 약 5ms
=== 3. 워밍업 유무에 따른 첫 반복 시간(JIT) ===
워밍업 0회(콜드) p50= 3363.3us p95=11028.1us
워밍업 2000회(웜) p50= 317.6us p95= 737.5us
=== 4. 자체 부하 테스트 (동시 20, 총 200 요청, 각 50ms) ===
요청 200건, 동시 20, p50=80.0ms p95=1608.2ms TPS=74.3
=== 5. JFR 기록 명령 (출력만, 실행 안 함) ===
jcmd <PID> JFR.start name=app duration=60s filename=app.jfr
jcmd <PID> JFR.dump name=app filename=app_snapshot.jfr
jfr print --events jdk.CPULoad app.jfrString + 는 매번 새 문자열을 복사해 만들어, 반복 횟수가 늘수록 O(n²)로 느려집니다. 10만 회에서 StringBuilder 대비 600배 이상 차이가 납니다. ArrayList.contains 도 같은 이유로 HashSet.contains 보다 100배 느립니다.
워밍업 유무 비교에서는 워밍업 후가 콜드보다 p50 기준 10배 빠릅니다. JIT 가 인터프리터 코드를 기계어로 바꾼 효과입니다. 실무 벤치마크에서 워밍업을 빼먹으면 이 차이만큼 결과가 왜곡됩니다.
부하 테스트는 p50 80ms 대비 p95 가 1.6초로 크게 벌어집니다. 로컬 루프백 환경에서 매 요청마다 새 커넥션을 맺을 때 생기는 지연 편차 때문으로, "평균만 보면 문제를 놓친다"는 2.2절 내용을 그대로 보여주는 예시입니다.