aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/007-buffer-retention-results.md
blob: 36fb68436b3192bd9f5a0e842a63470d8a0eacf4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Pure 연구 보고서 007: 버퍼를 공유할 때와 복사할 때

작성일: 2026-09-22
상태: **비교 CLI 구현·정확성 검사·Linux AArch64 측정 완료. Pure의 성능 우월성은 확인하지 못함.**

## 이번에 알아낸 것

첫 후보의 문제는 재현됐다. 작은 결과 때문에 큰 원본 버퍼가 오래 유지되면 메모리가 커진다.
하지만 이번 범위에서는 **일반적인 크기 기준 복사와 필드 선별 읽기만으로 큰 개선이 가능했다.**
현재 메모리 압력에 따라 복사를 결정하는 후보 정책은 그 단순한 방법보다 뚜렷하게 낫지 않았다.
이 결과를 근거로 새로운 런타임·ABI·컴파일러를 확대할 이유는 아직 없다.

같은 64개 큐 한도에서 64 KiB 레코드의 64바이트만 남기는 작업을 비교했다.
공유 방식의 최대 관리 메모리는 약 **4.070 MiB**, 일반 복사 방식은 **0.137 MiB**였다.
이는 실험 코드가 요청한 저장 공간의 약 30배 차이다. 전체 앱 RAM의 30배 개선이 아니다.
해당 조건의 Linux RSS 근삿값은 각각 약 5.05 MiB와 1.56 MiB였다.

사용 경험으로 이어질 수 있는 차이는 큰 데이터를 처리할 때 적은 메모리로 작업을 지속하는 것이다.
다만 메모리가 줄면 큐가 더 길어져 응답이 늦어질 수도 있었다. 버퍼 용량과 지연을 함께 제한해야 한다.
이번 근거는 합성 입력을 사용한 실제 네이티브 실행 측정이다. 실제 서비스·Mac·Pure 제작 경험의 측정은 아니다.

사용자는 이번에 새 데모를 평가할 필요가 없다. 실행 가능한 산출물은
[비교 CLI와 재현 방법](../../research/experiments/002-buffer-retention/README.md)이며,
현재 제작 화면의 문법과 동작은 바꾸지 않았다.

## 구현한 비교

같은 C17 프로그램 안에서 입력 처리·큐·출력·메모리 예산·계측을 공유하고 정책을 바꾼다.
Pure VM을 호출하지 않으며 새로운 메모리 관리 원리를 구현했다고 주장하지 않는다.

| 방식 | 처리 |
|---|---|
| `borrow` | 선택한 부분이 필요할 동안 원본 전체를 보관 |
| `copy` | 선택한 부분을 별도 버퍼에 복사한 뒤 원본 반납 |
| `threshold2/4/8` | 원본/선택 버퍼의 할당 용량 비율이 2·4·8 이상이면 복사 |
| `pressure` | 현재 활성 버퍼가 가용 예산의 절반을 넘으면 더 작은 결과를 복사 |
| `project` | 입력에서 필요한 부분만 결과 버퍼로 읽고 나머지는 작은 임시 공간으로 읽어서 버림 |
| `copy_no_pool` | 일반 복사와 같은 정책·용량 구분을 쓰되 버퍼 재사용 캐시를 끔 |

모든 방식에 전체 예산과 큐 개수 제한을 적용했다. 캐시의 빈 버퍼도 예산에 넣고 필요하면 반납한다.
복사 중 원본과 결과가 동시에 존재하는 공간도 포함했다. 원본 크기만큼 필요한 공간이 없으면
대기 중 출력을 처리하고, 그것으로도 불가능하면 명시적으로 실패한다.

입력은 길이·선택 범위가 헤더에 담긴 실험용 바이너리 레코드다. 모든 입력 바이트를 실제로 읽는다.
JSON 파서나 압축 라이브러리를 비교한 것이 아니다. `project`의 범위는 이처럼 필요한 위치를
알 수 있는 입력이다. 범용 문서에서 같은 효과를 얻으려면 별도 검증이 필요하다.

## 실행 조건과 검증

- 호스트: Raspberry Pi 계열 Linux AArch64, Cortex-A76, GCC 14.2.0. 사용자 Apple Silicon에서 실행하지 않았다.
- 빌드: C17, `-O2`, 경고를 오류로 처리, 진단용 assert·계측 활성화. 최적화 빌드와 ASan/UBSan 빌드를 따로 검사했다.
- 입력: 조건별 2,048개 레코드, 약 128~169 MiB. seed 20260922, 크기·선택 범위·바이트 생성 규칙과 해시 보관.
- 일반 조건: 관리 공간 8 MiB, 큐 최대 512개. 좁은 예산 조건은 2 MiB. 별도 민감도 검사는 큐를 64개로 제한했다.
- 느린 소비자: 각 결과는 enqueue 뒤 최소 5 ms 보관, 연속 출력 완료 사이 최소 20 μs 간격.
  실제 sleep과 파일 I/O를 수행하지만 이 지연은 합성 조건이다. OS 기상 지연 때문에 정확히 20 μs마다 출력되는 것은 아니다.
- 6개 조건 × 8개 방식 × 5회 = **240개 측정**. 방식별 워밍업 후 각 반복에서 순서를 섞었고 모든 출력 해시가 일치했다.
- 일반 빌드와 ASan/UBSan 합계 **88개 검사 그룹** 통과. 바이트 단위 출력 비교, 잘린 입력,
  잘못된 범위, 예산 부족, 완료 출력의 순서, 취소, SIGTERM, 실제 출력 실패(`/dev/full`), 버퍼 회수를 검사했다.

입력 생성·빌드·결과 검사는 시간 측정 밖이다. 측정에는 읽기·쓰기·복사·대기·계측·버퍼 정리가 포함된다.
파일 캐시를 사용하는 실행이며 출력에 `fsync`를 하지 않는다. 한 CPU로 고정했으나 온도·전력·다른 작업은 통제하지 않았다.

## 결과 1: 같은 큐 한도에서도 기존 복사 정책이 효과적

64 KiB 레코드에서 64바이트를 선택하고, 모든 방식의 큐를 64개로 제한했다.
아래는 5회 중앙값이다. 메모리는 고정 관리 공간·임시 공간·버퍼·빈 캐시를 포함한 요청 바이트다.
시간은 전체 2,048개 처리가 끝날 때까지의 값이며 합성 대기가 포함된다.

| 방식 | 최대 관리 공간 MiB | RSS 근사 MiB | 완료 시간 ms | 프로세스 내부 P99 상한 ms |
|---|---:|---:|---:|---:|
| 원본 공유 | 4.070 | 5.05 | 167.22 | 7.34 |
| 항상 복사 | 0.137 | 1.56 | 168.36 | 7.86 |
| 크기 비율 2 기준 | 0.137 | 1.56 | 167.15 | 7.86 |
| 메모리 압력 기준 | 4.070 | 5.50 | 167.12 | 7.34 |
| 필요한 부분만 읽기 | 0.074 | 1.52 | 167.51 | 7.86 |

공유의 완료 시간 범위는 167.16~169.33 ms, 복사는 166.68~177.07 ms였다.
이 표로 작은 시간 차이의 우열이나 동일한 서비스 SLO 충족을 주장하지 않는다.
여기서 분명한 관찰은 **작은 결과를 독립시키면 유지해야 하는 원본 공간이 줄어든다**는 것이다.

항상 복사해도 전체 명시적 복사는 128 KiB였다. 원본 입력 약 128 MiB 전체를 복사한 것이 아니다.
형식이 허용한 선별 읽기는 그 복사도 없앨 수 있었다. 다만 read/write 등 다른 데이터 이동은 여전히 있다.

크기 관계로도 설명할 수 있다. 대략 Q개 결과가 남아 있을 때 원본 공유는 `Q × 원본 용량`,
복사는 `Q × 선택 용량 + 처리 중인 원본 용량`을 요구한다. 실제 측정에는 관리 공간·캐시·할당 반올림이 더해진다.
이 비용 관계를 Pure의 새 발견으로 취급하지 않는다.

## 결과 2: 메모리만 줄이면 큐 대기는 커질 수 있다

큐 한도를 512개로 둔 같은 작은 선택 조건에서, 공유는 예산 때문에 최대 126개를 보관했고
복사는 512개까지 보관했다. 최대 관리 공간은 공유 7.963 MiB에서 복사 0.188 MiB로 줄었지만,
프로세스 내부 P99 상한 중앙값은 약 9.44 ms에서 41.94 ms로 커졌다.

그만큼 작업이 큐 안에서 기다렸다. 입력을 아직 읽지 않아 밖에서 대기한 시간은 이 지연에 포함되지 않는다.
따라서 공유 방식의 서비스 전체 지연이 더 짧다고 해석할 수도 없다.
**메모리 예산·큐 길이·외부 도착률을 함께 정해야 지연에 관한 비교가 가능하다.**

이 관찰 후 추가한 것이 앞의 64개 큐 검사다. 사후에 추가한 탐색 실험임을
[v2 계획](../../research/experiments/002-buffer-retention/plan.json)에 명시했다.

## 결과 3: 복사를 많이 하면 항상 좋은 것도 아니다

64 KiB 중 48 KiB를 남기는 조건에서는 이번 할당기의 용량 반올림 때문에 선택 버퍼도 64 KiB가 된다.
공유와 복사 모두 최대 관리 공간이 약 7.963 MiB였고, 항상 복사 방식은 추가로 96 MiB를 복사했다.
크기 기준 정책은 이 불필요한 복사를 하지 않았다.

즉, 원본과 선택 데이터의 관계뿐 아니라 **실제 할당 용량**을 고려해야 한다.
다른 용량 구분·정확한 크기 할당기에서는 결과가 달라질 수 있다. 이번 작은 풀을 최선의 할당기라고 주장하지 않는다.

메모리 부족이 없는 빠른 소비자 조건에서는 대부분의 방식이 약 0.149 MiB만 관리했고,
전체 완료 시간 중앙값도 약 20~23 ms 범위였다. 느린 소비자에서 관찰한 큰 유지량 차이를
모든 실행에 일반화할 수 없다.

## 결과 4: 좁은 예산에서는 원본 유지가 진행 자체를 늦출 수 있다

4~256 KiB 원본과 다양한 선택 비율이 섞인 입력을 2 MiB 예산으로 처리했다.
모든 방식이 거의 같은 최대 관리 공간까지 사용한다. 이 경우에는 완료 시간과 복사량이 달라졌다.

| 방식 | 완료 시간 중앙값 ms | 측정 범위 ms | 명시적 복사 MiB |
|---|---:|---:|---:|
| 원본 공유 | 493.63 | 491.78~495.15 | 0 |
| 항상 복사 | 191.25 | 191.04~191.77 | 51.24 |
| 크기 비율 2 기준 | 188.82 | 188.31~201.97 | 27.16 |
| 크기 비율 4 기준 | 225.45 | 224.73~229.05 | 10.71 |
| 크기 비율 8 기준 | 283.68 | 282.26~289.85 | 3.65 |
| 메모리 압력 기준 | 190.69 | 189.79~193.71 | 27.07 |
| 필요한 부분만 읽기 | 175.31 | 174.67~179.28 | 0 |

명시적 복사 바이트가 적다고 완료가 빠르지는 않았다. 원본을 오래 보관하면 다음 입력을 받아들일
공간이 부족해지고, 합성 보관 시간을 기다리는 동안 작업 진행이 제한된다.
압력 기준 정책은 단순한 크기 비율 2 정책과 비슷한 결과였다. 후보 정책의 고유한 이점은 확인하지 못했다.

이 결과는 해당 로컬 파이프라인의 처리 시간이다. 약 2.6배의 시간 차이를
서버·파서·Pure 전체의 성능 개선으로 표기하지 않는다.

버퍼 재사용의 효과도 별도로 확인했다. 64개 큐의 작은 선택 조건에서 복사 방식의 실제 할당 호출은
캐시를 끄면 4,096회, 켜면 65회였지만 완료 시간의 뚜렷한 개선은 확인하지 못했다.
할당 횟수 감소와 사용자 체감 성능 개선은 서로 다른 주장이다.

## 계측 중 발견하고 수정한 문제

첫 실행에서 모든 RSS가 약 27.42 MiB로 동일했다. 이를 결과로 확정하지 않고 조사했다.
Linux `getrusage`의 자원 통계는 exec를 지나서도 보존되므로, 이 실행 경로의 값에는
프로그램 실행 전 이력이 섞여 있었다.
[getrusage 문서](https://man7.org/linux/man-pages/man2/getrusage.2.html).

현재 결과는 Linux `/proc/self/status`의 현재 메모리 이미지 `VmHWM`을 별도로 읽고,
원래 `rusage`도 구분하여 남긴다. Linux 문서는 이 RSS 계측도 근삿값이라고 설명한다.
정확한 요청 바이트 계수와 동일한 지표가 아니다.
[proc status 문서](https://man7.org/linux/man-pages/man5/proc_pid_status.5.html).

[첫 실행 기록](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64/audit.json)과
당시 소스는 보존했다. 본문의 수치는 모두 수정된 v2 실행에서 가져왔다.
수정 후 큰 원본 유지와 작은 선택 결과가 RSS에서도 구분되는지 회귀 검사를 추가했다.

## 이 실험이 아직 다루지 않은 것

- 하나의 원본을 여러 소비자가 공유하는 분기, 영속 캐시, 독립적인 여러 처리 단계의 예산 조정.
- Sonic·Node.js·.NET·Arrow 자체의 현재 구현과 직접 비교.
- 일반 JSON·문서·압축 파서와 외부 라이브러리 내부 메모리.
- 독립적인 요청 도착, 서비스 전체 지연, 네트워크·GPU·비동기 취소 경합.
- 온도·전력 통제, 별도 계측 오버헤드 보정, Mac 실행, 통계적 유의성 검정.
- 사용자에게 필요한 실제 작업의 메모리·지연 목표와 새 도구의 개발 경험.

따라서 006의 여러 단계에 걸친 가설 전체를 기각한 것은 아니다.
**단일 입력·단일 소비자에서의 공유/복사 선택만으로는 Pure의 필요성을 입증하지 못했다**는 결론이다.

## 다음 설계 결정

1. 무복사나 전용 할당기 자체를 대표 차별점으로 삼지 않는다. 기존 크기 기준 정책을 필수 비교 대상으로 유지한다.
2. [버퍼 수명 초안](../design/002-memory-lifetime.md)에는 필요한 계측 질문이 생겼다.
   원본 할당과 선택 범위, 마지막 소비자, 빈 풀의 유지량, 큐 밖의 대기까지 구분해야 한다.
   이 실험만으로 현재 계약의 비동기 보장이 구현·검증된 것은 아니다.
3. 다음 후보 검증은 실제 파서와 여러 소비자가 같은 원본을 공유하는 작업으로 한정한다.
   그 작업의 기존 구현에서도 남는 비용이 확인될 때 전역 수명·예산 정책과 선행연구 비교를 진행한다.
   같은 해법으로 충분하면 이 주제의 차별화 우선순위를 낮추고 증분 편집 후보로 이동한다.

## 재현·원자료

- [비교 코드](../../research/experiments/002-buffer-retention/benchmark.c), [실행·검증 도구](../../research/experiments/002-buffer-retention/run.py)
- [실행 환경·빌드·소스 해시](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/environment.json)
- [입력·출력 해시](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/fixtures.json)
- [검증 기록](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/verification.json)
- [240개 원자료](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/raw.csv), [중앙값·범위](../../research/experiments/002-buffer-retention/results/2026-09-22-linux-aarch64-v2/summary.json)

```sh
python3 research/experiments/002-buffer-retention/run.py --check-only
python3 research/experiments/002-buffer-retention/run.py --output /tmp/pure-retention-results
```

출력 경로는 새 디렉터리여야 한다. 측정 도구는 기존 결과를 덮어쓰지 않는다.

2026-09-22 후속: [008](008-json-fanout-results.md)에서 실제 JSON 파서와 최대 세 소비자로
확장했다. 그 범위에서도 기존 복사·복사본 공유 정책이 효과적이었으며, 대표 연구 문제로서의
우선순위를 낮추고 증분 편집·상태 갱신 후보로 이동하기로 했다.