aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research/008-json-fanout-results.md
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-24 17:14:54 +0100
committereunhhu <[email protected]>2026-09-24 17:14:54 +0100
commitb1ce1927022d374198441afafeb27199fc8a052a (patch)
tree6021f175f4c67ed03b44240eaa2f90d846f12f6f /docs/research/008-json-fanout-results.md
parentd34f9426d5317a1948f12f229439946728bc342a (diff)
downloadpure-b1ce1927022d374198441afafeb27199fc8a052a.tar.gz
pure-b1ce1927022d374198441afafeb27199fc8a052a.zip
Establish native micro runtime checkpoint
Diffstat (limited to 'docs/research/008-json-fanout-results.md')
-rw-r--r--docs/research/008-json-fanout-results.md202
1 files changed, 202 insertions, 0 deletions
diff --git a/docs/research/008-json-fanout-results.md b/docs/research/008-json-fanout-results.md
new file mode 100644
index 0000000..5fcc5d0
--- /dev/null
+++ b/docs/research/008-json-fanout-results.md
@@ -0,0 +1,202 @@
+# Pure 연구 보고서 008: 실제 JSON 파서와 여러 소비자의 버퍼 수명
+
+작성일: 2026-09-22
+상태: **실제 파서를 연결한 비교 구현·검증·210회 측정 완료. Pure 고유의 우위는 확인하지 못함.**
+
+## 이번 판단
+
+실제 JSON과 여러 소비자를 붙여도 원본 유지 비용은 재현됐다.
+그리고 이번 범위에서도 **작은 결과를 일찍 복사하고 같은 결과의 복사본을 공유하는 기존 방법이 효과적이었다.**
+소비자가 하나만 남았을 때 뒤늦게 복사하는 후보는 원본의 유지 시간을 줄였지만,
+최대 메모리를 충분히 줄이지 못하거나 단순 크기 기준보다 진행이 느렸다.
+
+이제 ‘공유 버퍼를 어떻게 복사할 것인가’만으로 새로운 언어·실행기·ABI의 필요성을 설명하지 않는다.
+수명·예산 관리는 Pure의 기본 품질 요건으로 남기고, 연구의 대표 문제로서 우선순위를 낮춘다.
+다음 우선 후보는 006에서 정한 **작은 변경이 큰 재계산을 일으키는 편집·상태 갱신 문제**다.
+
+사용 경험에 연결되는 관찰은 분명하다. 작은 메타데이터를 느린 소비자에게 넘겼다는 이유로
+이미 처리한 큰 본문까지 붙잡을 필요는 없다. 다만 이것을 사용자 앱이나 Pure 문법으로 구현한 것은 아니다.
+이번 산출물은 [실행 가능한 비교 CLI](../../research/experiments/003-json-fanout/README.md)이며,
+사용자가 새 데모를 조작해 평가할 과제는 없다.
+
+## 이전 실험에서 보완한 것
+
+[007](007-buffer-retention-results.md)은 미리 알려진 바이트 범위와 단일 소비자만 다뤘다.
+이번에는 다음을 추가했다.
+
+- **실제 JSON 파싱:** 버전을 고정한 yyjson 0.12.0으로 줄마다 JSON 전체를 파싱한다.
+ 선택하지 않는 필드도 구문 검사를 거친다. Python의 별도 JSON 디코더와 실제 출력 바이트를 비교한다.
+- **최대 세 소비자:** 서로 다른 필드 또는 같은 필드를 소비하고, 각각 다른 시점에 완료한다.
+ 소비자별 순서는 유지하지만 소비자 사이의 출력 순서는 달라질 수 있다.
+- **공유 소유권:** 같은 원본을 여러 번 세지 않는다. 마지막 참조가 반납되어야 해당 할당을 반환한다.
+- **파서 내부 메모리:** yyjson의 할당·재할당·해제 콜백을 같은 예산에 연결했다.
+ 입력·결과만 작게 보이게 하고 파서 작업 공간을 제외하는 비교를 피했다.
+
+yyjson은 입력을 직접 사용해 문자열을 디코딩하는 in-situ 옵션을 제공한다.
+필요한 문자열의 위치와 길이를 얻은 뒤 파서 문서 메타데이터는 즉시 해제하고,
+문자열이 있는 입력 버퍼의 소유권만 유지했다.
+실험에 사용한 [고정 버전 소스](https://github.com/ibireme/yyjson/tree/8b4a38dc994a110abaec8a400615567bd996105f)와
+MIT 라이선스·해시는 저장소에 함께 보관했다.
+
+## 비교 조건
+
+모든 방식이 같은 파서, 읽기 경로, 소비자 스케줄러, 예산 검사와 출력 형식을 사용한다.
+각 소비자의 큐는 최대 64개다. 기본 지연은 첫 소비자 0 ms, 둘째 5 ms, 셋째 8 ms다.
+이는 실제 sleep으로 넣은 **합성 보관 시간**이며 네트워크나 서비스의 관측 지연이 아니다.
+
+| 방식 | 결정 |
+|---|---|
+| `borrow` | 모든 소비자가 원본을 참조 |
+| `copy` | 소비자마다 필요한 문자열을 별도로 복사 |
+| `shared_copy` | 같은 원본 범위를 선택한 소비자끼리 복사본 하나를 공유 |
+| `threshold2/8` | 원본/결과의 할당 용량 비율이 2·8 이상이면 복사하고 같은 복사본을 공유 |
+| `compact` | 소비자 완료 후 원본 참조가 하나만 남으면 작은 결과를 복사 시도 |
+| `copy_no_pool` | 독립 복사와 같은 용량 구분을 쓰되 빈 버퍼 캐시는 끔 |
+
+`compact`는 현재 참조 상태만 보는 단순 후보다. 미래의 보관 시간을 미리 알지 않는다.
+다른 소비자가 여전히 원본을 참조할 때 그 버퍼를 옮기지 않는다.
+단일 소비자 조건에서는 완료 전 참조 수 변화가 없으므로 이 후보의 복사 시도도 없다.
+이 특정 후보가 모든 동적 정책을 대표하지는 않는다.
+
+6개 조건 × 7개 방식 × 5회로 **210개 측정**을 수행했다.
+기본 입력은 본문 32 KiB·패딩 8 KiB와 작은 문자열·메타데이터를 포함한 JSON 1,024개, 약 40.13 MiB다.
+혼합 입력은 약 47.57 MiB, 메타데이터가 많은 입력은 512개·약 5.02 MiB다.
+모두 생성 규칙과 seed를 공개한 합성 데이터다. 실제 운영 트래픽은 아니다.
+
+호스트는 Linux AArch64, Cortex-A76, GCC 14.2.0이다. 한 CPU로 고정했으나 온도·전력은 통제하지 않았다.
+워밍업 뒤 방식 순서를 반복마다 섞었다. 아래 값은 5회 중앙값이며 통계적 유의성 검정은 하지 않았다.
+사용자 Mac에서의 측정도 아니다.
+
+## 결과 1: 느린 작은 소비자가 큰 원본을 붙잡는다
+
+첫 소비자가 본문을 바로 출력하고, 두 느린 소비자가 작은 `tag` 문자열을 보관하는 조건이다.
+‘관리 공간’은 고정 상태·큐·입력·결과·파서·빈 버퍼 캐시를 포함한 최대 요청 바이트다. 전체 RSS와 다르다.
+
+| 방식 | 최대 관리 공간 MiB | 명시적 선택 복사 MiB | 전체 완료 시간 ms |
+|---|---:|---:|---:|
+| 원본 공유 | 4.230 | 0 | 132.60 |
+| 소비자마다 복사 | 0.304 | 32.109 | 132.28 |
+| 복사본 공유 | 0.297 | 32.055 | 132.28 |
+| 크기 비율 2 기준 | 0.297 | 32.055 | 132.24 |
+| 크기 비율 8 기준 | 0.297 | 0.055 | 132.18 |
+| 마지막 참조에서 복사 | 4.231 | 0.055 | 132.70 |
+
+크기 비율 8 정책은 빠른 본문 소비자에게 원본을 잠깐 빌려주고 작은 문자열만 독립시켰다.
+이 조건에서는 약 **4.23 → 0.297 MiB**로 관리 공간이 줄면서 명시적 복사도 작았다.
+이것은 일반 C 코드의 정책 변경으로 얻은 결과다. Pure를 호출하거나 컴파일하지 않았다.
+
+원본 공유와 마지막 참조에서의 복사는 최대 공간이 거의 같았다.
+둘째 소비자가 완료될 때까지는 원본을 반납할 수 없기 때문이다.
+원본 활성 바이트·시간의 중앙값은 약 540,593에서 340,108 byte·seconds로 줄었지만,
+이미 형성된 최대 유지량은 해결하지 못했다.
+
+모든 방식에서 최대 큐 개수는 `[1, 64, 64]`였다. 느린 두 소비자의 내부 P99 상한도 각각
+약 5.24 ms, 8.39 ms였다. 이 수치는 입력이 프로그램 밖에서 대기한 시간을 포함하지 않는다.
+완료 시간은 합성 지연의 영향을 크게 받으므로 1 ms 미만의 차이를 서비스 속도 개선으로 해석하지 않는다.
+
+## 결과 2: 소비자마다 복사하면 같은 결과도 중복된다
+
+세 소비자가 모두 같은 32 KiB 본문을 선택하도록 바꿨다.
+
+| 방식 | 최대 관리 공간 MiB | 명시적 선택 복사 MiB | 완료 시간 ms |
+|---|---:|---:|---:|
+| 원본 공유 | 4.230 | 0 | 133.81 |
+| 소비자마다 복사 | 4.296 | 96 | 134.44 |
+| 복사본 공유 | 2.262 | 32 | 133.59 |
+| 크기 비율 2 기준 | 2.262 | 32 | 133.66 |
+| 마지막 참조에서 복사 | 4.512 | 32 | 134.20 |
+
+소비자별 독립 복사는 원본을 해제하면서도 결과를 중복 보관했다.
+**한 번 만든 복사본을 공유하는 기존 기법**으로 복사량과 관리 공간을 줄일 수 있었다.
+같은 내용의 모든 문자열을 검색·통합한 것은 아니다. 한 레코드에서 같은 위치와 길이를 선택한 경우만 공유했다.
+
+늦은 복사는 복사 중 원본과 결과의 공존, 빈 버퍼 캐시 때문에 최대 공간이 더 커질 수도 있었다.
+‘마지막 순간에 복사하면 항상 메모리가 작다’는 결론은 성립하지 않았다.
+
+## 결과 3: 좁은 예산에서는 단순한 정책이 작업 정체도 줄인다
+
+본문·패딩 크기가 섞인 JSON을 2 MiB 예산으로 처리했다.
+빠른 소비자는 본문을, 느린 소비자들은 작은 문자열을 선택한다.
+
+| 방식 | 최대 관리 공간 MiB | 완료 시간 중앙값 ms | 측정 범위 ms |
+|---|---:|---:|---:|
+| 원본 공유 | 2.000 | 516.88 | 516.42~519.74 |
+| 복사본 공유 | 1.047 | 134.70 | 134.66~134.73 |
+| 크기 비율 2 기준 | 1.047 | 134.65 | 134.60~134.81 |
+| 크기 비율 8 기준 | 1.047 | 134.63 | 134.51~135.14 |
+| 마지막 참조에서 복사 | 2.000 | 330.51 | 330.33~332.81 |
+
+큰 원본이 남아 있으면 다음 입력과 파서가 사용할 공간이 부족해진다.
+늦은 복사도 정체를 완화했지만, 이 작업에서는 작은 결과를 일찍 분리하는 방법이 더 효과적이었다.
+이 수치는 특정 파이프라인의 처리 시간이며 서버나 Pure 전체가 몇 배 빨라진다는 뜻은 아니다.
+
+큰 본문을 **느린** 소비자에게 보내는 별도 조건에서는 크기 비율 8 정책의 최대 관리 공간이
+4.237 MiB, 비율 2 정책은 2.268 MiB였다. 복사량을 적게 만드는 기준이 메모리까지 항상 최소화하지 않는다.
+소비자의 역할과 실제 할당 크기를 같이 봐야 한다.
+
+## 파서와 풀을 빼면 결과가 왜곡된다
+
+메타데이터가 많은 입력에서는 크기 기준 정책의 최대 관리 공간이 약 0.172 MiB였다.
+그 실행의 파서 활성 할당 최고치는 약 0.094 MiB였다.
+혼합 입력에서는 파서 최고치가 약 0.500 MiB였다. 입력·출력만 계측해서는 이 공간을 설명할 수 없다.
+각 영역의 최고치는 발생 시점이 다를 수 있으므로 서로 더해서 총 최고치라고 하지 않는다.
+
+파서 콜백이 메모리 부족 시 기존 소비자를 진행시키도록 했으므로,
+`parse_call_ns`에는 그 진행과 대기 시간이 섞일 수 있다. 순수 파서 CPU 비용으로 사용하지 않는다.
+전체 처리 시간·프로세스 CPU·영역별 메모리를 별도로 기록했다.
+
+또한 빈 버퍼를 보관하는 풀은 할당을 재사용하는 대신 공간을 유지한다.
+세 소비자의 다른 속도 조건에서 독립 복사는 풀을 켜면 약 0.304 MiB,
+끄면 약 0.241 MiB였다. 풀의 존재만으로 메모리 최적화를 주장하지 않는다.
+이번 작은 풀을 각 플랫폼의 최선의 할당기와 비교한 것도 아니다.
+
+## 검증과 재현 가능한 범위
+
+일반 빌드와 ASan·UBSan 빌드에서 총 **108개 검사 그룹**이 통과했다.
+파서 자체도 계측해서 빌드했다. 검사는 다음을 포함한다.
+
+- 1·2·3 소비자의 독립 출력, 큐 슬롯 재사용, Unicode·이스케이프·NUL·빈 문자열.
+- 파서 문서 해제 뒤에도 남아 있는 입력/복사본의 유효성, 공유 결과의 마지막 반납.
+- 잘린 JSON, 잘못된 필드 타입·누락, 잘못된 UTF-8, 선택하지 않은 필드의 구문 오류.
+- 파서 작업 공간이 예산을 넘는 경우의 실패와 반납, 너무 긴 레코드 거절.
+- 취소·SIGTERM·실제 출력 실패와 남은 소유권 정리.
+
+210개 측정 모두 Python JSON 디코더로 만든 소비자별 기대 해시·개수와 일치했고,
+모든 측정에서 관리 예산을 지켰으며 종료 후 남은 버퍼 할당은 0이었다.
+이 검사가 yyjson의 모든 동작이나 다중 스레드·장치 취소 경합을 검증한 것은 아니다.
+
+## 남은 한계와 다음 결정
+
+이 실험은 실제 파서를 쓰지만 입력·소비자 지연은 합성이고 실행은 한 스레드다.
+JSON 한 줄을 모두 읽은 뒤 처리하며 지속 캐시·네트워크·독립적인 요청 도착·GPU를 다루지 않는다.
+Sonic·Node.js·.NET 자체의 비교도 아니다. 운영 부하에서 자동 정책이 유리할 가능성까지 기각하지 않는다.
+
+현재 증거로 결정할 수 있는 범위는 다음과 같다.
+
+1. 버퍼 수명·역압력·정확한 예산 계측은 **필요한 기반 기능**으로 유지한다.
+ 자체 ABI나 커널 우회가 이 개선에 필요했다는 주장은 하지 않는다.
+2. 공유/복사 정책만으로 Pure의 차별화를 설명하는 이번 비교 연구는 여기서 종료한다.
+ 남은 특수 조건을 계속 추가해서 후보 정책이 이기는 사례만 찾지 않는다.
+ 구체적인 새 작업이나 요구 조건이 생기면 보존한 증거부터 검토한다.
+3. 다음 조사 후보는 006의 P07/P08, 즉 **큰 문서·상태에서 작은 변경에 필요한
+ 계산과 갱신 범위를 줄이는 문제**다. 기존 증분 구현의 잔여 비용을 먼저 확인하고,
+ Pure가 바꿀 수 있는 경로가 있을 때만 기준 구현을 만든다. 전체 재계산 대비
+ 증분 처리의 알려진 이점을 확인하는 것만으로 새 실험을 정당화하지 않는다.
+ 이 후보에서도 증분 정보를 유지하는 메모리 비용을 포함해야 한다.
+
+## 코드와 원자료
+
+- [실험 설명·실행 방법](../../research/experiments/003-json-fanout/README.md)
+- [비교 구현](../../research/experiments/003-json-fanout/benchmark.c), [실행·검증 도구](../../research/experiments/003-json-fanout/run.py)
+- [실험 계획](../../research/experiments/003-json-fanout/plan.json)
+- [환경·버전·빌드·소스 해시](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/environment.json)
+- [입력과 소비자별 출력 해시](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/fixtures.json)
+- [검증 기록](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/verification.json)
+- [210개 원자료](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/raw.csv), [중앙값·범위](../../research/experiments/003-json-fanout/results/2026-09-22-linux-aarch64/summary.json)
+
+```sh
+python3 research/experiments/003-json-fanout/run.py --check-only
+python3 research/experiments/003-json-fanout/run.py --output /tmp/pure-json-fanout-results
+```
+
+새 결과 디렉터리만 허용한다. 의존성은 고정 버전으로 포함되어 있어 재현 시 다운로드가 필요 없다.