aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research')
-rw-r--r--docs/research/140-spec-suite.md63
1 files changed, 63 insertions, 0 deletions
diff --git a/docs/research/140-spec-suite.md b/docs/research/140-spec-suite.md
index 9cbe5c2..da1f562 100644
--- a/docs/research/140-spec-suite.md
+++ b/docs/research/140-spec-suite.md
@@ -959,6 +959,69 @@ min과 max가 남는다. Flexbox의 어려운 반은 크기가 아래에서 위�
- aup에 두는 것: 축을 필드 번호로 고르기(배치의 마찰 3), `number`나 `send` 같은 이름. 이름은 타입과 말의 자리를 지키려고 그대로 둔다.
- 원본: `2026-10-19-frictions.jsonl`(검사, 전과 뒤의 명령과 시간, 줄). aup의 설계 002 "140의 마찰을 걷어 냄".
+## LIST_VALUE의 복사를 다시 봄 (2026-10-19)
+
+리스트 값(let의 리스트를 fn에 넘기거나 레코드에 담기)은 항목을 복사한다. 여러 명세에서 그 값이 보였다. 새 명령 없이 줄일 수 있는지
+설계부터 보고, 실익이 분명한 것만 만들었다.
+
+**잰 것.** 계측한 빌드(연구용, 커밋하지 않음)로 명세마다 LIST_VALUE의 시간 몫을 쟀다(시계의 값은 대략 뺐다).
+
+| 명세 | 한 번 돌 때의 LIST_VALUE | 원본의 꼴 | 시간 몫 |
+|---|---|---|---|
+| UAX #14 | 168,048번(줄마다 9번, 평균 3.3항목) | FLAT 배열 | 약 10% |
+| JSON 트리 | 4,001번 | 트리(리스트를 담은 레코드) | 약 6% |
+| HTTP/1.1 | 596번 | 트리(바이트를 담은 레코드) | 약 2% |
+| inflate | 9번 | FLAT 배열 | 0.06% |
+| DNS, JSON 검사기, 상자 배치 | 0 | — | 0 |
+
+- **원인.** `nest()`가 원본의 꼴과 상관없이 새 리스트를 트리로, 항목을 하나씩 넣어 지었다. 항목당 약 64 ns였고, 한 번에 약 250 ns의
+ 고정 비용(새 리스트의 할당과 초기화)이 더 들었다. 수 8바이트를 베끼는 값이 아니라 트리에 넣는 값이다.
+
+**검토한 셋.**
+1. **빌려주고 뗀다(CoW).** 리스트 값이 원본의 리스트를 그대로 가리키고, 원본이 바뀌기 전에 떼어 낸다. 복사가 없어질 수 있는 유일한
+ 길이다. 다만 비용이 넓다.
+ - 실행기에서 이벤트 리스트를 바꾸는 모든 자리에 갈래가 하나씩 는다. LIST_APPEND는 어디서나 뜨겁다.
+ - `drop_nested`가 누구의 리스트를 놓을지 가려야 한다.
+ - `nested.shared`를 세서, APPEND_IN이 빌린 값을 통해 let의 리스트에 쓰지 않게 해야 한다.
+ - 블록 안의 let 리스트가 반복마다 도는 비우기 고리가 떼어 내기를 부른다.
+ - 그 값으로 얻는 것은 10%, 6%, 2%, 0, 0, 0이다. 지금은 만들지 않고 결정점으로 남긴다.
+2. **컴파일 때 옮긴다.** aup가 let 리스트의 마지막 쓰임을 알아 LIST_VALUE를 "옮기기"로 낸다. LIST_VALUE의 뜻(명령의 계약)을 바꾸므로
+ 이번의 규칙(새 명령도 계약의 변경도 없이) 밖이다. 적어만 둔다.
+3. **배열을 통째로 복사한다(만들었다).** 원본이 FLAT 배열이고 키가 없고 칸이 수, bool, i64뿐이면, 열마다 memcpy로 새 리스트를
+ 짓는다(`pure_list_copied`). 내부 표현일 뿐이라 명령도 모듈의 꼴도 검증기도 그대로다. 리스트 값을 쓰지 않는 일에는 닿지 않는다.
+
+**결과.**
+
+| 복사 하나 | 전 | 뒤 |
+|---|---|---|
+| 3항목 | 540 ns | 320 ns |
+| 60항목 | 3,926 ns | 324 ns |
+| 600항목 | 38,268 ns | 985 ns |
+
+(계측한 빌드의 값이다. 시계 값으로 한 번에 100–150 ns가 더 들어 있다.)
+
+| 명세 | 코드 포인트·바이트당 ns 전 → 뒤 | 명령 |
+|---|---|---|
+| UAX #14 | 6,917 → 6,445(**−6.8%**) | 같다(1,774.3) |
+| JSON 트리 | 606.0 → 607.4 | 같다 |
+| HTTP/1.1 | 270.6 → 270.5 | 같다 |
+| inflate | 895.0 → 895.2 | 같다 |
+
+- UAX #14의 LIST_VALUE 몫은 약 10%에서 약 3%로 줄었다. 남은 것은 한 번의 고정 비용(할당)이다.
+- JSON 트리와 HTTP는 그대로다. 원본이 트리이고(레코드에 리스트나 바이트가 들었다), 리스트를 담은 레코드는 칸마다 공유 수를 센다.
+- **남은 결정점.** 셋이 있다. 모두 지금은 만들지 않는다(얻는 것이 3% 이하, 6% 이하).
+ - 리스트 값의 리스트를 사건마다 새로 할당하지 않고 다시 쓰기(UAX #14의 남은 약 3%).
+ - 트리의 잎을 통째로 복사하기(JSON 트리, HTTP).
+ - 빌려주고 떼기.
+- **검사.** `test-module`의 `copied_lists`가 무작위 리스트(좁고 넓은 수, bool, i64)를 복제해 하나씩 넣은 것과 같은지 보고, 복제본과
+ 원본을 따로 바꿔 서로 닿지 않는지 본다. 트리, 키, 텍스트는 그렇게 복사하지 않는다. ASan에서도 지난다. aup의 `make check`(리스트
+ 값, 블록 안의 리스트, 저장되는 중첩)와 Pure의 `make check`가 지난다.
+- **다른 일에 닿지 않는다.** 041의 다섯 일(b49 대 b50, 3라운드의 사용자 모드 명령)은 묶음과 라운드 두 길 모두 ±0.03% 안이다.
+ 가장 큰 것이 묶음의 knapsack +0.023%다. 은행 질의(s49 대 s50, 025의 count.py, 둘씩 번갈아)는 모두 0.3% 안이다(in_credit
+ +0.26%, per_branch +0.15%). 같은 빌드끼리의 흔들림(per_branch 0.47%)보다 작다. 리스트 값을 쓰지 않는 일이므로 바뀌지 않는 것이
+ 맞다.
+- 원본: `2026-10-19-list-value.jsonl`, 041의 `2026-10-19-counts.jsonl`(b49, b50), 040의 `2026-10-19-counts.jsonl`.
+
## 착수 기준 (다음 일)
- **바꿀 결정.** 계산의 핵을 설계한다(027). 위 표의 여덟 후보 가운데 무엇을 어떤 모양으로 받을지 정한다.