부록: 성능 벤치마크

이 부록은 리포지토리의 benchmark/ 디렉터리에 있는 벤치마크를 한 번 실행한 결과를 보고하며, 이 벤치마크는 Spider Silk와 Spring Boot를 같은 머신에서 같은 JVM과 같은 부하로 측정한다. 측정은 2026-10-12 밤에 머신에 기술한 노트북에서, 1.1.0 릴리스 이후 165개 커밋을 지난 main 브랜치의 커밋 `3d85b96`을 대상으로 했다.

요약

아래 수치는 중앙값이며, 이 머신과 이 핸들러에만 해당한다.

  • 단순 텍스트와 작은 JSON 객체는 각각 Jetty 위의 Spider Silk가 Spring Boot보다 초당 약 1.42배 많은 요청을 처리한다. 같은 컨테이너인 Tomcat 위에서는 그 차이가 약 1.31배와 1.24배다.

  • 직렬화기가 대부분의 일을 하는 레코드 100개의 JSON 목록은 Jetty 위의 Spider Silk가 Jackson을 쓰는 Spring Boot보다 초당 1.72배 많은 요청을 처리하며, Tomcat 위에서는 1.37배를 처리한다.

  • 파서가 대부분의 일을 하는, 같은 목록을 요청 본문에서 읽는 케이스는 어느 컨테이너에서든 Spider Silk가 초당 약 2배 많은 요청을 처리한다.

  • Spider Silk의 기본 엔진인 jte로 렌더링한 페이지는 Thymeleaf를 쓰는 Spring Boot보다 초당 약 3.1배 많은 요청을 처리한다. 양쪽 모두 Thymeleaf를 쓰고 같은 컨테이너인 Tomcat 위에서 실행하면 두 프레임워크는 2% 이내로 차이가 난다.

  • JVM 프로세스는 Jetty 위의 Spider Silk가 295 ms, Spring Boot가 902 ms에 부팅하며, 그 시점의 상주 메모리는 117 MB 대 210 MB다.

  • GraalVM CE 네이티브 이미지로는 Spider Silk가 24 ms, Spring Boot가 37 ms에 부팅하며, 이미지 빌드에는 각각 72초와 85초가 걸린다. GraalVM CE 이미지는 모든 케이스에서 JVM보다 초당 20%~86% 적은 요청을 처리한다.

  • Oracle GraalVM의 프로파일 기반 최적화(PGO)로 빌드한 네이티브 이미지는 GraalVM CE 이미지보다 초당 1.18배~1.98배 많은 요청을 처리한다. Spider Silk의 PGO 이미지는 JVM 처리량의 79%~94%를, Spring Boot의 PGO 이미지는 27%~89%를 유지한다.

측정 대상

대상 서버

아홉 서버를 각자 별도 프로세스에서 측정했다.

대상 스택 런타임

spider-silk-jetty

Spider Silk, 임베디드 Jetty 12.1.13, jte 3.2.4 또는 Thymeleaf 3.1.5

JVM

spider-silk-tomcat

Spider Silk, 임베디드 Tomcat 11.0.26 위의 spider-silk-tomcat, jte 3.2.4 또는 Thymeleaf 3.1.5

JVM

spring-boot

Spring Boot 4.1.1 (Spring Framework 7.0.9), Spring Web MVC, 임베디드 Tomcat 11.0.24, Jackson 3.1.5, Thymeleaf 3.1.5

JVM

spider-silk-jetty-native

`spider-silk-jetty`와 같은 애플리케이션

GraalVM CE 네이티브 이미지

spider-silk-jetty-native-oracle

`spider-silk-jetty`와 같은 애플리케이션

Oracle GraalVM 네이티브 이미지

spider-silk-jetty-native-pgo

`spider-silk-jetty`와 같은 애플리케이션

PGO를 적용한 Oracle GraalVM 네이티브 이미지

spring-boot-native

`spring-boot`와 같은 애플리케이션, AOT 처리를 거침

GraalVM CE 네이티브 이미지

spring-boot-native-oracle

`spring-boot`와 같은 애플리케이션, AOT 처리를 거침

Oracle GraalVM 네이티브 이미지

spring-boot-native-pgo

`spring-boot`와 같은 애플리케이션, AOT 처리를 거침

PGO를 적용한 Oracle GraalVM 네이티브 이미지

Spring Boot는 기본 설정으로 실행한다: 설정한 프로퍼티가 없으며, Tomcat, Jackson, Thymeleaf는 스타터가 가져오는 그대로다. Spider Silk도 인자로 고르는 서버와 템플릿 엔진 외에는 기본 설정으로 실행한다. 두 서버 모두 최대 200개의 플랫폼 스레드 풀을 쓰며, 어느 쪽도 가상 스레드를 쓰지 않는다.

Spider Silk의 네이티브 이미지는 Jetty용으로만 빌드했다.

케이스

각 케이스는 두 프레임워크에서 같은 응답을 받는 요청 하나다.

케이스 경로 Spider Silk Spring Boot 본문

text

/text

WebResponse.text("Hello, World!")

text/plain`으로 `String`을 반환하는 `@GetMapping

13 B

json

/json

WebResponse.json(new Message(…​), MessageJson.CODEC), 생성된 코덱

Message 레코드를 반환하는 @RestController, Jackson이 씀

27 B

json-list

/items

생성된 코덱의 JsonCodec.list`를 쓰는 `WebResponse.json(items, ITEMS)

List<Item>`을 반환하는 `@RestController, Jackson이 씀

10,490 B

json-post

/items

POST`에서 `req.bodyJson(ITEMS), 개수를 텍스트로 응답

@RequestBody List<Item>`을 받는 `@PostMapping, Jackson이 읽음

10,490 B 수신, 3 B 송신

template-jte

/fortunes

WebResponse.template("fortunes", model), 사전 컴파일한 jte

없음

1,177 B

template-thymeleaf

/fortunes

WebResponse.template("fortunes", model), ThymeleafTemplates

뷰 이름을 반환하는 @Controller, Spring Boot의 Thymeleaf 뷰 리졸버

1,178 B

json-list 본문은 필드 다섯 개짜리 레코드 100개이며, 두 프레임워크는 이를 바이트 단위까지 똑같이 보낸다. json-post 케이스는 그 문서를 POST /items`의 본문으로 보내며, 두 프레임워크는 이를 레코드로 읽고 그 개수로 응답한다. `/fortunes 페이지는 TechEmpower 벤치마크의 fortune 12개를 담은 표이며, 그중 하나는 HTML 이스케이프가 필요하고 하나는 일본어다. 두 Thymeleaf 페이지는 같은 템플릿 파일이며 같은 바이트로 렌더링된다. jte 페이지는 빈 줄, 그리고 텍스트 안의 "와 '를 이스케이프하지 않는다는 점이 다르며, 이는 HTML이 허용하는 형태다. 핸들러가 응답하는 값은 모두 시작할 때 한 번 만들어 두므로, 요청은 데이터가 아니라 프레임워크, 직렬화기, 렌더러를 측정한다.

측정 방법

benchmark/run.sh가 전체 실행을 이끈다.

  1. native-build.sh가 애플리케이션마다 Oracle GraalVM의 --pgo-instrument로 프로파일 기반 최적화용 계측 이미지를 빌드한다. pgo-profile.sh는 그 이미지를 애플리케이션이 처리하는 모든 케이스의 부하 아래에서 케이스마다 10초씩 실행하며, 이미지는 멈출 때 모은 횟수를 프로파일로 쓴다. Spider Silk는 인자가 시작할 때 엔진을 고르므로 jte용과 Thymeleaf용 프로파일 두 개를 얻는다.

  2. 이어서 native-build.sh가 두 애플리케이션을 번갈아 가며, 깨끗한 프로젝트에서 애플리케이션마다 세 가지 네이티브 이미지를 각각 세 번씩 빌드한다. PGO 이미지는 그 프로파일에 --pgo를 주어 빌드하며, 이때 최적화 수준도 -O2에서 -O3로 올라간다.

  3. 1분을 쉰 뒤, startup.sh가 각 대상을 열 번씩 시작하며, 매 시작 전에 3초를 쉰다. 이 스크립트는 프로세스를 띄우기 직전부터 /text의 첫 200까지를 10 ms 간격으로 폴링해 재고, 그 시점의 프로세스 상주 메모리를 읽는다.

  4. 이어서 각 케이스마다 새 프로세스를 띄운다: 20초 워밍업, 30초 측정 후 프로세스를 멈춘다.

  5. 한 라운드는 모든 케이스를 한 번씩, 케이스별로 모든 대상에 걸쳐 실행하며, 세 라운드를 차례로 실행한다. 따라서 열이 오르며 느려지는 머신은 그 편차를 마지막 대상이 아니라 모든 대상에 고르게 나눈다.

최대 부하 상태에서 이전 프로세스가 멈춘 직후에 시작한 프로세스는 부팅이 최대 1.5배까지 느리므로, 부팅 시간은 부하 케이스와 따로 잰다.

부하는 wrk 4.1.0이 루프백으로 스레드 4개와 keep-alive 연결 64개를 써서 건다. 서버는 성능 코어 네 개인 CPU 0~3에, wrk는 저전력 효율 코어 네 개인 CPU 4~7에 고정한다. 모든 JVM과 네이티브 이미지에는 -Xms1g -Xmx1g를 준다. JVM은 Temurin 25이며 기본 컬렉터인 G1을 쓴다. -native 이미지는 GraalVM CE 25.2.4로 빌드하며, 이 배포판의 컬렉터는 Serial GC뿐이다. -native-oracle과 -native-pgo 이미지는 Oracle GraalVM 25.4.4로 빌드하며, 기본 컬렉터인 Serial GC를 그대로 쓴다. 기본 설정으로 빌드하면 Oracle GraalVM은 기계 학습 모델로 코드에서 메서드마다 프로파일을 추론한다. PGO 이미지는 그 자리에 기록한 프로파일을 쓴다.

보고하는 처리량과 각 지연 시간 백분위수는 세 라운드의 중앙값을 각각 따로 구한 것이다. 라운드 간 대부분 케이스의 처리량은 중앙값에서 10% 이내에 머물렀다. `spider-silk-jetty`는 한 라운드에서 `text`의 중앙값보다 36% 높았고, `json-list`에서 13% 높았다. `spring-boot`는 `json`에서 중앙값보다 11% 높았다. 네이티브 이미지 중에서는 여덟 케이스에서 한 라운드가 중앙값보다 10%~15% 높았고, 두 케이스에서 11%~12% 낮았다: `json`의 `spring-boot-native`와 `json-post`의 `spider-silk-jetty-native`다. 세 라운드의 중앙값은 그런 라운드를 걸러 낸다. 실행 중 실패한 요청은 없었다.

머신

머신

Lenovo ThinkPad X1 Carbon Gen 13, AC 전원 연결

CPU

Intel Core Ultra 7 258V (Lunar Lake): 최대 4.8 GHz 성능 코어 4개와 최대 3.7 GHz 저전력 효율 코어 4개, 8스레드, L3 캐시 12 MiB

메모리

30 GiB (32 GB LPDDR5X)

OS

Ubuntu 24.04.5 LTS, Linux 6.17.0-1033-oem

CPU 클럭

powersave 거버너, performance 플랫폼 프로파일

JVM

Eclipse Temurin 25 (25+36-LTS)

네이티브 이미지

GraalVM CE 25.2.4 (native-image 25.0.4), Oracle GraalVM 25.4.4 (native-image 25.0.4.1.1)

빌드

Gradle 9.5.1

부하 생성기

wrk 4.1.0 (Ubuntu 패키지 wrk 4.1.0-4build2)

실행 중에도 데스크톱 세션은 켜져 있었으며, 터미널, Dropbox, 유휴 상태의 로컬 텍스트 음성 변환 서버가 열려 있었고, 이들은 쉬는 동안 CPU 하나의 몇 퍼센트를 쓴다. 웹 브라우저 창은 열려 있지 않았다.

결과

처리량과 지연 시간

처리량은 초당 요청 수다. 지연 시간은 wrk가 요청을 보낸 때부터 응답을 받을 때까지 잰 시간이며, 단위는 밀리초다. CPU는 측정 동안 서버 자신이 쓴 CPU 시간이며, 400%는 측정 내내 코어 네 개가 바빴다는 뜻이다.

케이스 대상 초당 요청 수 p50 p90 p99 CPU

text

spider-silk-jetty

256,545

0.26

0.47

0.79

381%

spider-silk-tomcat

237,567

0.24

0.44

0.60

281%

spring-boot

180,984

0.33

0.49

0.80

304%

spider-silk-jetty-native

191,639

0.35

0.66

1.34

371%

spider-silk-jetty-native-oracle

214,336

0.31

0.58

1.19

373%

spider-silk-jetty-native-pgo

239,214

0.28

0.52

1.12

374%

spring-boot-native

117,689

0.50

0.77

2.06

320%

spring-boot-native-oracle

132,754

0.44

0.68

1.68

308%

spring-boot-native-pgo

152,600

0.38

0.63

1.56

299%

json

spider-silk-jetty

256,318

0.26

0.48

0.82

381%

spider-silk-tomcat

224,852

0.25

0.45

0.61

287%

spring-boot

180,642

0.33

0.50

0.79

302%

spider-silk-jetty-native

204,013

0.32

0.63

1.31

370%

spider-silk-jetty-native-oracle

213,319

0.31

0.59

1.21

372%

spider-silk-jetty-native-pgo

240,873

0.27

0.52

1.09

374%

spring-boot-native

124,780

0.48

0.76

1.98

312%

spring-boot-native-oracle

129,453

0.45

0.70

1.75

307%

spring-boot-native-pgo

155,736

0.37

0.61

1.59

302%

json-list

spider-silk-jetty

163,817

0.41

0.70

1.10

379%

spider-silk-tomcat

130,528

0.46

0.66

1.05

326%

spring-boot

95,200

0.67

0.89

1.25

349%

spider-silk-jetty-native

98,275

0.68

1.13

1.73

377%

spider-silk-jetty-native-oracle

123,486

0.54

0.90

1.42

377%

spider-silk-jetty-native-pgo

149,915

0.44

0.75

1.20

378%

spring-boot-native

13,610

3.95

10.07

32.79

388%

spring-boot-native-oracle

17,729

3.23

5.40

18.02

381%

spring-boot-native-pgo

25,394

2.28

3.93

9.77

373%

json-post

spider-silk-jetty

114,480

0.58

0.97

1.60

381%

spider-silk-tomcat

111,260

0.55

0.81

1.34

320%

spring-boot

55,860

1.13

1.42

2.19

360%

spider-silk-jetty-native

83,549

0.79

1.47

2.60

369%

spider-silk-jetty-native-oracle

80,968

0.82

1.42

2.36

372%

spider-silk-jetty-native-pgo

102,891

0.64

1.12

1.93

371%

spring-boot-native

7,654

8.87

18.27

28.53

330%

spring-boot-native-oracle

11,833

4.33

15.81

58.55

388%

spring-boot-native-pgo

15,050

3.63

8.32

31.50

385%

template-jte

spider-silk-jetty

197,418

0.34

0.61

1.09

378%

spider-silk-tomcat

182,370

0.32

0.51

0.81

302%

spider-silk-jetty-native

140,651

0.47

0.88

1.66

367%

spider-silk-jetty-native-oracle

160,281

0.41

0.76

1.50

369%

spider-silk-jetty-native-pgo

181,412

0.37

0.69

1.39

370%

template-thymeleaf

spider-silk-jetty

70,928

0.93

1.61

2.53

377%

spider-silk-tomcat

62,910

0.99

1.31

2.09

357%

spring-boot

63,927

1.00

1.32

1.98

349%

spider-silk-jetty-native

28,340

2.33

4.13

6.78

368%

spider-silk-jetty-native-oracle

45,465

1.44

2.68

4.57

365%

spider-silk-jetty-native-pgo

56,220

1.17

2.12

3.66

365%

spring-boot-native

36,143

1.69

2.60

5.09

358%

spring-boot-native-oracle

40,469

1.46

2.34

3.77

362%

spring-boot-native-pgo

56,804

1.06

1.55

2.72

350%

아래 비율은 Spider Silk의 처리량을 Spring Boot의 처리량으로 나눈 값이므로, 1보다 크면 Spider Silk가 앞선다. 마지막 세 열은 같은 GraalVM으로 빌드한 두 애플리케이션의 네이티브 이미지끼리 견준다.

케이스 Jetty 대 Spring Boot Tomcat 대 Spring Boot GraalVM CE Oracle GraalVM PGO

text

1.42

1.31

1.63

1.61

1.57

json

1.42

1.24

1.63

1.65

1.55

json-list

1.72

1.37

7.22

6.97

5.90

json-post

2.05

1.99

10.92

6.84

6.84

Spider Silk의 jte 대 Spring Boot의 Thymeleaf

3.09

2.85

3.89

3.96

3.19

양쪽 모두 Thymeleaf

1.11

0.98

0.78

1.12

0.99

네이티브 이미지와 JVM

아래 각 칸은 네이티브 이미지의 처리량을 같은 애플리케이션의 JVM 처리량에 대한 비율로 나타낸 것이며, Spider Silk는 Jetty 위의 값이다.

케이스 애플리케이션 GraalVM CE Oracle GraalVM PGO

text

Spider Silk

75%

84%

93%

Spring Boot

65%

73%

84%

json

Spider Silk

80%

83%

94%

Spring Boot

69%

72%

86%

json-list

Spider Silk

60%

75%

92%

Spring Boot

14%

19%

27%

json-post

Spider Silk

73%

71%

90%

Spring Boot

14%

21%

27%

template-jte

Spider Silk

71%

81%

92%

template-thymeleaf

Spider Silk

40%

64%

79%

Spring Boot

57%

63%

89%

Oracle GraalVM의 기본 설정은 GraalVM CE보다 0.97배~1.60배 많은 요청을 처리하며, 기록한 프로파일은 그 위에 1.12배~1.43배를 더한다. PGO 이미지는 한 곳을 빼고 모든 백분위수에서 GraalVM CE 이미지보다 지연 시간도 낮다. spring-boot-native-pgo의 p99는 json-list에서 GraalVM CE의 32.79 ms에 비해 9.77 ms이지만, json-post에서는 p50이 8.87 ms에서 3.63 ms로 내려가는 동안 p99는 31.50 ms로 GraalVM CE의 28.53 ms보다 낫지 않다.

부팅 시간과 메모리

부팅 시간은 프로세스를 띄우기 직전부터 첫 200까지를 열 번 잰 값의 중앙값이다. 메모리는 그 시점의 프로세스 상주 메모리(RSS)다.

대상 부팅 시간 (중앙값) 최소 최대 상주 메모리

spider-silk-jetty

295 ms

292 ms

300 ms

117 MB

spider-silk-tomcat

344 ms

342 ms

348 ms

117 MB

spring-boot

902 ms

887 ms

911 ms

210 MB

spider-silk-jetty-native

24 ms

21 ms

24 ms

37 MB

spider-silk-jetty-native-oracle

24 ms

23 ms

41 ms

40 MB

spider-silk-jetty-native-pgo

23 ms

9 ms

24 ms

28 MB

spring-boot-native

37 ms

36 ms

38 ms

73 MB

spring-boot-native-oracle

37 ms

37 ms

38 ms

75 MB

spring-boot-native-pgo

37 ms

34 ms

38 ms

51 MB

폴링은 curl 자체의 기동을 포함해 약 14 ms 간격으로 이루어지므로, 네이티브 이미지의 부팅 시간은 그 눈금 위에 놓인다. Spider Silk의 PGO 이미지는 열 번 중 네 번은 첫 폴링에 9~10 ms로, 나머지는 두 번째 폴링에 23~24 ms로 응답했다. PGO 이미지는 부팅 시점의 메모리도 적다: GraalVM CE의 37 MB와 73 MB에 비해 28 MB와 51 MB다.

Spring Boot 자신의 로그는 JVM에서 Started SpringBootBenchmark in 0.72 seconds, GraalVM CE 네이티브 이미지에서 in 0.024 seconds를 보고한다. 표와의 차이는 JVM 자체의 기동 시간과 첫 응답까지의 시간이다.

부하 중 JVM 프로세스는 770~870 MB를 차지했으며, 그 대부분은 -Xms1g가 확보하는 1 GiB 힙이다. 네이티브 이미지는 55~351 MB를 차지했으며, spring-boot-native의 json-post에서 가장 많았다.

네이티브 이미지 빌드

각 네이티브 이미지는 깨끗한 프로젝트에서 세 번씩, 계측 이미지는 한 번씩 빌드했다. 빌드 시간은 애플리케이션 컴파일, 해당하는 경우 Spring의 AOT 처리, native-image 실행을 포함한다. 애플리케이션이 의존하는 Spider Silk 모듈은 이미 컴파일되어 있었으므로 포함하지 않는다.

애플리케이션 이미지 빌드 (중앙값) native-image 단독 native-image 최대 메모리 바이너리

Spider Silk

GraalVM CE

71.5 s

70 s

4.2 GiB

63.6 MB

Oracle GraalVM

133.3 s

131 s

4.6 GiB

74.1 MB

Oracle GraalVM, 계측

142.8 s

141 s

7.0 GiB

167.6 MB

Oracle GraalVM, PGO

86.5 s

85 s

4.5 GiB

47.2 MB

Spring Boot

GraalVM CE

84.5 s

81 s

4.0 GiB

73.9 MB

Oracle GraalVM

152.1 s

149 s

4.9 GiB

84.9 MB

Oracle GraalVM, 계측

171.3 s

168 s

9.1 GiB

197.5 MB

Oracle GraalVM, PGO

104.6 s

102 s

4.0 GiB

55.1 MB

native-image는 모든 빌드에서 CPU 여덟 개를 6.5~7.1의 부하로 썼다. PGO 빌드는 Oracle GraalVM의 기본 빌드보다 시간이 약 3분의 1 덜 걸리며, 그 바이너리는 GraalVM CE의 바이너리보다 약 4분의 1 작다. PGO 이미지에는 먼저 계측 빌드와 프로파일 수집 실행이 필요하며, 계측 이미지는 GraalVM CE 이미지의 2.6배 크기다. JVM 애플리케이션의 의존성은 Spider Silk가 9.6 MB, Spring Boot가 21 MB다.

직렬화기와 파서만 따로

benchmark/json-bench는 HTTP 없이 JSON 엔진만을 HTTP 케이스가 보내는 문서로 잰다: json-list의 레코드 100개와 json의 27바이트 객체다. 소수부가 있는 숫자를 셋씩 가진 장소 100개와 상태가 enum인 주문 100개도 재는데, 이를 보내는 HTTP 케이스는 없다. 같은 레코드 100개를 더 여러 형태로도 잰다: 이스케이프할 것이 없는 것, 문자열이 한글인 것, 키가 역순인 것, 설명이 영어와 한글로 200자인 것, Python의 json.dumps와 pretty printer가 쓰듯 공백을 넣은 것, id가 13자리와 18자리인 것, 문자열이 0~1자인 것이다. 역순 키와 공백을 넣은 키는 다른 프로그램이 쓴 문서를, 긴 설명은 상품 페이지 같은 산문을 대표한다. 측정은 2026-10-11에 같은 머신에서, AC 전원과 performance 플랫폼 프로파일로 실행했다. 커밋 11b9a13 위에 변경을 얹어 Java 25용으로 컴파일한 silk-json을 대상으로, JMH 1.37로 포크 둘, 포크마다 각 1초짜리 워밍업 다섯 번과 측정 다섯 번, CPU 0~3에 고정, -Xms1g -Xmx1g, Temurin 25로 쟀다. 각 칸은 연산당 평균 시간과 연산당 할당 바이트이며, 후자는 JMH의 gc 프로파일러가 잰 값이다.

연산 경로 시간 할당

레코드 100개 쓰기 (10,490 B)

JsonWriter.tree로 쓴 Json.object() 트리

8.0 µs

24.1 KB

손으로 작성한 JsonWriter

5.9 µs

11.3 KB

생성된 코덱

4.7 µs

10.7 KB

Jackson 3.1

11.8 µs

19.7 KB

fastjson2 2.0.57

4.6 µs

10.7 KB

fastjson2 2.0.57, creator=reflect

5.7 µs

10.7 KB

레코드 100개를 버퍼에서 스트림으로 쓰기

생성된 코덱, writeTo

3.8 µs

184 B

fastjson2 2.0.57, JSON.writeTo

3.6 µs

184 B

이스케이프할 것이 없는 레코드 100개 쓰기 (10,090 B)

생성된 코덱

4.2 µs

10.3 KB

fastjson2 2.0.57

3.6 µs

10.3 KB

한글 레코드 100개 쓰기 (9,590 B)

생성된 코덱

4.0 µs

9.8 KB

fastjson2 2.0.57

3.6 µs

9.8 KB

설명이 200자인 레코드 100개 쓰기 (27,498 B)

생성된 코덱

6.8 µs

28.4 KB

fastjson2 2.0.57

6.3 µs

27.7 KB

설명이 한글 200자인 레코드 100개 쓰기 (54,516 B)

생성된 코덱

23.0 µs

55.4 KB

fastjson2 2.0.57

37.0 µs

54.7 KB

주문 100개 쓰기 (5,985 B)

생성된 코덱

2.4 µs

6.2 KB

fastjson2 2.0.57

2.7 µs

6.2 KB

레코드 100개 읽기

손으로 작성한 JsonReader.object

14.2 µs

37.8 KB

생성된 코덱

5.6 µs

17.6 KB

Jackson 3.1

23.7 µs

52.8 KB

fastjson2 2.0.57

6.1 µs

24.8 KB

fastjson2 2.0.57, creator=reflect

24.5 µs

111.2 KB

한글 레코드 100개 읽기

생성된 코덱

5.7 µs

23.1 KB

fastjson2 2.0.57

7.9 µs

34.1 KB

키가 역순인 레코드 100개 읽기

생성된 코덱

7.3 µs

17.6 KB

fastjson2 2.0.57

6.3 µs

24.8 KB

json.dumps가 공백을 넣은 레코드 100개 읽기 (11,489 B)

생성된 코덱

6.7 µs

17.6 KB

fastjson2 2.0.57

7.0 µs

24.8 KB

들여쓴 레코드 100개 읽기 (14,091 B)

생성된 코덱

9.0 µs

17.6 KB

fastjson2 2.0.57

8.1 µs

24.8 KB

설명이 200자인 레코드 100개 읽기

생성된 코덱

6.6 µs

34.3 KB

fastjson2 2.0.57

5.4 µs

34.4 KB

id가 13자리인 레코드 100개 읽기

생성된 코덱

5.9 µs

17.6 KB

fastjson2 2.0.57

6.9 µs

24.8 KB

id가 18자리인 레코드 100개 읽기

생성된 코덱

6.0 µs

17.6 KB

fastjson2 2.0.57

7.3 µs

24.8 KB

문자열이 0~1자인 레코드 100개 읽기 (6,956 B)

생성된 코덱

2.8 µs

5.5 KB

fastjson2 2.0.57

3.9 µs

14.8 KB

주문 100개 읽기

생성된 코덱

3.1 µs

9.5 KB

fastjson2 2.0.57

3.2 µs

9.6 KB

27바이트 객체 쓰기

생성된 코덱

25 ns

232 B

Jackson 3.1

135 ns

496 B

fastjson2 2.0.57

27 ns

216 B

장소 100개 쓰기 (8,263 B)

생성된 코덱

9.4 µs

8.5 KB

Jackson 3.1

20.1 µs

50.1 KB

fastjson2 2.0.57

9.5 µs

17.6 KB

장소 100개 읽기

생성된 코덱

5.1 µs

11.1 KB

Jackson 3.1

26.6 µs

59.7 KB

fastjson2 2.0.57

5.3 µs

11.2 KB

생성된 코덱은 목록을 Jackson 시간의 40%에 쓰고 24%에 읽으며, 할당은 3분의 1에서 절반 사이이고, 작은 객체는 Jackson 시간의 5분의 1이 안 되는 시간에 쓴다. 손으로 작성한 라이터는 트리와 코덱 사이에 놓이는데, 코덱이 상수에서 키를 복사하는 자리에서 라이터는 문서가 이미 쓴 키들 가운데서 키를 찾기 때문이다. 코덱은 10 KB 문서를 fastjson2 시간의 92%에 읽고, 작은 객체는 그 92%의 시간에 쓴다. fastjson2는 10 KB 문서 쓰기에서 3% 차이로 여전히 앞선다. 그 우위는 문자열을 쓰는 데서 나오며, 이스케이프할 문자열이 없는 곳에서 더 벌어진다: 평범한 레코드에서 1.17배, 한글 레코드에서 1.11배다. fastjson2는 String의 바이트를 sun.misc.Unsafe로 직접 읽는데, JDK는 이를 거두어들이는 중이고 코어는 쓰지 않는다. 코덱은 문자열의 문자를 getChars로 복사해 내고 하나씩 검사한다. 올바른 라이터라면 설 수 없는 바닥, 곧 문자열의 바이트를 아무 검사 없이 복사하는 쓰기는 평범한 레코드를 2.7 µs에 써서 fastjson2의 3.7 µs보다 빠르다. 그 복사본을 8바이트씩 검사하면 4.0 µs가 되므로, 우위는 전부 문자열 표현의 몫이다. 코덱은 따옴표와 역슬래시를 평범한 문자를 쓰는 같은 루프에서 이스케이프하며, 문자마다 표를 찾는 대신 비교한다. 그 전에는 첫 따옴표가 문자열의 나머지를 더 느린 루프로 보냈고, 같은 쓰기에 5.3 µs가 들었다. JDK 25의 JIT 컴파일러는 char를 byte로 좁히는 루프를 벡터화하지 않으므로, 쓰기 전에 문자열 전체를 검사하는 단계는 빨라지지 않고 오히려 느리게 측정됐다. 코덱은 멤버의 키와 값을 호출 하나, put(JsonKey, …​)로 쓰며, 이 호출은 라이터의 상태를 한 번 검사한다. 1000 미만의 수는 저장 한 번으로, 한글 음절 같은 3바이트 문자는 4바이트 저장 한 번으로 쓴다. 이전 빌드와 연달아 재면, 이로써 평범한 레코드가 4.4 µs에서 4.2 µs로, 한글 레코드가 4.4 µs에서 4.0 µs로 줄었다. 릴리스가 Java 25를 요구하기 전에 대상으로 삼던 Temurin 21에서도 비율은 같았다: fastjson2는 평범한 레코드를 1.15배, 한글 레코드를 1.13배 빠르게 쓰고, 코덱은 목록을 fastjson2 시간의 88%에 읽는다.

64자 이상의 문자열은 다른 경로를 탄다. JDK의 US_ASCII 인코더가 JIT 컴파일러의 인트린식으로 이를 버퍼에 좁혀 쓴다. 또 다른 인트린식인 문자열 자신의 indexOf가 따옴표와 역슬래시를 찾고, 그 앞까지 좁힌 바이트는 제어 문자만 8개씩 검사한다. 이스케이프는 그 뒤에 좁혀 둔 바이트를 다시 좁히지 않고 뒤로 민다. 인코더는 ASCII 밖의 첫 문자에서 멈추고, 문자열의 나머지는 한 문자씩 쓰는 루프로 간다. 이전 빌드들과 연달아 재면, 이로써 설명이 긴 레코드가 15.0 µs에서 8.6 µs로, 다시 6.9 µs로 줄었다. fastjson2는 여전히 이를 1.08배 빠르게 쓴다. 한글로 된 긴 설명은 코덱이 fastjson2 시간의 62%에 쓴다. Java 25에는 문자열의 Latin-1 바이트를 char로 넓히지 않고 복사하는 유일한 공개 호출인 MemorySegment.setString이 있다. 이를 거쳐 복사하고 바이트를 8개씩 검사하면 10 KB 목록 쓰기가 4.9 µs에서 5.4 µs로, 긴 설명 쓰기가 6.8 µs에서 7.1 µs로 느려졌으므로, 코덱은 이를 쓰지 않는다. 코덱은 이 문서에 0.7 KB를 더 할당하는데, 문서마다 한 번 만드는 인코더와 그 둘레의 버퍼 둘이다. 평범한 문자열은 Temurin 21, Temurin 25, GraalVM의 JIT 컴파일러 모두에서 약 16자부터 이 경로가 이득이다. 이스케이프가 있는 문자열은 컴파일러에 따라 48자에서 96자부터야 이득이고, 기준값 64는 그 둘 사이에 있다. 이 경로가 처음에 10 KB 목록에 지운 2~2.5%의 비용은 더는 보이지 않는다: 같은 빌드에서 이 경로를 빼면 목록 쓰기는 4.72 µs가 아니라 4.68 µs다.

enum 상수는 JsonKey로 한 번만 인코딩한 이름으로 쓰고 읽는다. 이름은 키처럼 복사해 쓰고, 읽을 때는 문자열을 만들지 않고 제자리에서 비교한다. 이전 빌드와 연달아 재면, 이로써 주문 쓰기가 2.9 µs에서 2.4 µs로, 읽기가 4.6 µs에서 3.0 µs로 줄었고, 지금은 fastjson2 시간의 91%와 96%다. 생성된 코덱은 enum이 어디에 있든 이렇게 한다: 프로퍼티, Optional, 컬렉션 원소, 맵 값이다.

코덱은 한글 레코드를 fastjson2 시간의 72%에 읽는다. ASCII 밖의 바이트가 있는 문자열은 같은 패스에서 검사하며 문자로 디코드하고, 그 앞의 ASCII는 검사 없이 넓힌다. 그 전에는 바이트를 검사한 뒤 JDK에 다시 디코드를 맡겼고, 같은 읽기에 11.0 µs가 들었다. fastjson2는 이를 검사하지 않는다: 과잉 길이 시퀀스나 UTF-8로 쓴 서로게이트가 값 안에서 U+FFFD가 되며, 코덱은 그런 문서를 거부한다. 생성된 읽기는 선언된 키를 먼저 순서대로 시도한다. 그 순서를 벗어난 키는 다음 멤버가 있는 자리의 4바이트로 switch해서 찾으며, 이 4바이트는 JsonInput.keyHead()가 읽는다. 이전 빌드와 연달아 재면, 이로써 목록이 6.0 µs에서 5.5 µs로, 역순 목록이 9.5 µs에서 8.0 µs로 줄었다. 문서가 선언 순서를 한 번 벗어나면 그 뒤의 키는 곧장 그 switch로 가며, 이로써 역순 레코드가 7.2 µs가 됐다. fastjson2는 여전히 이를 1.17배 빠르게 읽는다. 읽기는 키 앞과 쉼표 뒤의 공백도 건너뛰며, 쉼표 하나와 공백 하나를 먼저 확인한다. 이전 빌드와 연달아 재면, 이로써 json.dumps가 공백을 넣은 레코드가 10.0 µs에서 6.6 µs로 줄어 fastjson2 시간의 95%가 됐고, 들여쓴 레코드가 11.6 µs에서 8.6 µs로 줄었다. 이번 실행에서 들여쓴 레코드는 fastjson2 시간의 1.11배가 걸렸다. 8~16자리 정수는 두 워드로 읽으며, 이로써 18자리 id가 6.5 µs에서 5.9 µs로 줄었다. 코덱은 13자리와 18자리 id를 fastjson2 시간의 86%와 83%에 읽는다. 빈 문자열과 한 글자 문자열은 할당 없이 표에서 꺼내며, 이로써 그런 문자열을 가진 레코드가 3.4 µs와 13.9 KB에서 2.8 µs와 5.5 KB로 줄어 fastjson2 시간의 73%가 됐다. fastjson2는 긴 설명도 1.21배 빠르게 읽는다. fastjson2는 sun.misc.Unsafe를 거쳐 묶은 JDK 내부 메서드로 문자열의 끝을 찾고 String을 만들며, 공개 API에는 이에 맞먹는 것이 없다.

장소마다 소수 넷째 자리까지의 위도, 여섯째 자리까지의 경도, 첫째 자리까지의 평점이 있다. 코덱은 이를 fastjson2 시간의 96%에 같은 할당으로 읽고, Jackson 시간의 5분의 1에 읽는다. readDouble은 숫자를 그 자릿수에서 변환하며, 이런 숫자에는 곱셈이나 나눗셈 한 번이면 된다. 그 전에는 숫자마다 문자열을 만들어 Double.parseDouble에 넘겼고, 같은 읽기에 12.6 µs와 43.9 KB가 들었다. Jackson 3는 기본으로 빠른 파서로 double을 파싱하므로, USE_FAST_DOUBLE_PARSER를 켜도 달라지는 것이 없었다. 코덱은 장소를 fastjson2 시간의 98%에 그 절반의 할당, 곧 문서 자체보다 조금 많은 할당으로 쓰고, Jackson 시간의 절반이 안 되는 시간에 쓴다. JsonOutput은 double의 가장 짧은 십진수를 JDK 19부터 Double.toString이 쓰는 알고리즘으로 버퍼에 바로 늘어놓으며, 같은 텍스트를 쓴다. 그 전에는 숫자마다 Double.toString으로 문자열을 만들어 복사했고, 같은 쓰기에 16.5 µs와 35.6 KB가 들었다.

WebResponse.json이 본문을 보내듯 스레드의 버퍼에서 스트림으로 바로 쓰면, 목록은 3.8 µs와 184바이트가 든다. 이는 toBytes()가 문서를 복사해 넣는 배열보다 1.0 µs와 10.5 KB 적다. fastjson2의 JSON.writeTo도 그만큼 아끼므로, 이 절약은 라이브러리가 아니라 프레임워크의 몫이다. HTTP json-list 케이스에서는 같은 변경으로 요청당 할당이 Jetty에서 14.4 KB에서 3.8 KB로, Tomcat에서 13.2 KB에서 2.6 KB로 줄었으며, 30초 부하 동안의 GC 로그로 셌다.

fastjson2의 속도에는 표에 드러나지 않는 조건이 따른다. --sun-misc-unsafe-memory-access=deny에서 fastjson2 2.0.57은 초기화되지 않는다: JDKUtils.<clinit>이 arrayBaseOffset에서 실패하고, 이후의 모든 호출이 NoClassDefFoundError를 던진다. -Dfastjson2.creator=reflect도 같은 방식으로 실패하며, fastjson2에는 sun.misc.Unsafe 없이 도는 모드가 없다. JDK 24 이상에서는 첫 호출이 sun.misc.Unsafe의 메서드가 앞으로의 릴리스에서 제거된다는 경고를 찍는다. JEP 471과 JEP 498이 정한 순서는 경고, 기본값으로 거부, 제거다. JDK 26과 JDK 27의 기본값은 아직 경고이고, 거부를 기본값으로 삼거나 메서드를 제거하는 JEP는 아직 없다. VarHandle로 옮겨 가는 fastjson2의 2.1.x 브랜치는 2026년 2월 이후 변경이 없다. JVM에서 -Dfastjson2.creator=reflect를 주면 fastjson2는 실행 중에 클래스를 만들지 않으며, 목록을 평소의 네 배인 24.5 µs와 111.2 KB에 읽고 5.7 µs에 쓴다. GraalVM 네이티브 이미지에서 fastjson2는 String fast path와 생성 클래스를 끄고 리플렉션으로 바인딩하며, 이를 위해 바인딩하는 타입마다 reachability 메타데이터가 필요하다. fastjson2 2.0.57은 GraalVM CE 25.2.4에서 jar에 든 설정만으로는 네이티브 이미지로 빌드되지 않는다. 그 설정은 JSONFactory를 비롯한 몇 클래스를 빌드 시 초기화하고, 그 클래스들이 붙든 객체는 설정이 실행 시로 남겨 둔 클래스에 닿으며, native-image는 이를 거부한다. 패키지 전체를 빌드 시 초기화해 빌드한 이미지는 레코드를 잘못 읽는다: "available":false는 syntax error : 102로 실패하고 true는 읽힌다. fastjson2는 false를 sun.misc.Unsafe로 읽는데, 그 바이트 배열 오프셋은 JDKUtils가 이미지를 빌드한 JVM에서 계산한 값이다. 그 오프셋은 16이고, 이미지 자신의 값은 12다. JDKUtils는 fastjson2 자신의 설정이 빌드 시 초기화하는 클래스 가운데 하나이므로, 빌드되는 어떤 이미지에서도 오프셋이 틀린다. silk-json은 배열의 VarHandle 뷰로 읽으며, 이 뷰는 오프셋을 들고 있지 않다.

json-bench/native-bench.sh는 케이스 여섯 개를 GraalVM CE 25.2.4의 기본값으로 네이티브 이미지로 빌드하고, NativeBench가 JMH 없이 잰다: 3초 워밍업 뒤 1초 라운드 열 번의 중앙값이다. 같은 측정기를 Temurin 25에서 돌린 것이 JVM 열이다.

연산, 생성된 코덱 네이티브 이미지 JVM

레코드 100개 쓰기

12.2 µs

4.6 µs

한글 레코드 100개 쓰기

9.7 µs

4.5 µs

장소 100개 쓰기

12.9 µs

9.6 µs

레코드 100개 읽기

10.5 µs

5.6 µs

한글 레코드 100개 읽기

10.8 µs

6.5 µs

장소 100개 읽기

9.6 µs

5.1 µs

네이티브 이미지는 JVM 시간의 1.3배에서 2.6배가 걸리며, HTTP 케이스의 네이티브 이미지가 JVM보다 적은 요청에 응답하는 것과 같은 방향이다. -O3로 빌드한 이미지는 케이스마다 0%에서 8% 덜 걸렸고, -march=native로 빌드한 이미지는 6% 덜 걸리는 것부터 11% 더 걸리는 것까지 갈렸으므로, 어느 옵션도 JVM과의 차이를 닫지 못한다. fastjson2는 이미지가 문서를 잘못 읽었으므로 표에서 뺐다. HTTP 케이스에서 직렬화기와 파서는 각 요청의 한 몫이며, 결과 해석이 이를 처리량과 견준다.

결과 해석

text`와 `json 케이스는 본문이 아주 작으므로 프레임워크가 요청에 더하는 비용을 잰다. 같은 컨테이너인 Tomcat 위에서 Spider Silk는 Spring Boot보다 `text`에서 초당 31%, `json`에서 24% 많은 요청을 처리한다. 두 프레임워크는 요청마다 하는 일이 다르다. Spider Silk에서 서블릿부터 핸들러까지의 경로는 스택 프레임 두 개이며, Spring Web MVC는 요청마다 핸들러 매핑, 인자 리졸버, 반환값 핸들러, 메시지 컨버터를 거친다. Jetty는 여기에 8%~14%를 더한다. 또한 이 64개 연결에서 코어 네 개를 계속 바쁘게 쓴 서버는 Jetty뿐이며, 두 Tomcat 대상은 약 300%에 머물렀다.

json-list 케이스에서는 격차가 Jetty에서 1.72배, Tomcat에서 1.37배로 벌어지는데, 10 KB 문서는 프레임워크가 아니라 직렬화기의 일이기 때문이다. 생성된 코덱은 문서를 스레드의 버퍼에 쓰며, WebResponse.json은 그 버퍼에서 복사 없이 본문을 보낸다. 직렬화기와 파서만 따로는 같은 목록에서 코덱이 Jackson 시간의 40%를 쓴다고 잰다. 2026-10-09의 실행에서는 JsonWriter.list가 문서 전체를 트리로 만든 뒤에야 쓰기 시작했고, 격차는 2%~5%였다. 큰 문서를 보내는 애플리케이션에는 다른 두 경로가 있다: 대용량 JSON과 NDJSON의 스트리밍 라이터, 그리고 외부 JSON 바인딩을 통한 Jackson 같은 바인딩 라이브러리다. 둘 다 여기서는 측정하지 않았다.

json-post 케이스는 파서를 재며, 어느 컨테이너에서든 Spider Silk가 Spring Boot보다 초당 약 2배 많은 요청을 처리한다. 생성된 코덱은 트리도 리플렉션도 없이 본문의 바이트를 곧바로 레코드로 읽는다. 직렬화기와 파서만 따로는 같은 목록에서 코덱이 Jackson 시간의 24%를 쓴다고 잰다. 여기서는 파싱이 컨테이너보다 무거우므로 Jetty와 Tomcat이 서로 3% 이내로 차이 난다.

템플릿 케이스에서는 프레임워크보다 엔진의 비중이 크다. jte는 템플릿을 컴파일된 Java로 실행하며, 템플릿을 해석하고 표현식 언어로 모델을 읽는 Thymeleaf보다 이 페이지를 약 2.8배 빠르게 렌더링한다. 양쪽 모두 Thymeleaf를 쓰면 프레임워크의 몫이 충분히 작아서 Tomcat 위의 두 프레임워크가 2% 이내로 차이 난다. Jetty는 Spider Silk의 Thymeleaf 페이지에 13%를 더한다.

네이티브 이미지는 부팅 시간과 메모리를 얻는 대신 처리량을 내준다. 네이티브 이미지는 미리 컴파일된 코드를 G1 대신 Serial GC 위에서 실행한다. GraalVM CE에서는 할당이 많은 케이스가 가장 크게 손해를 본다: spring-boot-native는 같은 애플리케이션이 JVM에서 처리하는 양의 json-list와 json-post에서 모두 14%를 처리하며, p99는 33 ms와 29 ms다. Spider Silk의 GraalVM CE 이미지는 같은 케이스에서 JVM 처리량의 60%와 73%를 유지하며, 이는 직렬화기와 파서만 따로의 코덱 시간과 같은 방향이다. GraalVM CE에서 Thymeleaf는 Spider Silk의 네이티브 이미지가 Spring보다 느린 유일한 케이스다. 두 프레임워크는 Thymeleaf의 표현식을 다르게 평가한다: Spider Silk의 모듈은 Fortune을 리플렉션으로 읽는 OGNL로, Spring의 통합 모듈은 SpEL로 평가한다.

프로파일 기반 최적화는 두 애플리케이션 모두에서 GraalVM CE가 내준 처리량의 대부분을 되찾는다. JIT 컴파일러는 실행 중인 프로그램이 거친 분기와 타입을 보고 각 메서드를 컴파일하며, GraalVM CE 이미지는 그런 정보 없이 컴파일된다. PGO 이미지는 계측된 쌍둥이 이미지가 같은 케이스 아래에서 기록한 횟수를 보고 컴파일된다. Spider Silk의 PGO 이미지는 JVM 처리량의 79%~94%를, Spring Boot의 PGO 이미지는 27%~89%를 유지한다. 이득의 일부는 Oracle GraalVM 자체의 몫이다: 기록이 아니라 코드에서 추론한 프로파일을 쓰는 기본 빌드만으로도 GraalVM CE보다 최대 1.60배 많은 요청을 처리한다. 가장 큰 이득은 GraalVM CE가 가장 많이 잃은 곳에서 나온다: Spider Silk의 Thymeleaf, 그리고 Spring Boot의 json-list와 json-post이며, 각각 GraalVM CE 처리량의 2배에 가깝다. PGO에서는 두 Thymeleaf 페이지가 서로 1% 이내로 차이 난다. 프로파일은 이후에 측정한 바로 그 요청 아래에서 기록했으며, 이는 한계가 짚듯 가장 유리한 프로파일이다. Oracle GraalVM이 함께 제공하는 G1 컬렉터는 측정하지 않았다.

한계

  • 클라이언트와 서버가 한 노트북에서 루프백으로 통신하므로, 네트워크 지연도 네트워크 병목도 없다.

  • CPU의 두 절반은 성능이 다르다: 서버는 성능 코어에서, wrk는 효율 코어에서 실행됐다. wrk는 가장 빠른 케이스도 따라잡았으므로, 처리량을 정한 것은 wrk가 아니라 서버다.

  • wrk는 한 연결에서 이전 응답이 도착한 뒤에야 다음 요청을 보내므로, 백분위수에는 요청이 느린 요청 뒤에서 기다렸을 시간이 빠져 있다(coordinated omission). 이 백분위수는 대상끼리 비교하기 위한 것이며, 서비스 수준 목표와 비교하기 위한 것이 아니다.

  • CPU는 powersave 거버너와 performance 프로파일로 실행됐으므로, 클럭은 여전히 부하와 온도에 따라 오르내렸다. 번갈아 실행한 라운드가 그 편차를 모든 대상에 고르게 나눈다.

  • 핸들러가 I/O를 하지 않으므로, 이 수치는 프레임워크, 직렬화기, 템플릿 엔진만을 잰다. 데이터베이스를 기다리는 애플리케이션은 요청 시간의 대부분을 거기서 쓰며, 위의 차이는 그만큼 줄어든다.

  • PGO 이미지는 측정한 바로 그 케이스 아래에서 프로파일을 모았다. 애플리케이션의 프로파일은 프로파일 수집 부하가 닿은 코드만을 다루므로, 그 이득은 그 부하가 운영 트래픽과 얼마나 맞는지에 달려 있다.

  • Jetty 대상의 응답은 Tomcat 대상의 응답보다 26바이트 길며, 대부분은 Server: Jetty(12.1.13) 헤더다.

실행 재현

benchmark/ 디렉터리에 두 애플리케이션과 스크립트가 있으며, 그 README.md에 설정이 나열되어 있다. 이 디렉터리는 루트 빌드를 포함하는 독립된 Gradle 빌드이므로, Spider Silk 쪽은 이 디렉터리가 놓인 작업 트리의 모듈로 실행된다.

sudo apt install wrk # or: brew install wrk
sdk install java 25.2.4-graalce # for the native images
sdk install java 25.4.4+1-graal # for the Oracle GraalVM and PGO images
GRAALVM_HOME=~/.sdkman/candidates/java/25.2.4-graalce ORACLE_GRAALVM_HOME=~/.sdkman/candidates/java/25.4.4+1-graal benchmark/run.sh

실행은 benchmark/results/<timestamp>/에 원시 CSV 파일, wrk 출력, 각 서버의 로그, 케이스마다의 응답 표본, 머신 설명을 쓴다. benchmark/summarize.sh는 그 디렉터리를 위의 표로 줄인다. 마이크로벤치마크는 benchmark/ 디렉터리에서 따로 실행한다:

../gradlew -p json-bench installDist
taskset -c 0-3 json-bench/build/install/json-bench/bin/json-bench -jvmArgs "-Xms1g -Xmx1g" -prof gc

GRAALVM_HOME이 없으면 JVM 대상만 측정한다. ORACLE_GRAALVM_HOME이 없으면 Oracle GraalVM과 PGO 대상을 뺀다. 전체 실행은 이 머신에서 2시간 51분이 걸렸으며, Oracle GraalVM 대상 없이는 약 80분이 걸린다. ROUNDS=1 BUILD_ROUNDS=1 STARTS=3 WARMUP=5s DURATION=10s로는 Oracle GraalVM 대상 없이 약 15분 만에 대략적인 그림을 얻는다.