aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-06 04:32:33 +0100
committereunhhu <[email protected]>2026-10-06 04:32:33 +0100
commit9cb6985c0d9459ccf4dbbab4b05109168c362f75 (patch)
treeccacc7e336a4dfdf149bb5791e655e72a18ceea1
parent15d1ac33dbc54962578726d0add3d08137154457 (diff)
downloadpure-9cb6985c0d9459ccf4dbbab4b05109168c362f75.tar.gz
pure-9cb6985c0d9459ccf4dbbab4b05109168c362f75.zip
Research 128: the batch executor's appends, recurrences, sums into cells and walks (join 423 to 161 ms without machine code, the tier 74) and what a batch pays for, measured apart: an instruction about 20 ns a batch (0.02 ns a round at 1024 lanes), a reduction's end 4-9 ns a round, an append about 100 ns (the executor's), join's recurrence about 60 ns a round; BFS not batched (a count that grows, a filter that only grows, nested walks), so the JIT stays for now; the next in that order: appends at once, the recurrence's steps in registers, reductions' ends; 022 and 024 and CHECKPOINT say so
-rw-r--r--CHECKPOINT.md2
-rw-r--r--docs/design/022-arrays-and-registers.md4
-rw-r--r--docs/design/024-batch-executor.md2
-rw-r--r--docs/research/128-batch-executor-tables-and-costs.md147
4 files changed, 153 insertions, 2 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 87f644f..3d33bf4 100644
--- a/CHECKPOINT.md
+++ b/CHECKPOINT.md
@@ -13,7 +13,7 @@
| 영역 | 확인한 것 | 근거 |
|---|---|---|
| 앱 표현 | 상태(수, `bool`, 정확한 64비트 정수 `i64`, text와 커서·마크, 레코드의 리스트와 키 있는 리스트, 인덱스), 입력(키, text, 필드 있는 메시지, 타이머), 원자적 이벤트 핸들러, 순수 보기(조건식, 표와 걸러진 표, count·sum·max·min·first), `fn`·`use` 라이브러리, text 비교, 시간(`now`, `every`, `floor`, `%`). 로드할 때 타입·스택·합류·커서와 인덱스의 소속을 모두 검증하고 이벤트의 실행량에 상한이 있다 | 008, [016](docs/design/016-bool-and-i64.md), aup `docs/design/002-dsl-direction.md`, aup `make check`(파이썬 참조 모델 대조, 진단 223건) |
-| 수명과 계산 | 값의 수명은 선언이 말한다: `state`(저장), `transient state`(앱이 도는 동안: 저장소에 쓰지 않고 다시 시작하면 초기값, SD 카드에서 쓰기 17배), `let`(이벤트 동안). 핸들러와 보기가 계산한다: 이벤트의 리스트(`let seen: list of Seen by node`), 세는 `for i in a .. b`, `break`, 8겹까지의 중첩, 그리고 `view 이름(인자) { … return … }`(스냅숏에서 돌아 소유자를 막지 않는다). 모든 반복은 시작할 때 횟수의 상한을 갖고, 모든 이벤트와 보기는 실행량 예산(정하지 않으면 2^28 명령)과 메모리 몫(2^28 바이트) 안에서 끝난다. 노드 10만·간선 100만의 BFS 한 이벤트가 HTTP로 90 ms였고(처음 397, 020 뒤 191; CPython 452, C 9.1, 123), 022 뒤 코어는 7.4 ms다(같은 프로세스의 C 9.9): 많이 찾는 저장된 리스트를 배열과 칸의 색인으로 펴고, 열을 값만큼 좁히고, 루프 묶음을 레지스터의 기계어로 돈다(리눅스 AArch64의 JIT; 다른 곳은 실행기, 답은 같다). 다만 C를 넘은 것은 미리 읽기 덕이고, 끄면 15.7 ms로 C의 1.57배다. 일곱 가지 일(실수 루프, 배낭, 정렬, 조인, 텍스트, 행렬, BFS)로 C와 견주면 124에서는 3–300배 느렸고, 023의 계층과 표현(HASHED: 차례를 어긴 키는 해시 표의 칸에, 위치가 필요할 때 정렬)으로 지금은 1.19–5.58배(기하 평균 1.92)다(125). Pure의 표현과 뜻대로 손으로 쓴 바닥은 C의 0.90배이고 뜻의 검사는 끌어내면 0–3%라, 막는 것은 실행 계층이다(124). 모든 블록을 SSA로 올리는 최적화 계층(023)의 첫 걸음들이 들어갔고, 텍스트(실행기로 돈다)와 BFS(계층이 덧붙이기를 못 한다)가 남았다. 기계어를 만들지 않는 길로, 루프를 회차 256개의 묶음으로 돌고 연산마다 빌드 때 만든 C 루프를 쓰는 묶음 실행기(024)를 시제품으로 재니 연산을 따로 해석하면 roots 0.99배, 배낭 0.71배(4바이트 레인), BFS 1.03배, join 1.77배, 행렬 곱 2.50배이고, 행렬 곱은 곱해 더하기를 합치면 0.94배였다(계층 1.20–2.46배, 기하 평균 1.92 대 1.58, 합친 꼴로 1.24; 041, 손으로 짠 천장). 1단계(세는 루프의 레인 꼴 몸체, 회차 1024개씩)가 들어가 knapsack이 실행기에서 1,041 ms에서 13.6 ms가 됐다(계층 7.5, 안쪽 루프는 1.17배). 묶음 루프가 없는 모듈의 실행기는 명령과 사이클이 그대로다(126). 2단계(중첩 루프를 레인마다 함께, `x = x + e`·`min`·`max` 모으기)로 roots가 9.13 ms가 되어 기계어 없이 계층(9.57)보다 빠르고, matmul은 914.9에서 50.0 ms다(계층 14.3, 127). TPC-C −2.8%(기준 3% 안), 은행 질의의 명령 수는 1% 안. 같은 계산이 Pi(JIT)·WSL2·M3 Mac에서 같다. 같은 탐색을 보기로 하면 쓰기는 CPU를 나누는 만큼만 줄고(이벤트로 하면 0.2%로 멈춘다), 본문이 있는 보기는 보기의 스레드(`--view-workers`)가 계산해 연결 스레드를 막지 않는다(쓰기 최대 지연이 CPU만 나눌 때의 1.0배, 연결 스레드 하나에서 묻는 연결 둘이 1.95배) | [018](docs/design/018-lifetimes-and-computation.md), [020](docs/design/020-computing-faster.md), [117](docs/research/117-transient-states.md), [118](docs/research/118-computing-in-one-event.md), [119](docs/research/119-views-that-compute.md), [121](docs/research/121-computing-faster.md), [122](docs/research/122-views-off-the-connection-threads.md), [021](docs/design/021-past-c.md), [123](docs/research/123-past-c.md), [022](docs/design/022-arrays-and-registers.md), [124](docs/research/124-against-c.md), [023](docs/design/023-optimizing-tier.md), [125](docs/research/125-tier-first-steps.md), [024](docs/design/024-batch-executor.md), [126](docs/research/126-batch-executor-stage-one.md), [127](docs/research/127-batch-executor-stage-two.md) |
+| 수명과 계산 | 값의 수명은 선언이 말한다: `state`(저장), `transient state`(앱이 도는 동안: 저장소에 쓰지 않고 다시 시작하면 초기값, SD 카드에서 쓰기 17배), `let`(이벤트 동안). 핸들러와 보기가 계산한다: 이벤트의 리스트(`let seen: list of Seen by node`), 세는 `for i in a .. b`, `break`, 8겹까지의 중첩, 그리고 `view 이름(인자) { … return … }`(스냅숏에서 돌아 소유자를 막지 않는다). 모든 반복은 시작할 때 횟수의 상한을 갖고, 모든 이벤트와 보기는 실행량 예산(정하지 않으면 2^28 명령)과 메모리 몫(2^28 바이트) 안에서 끝난다. 노드 10만·간선 100만의 BFS 한 이벤트가 HTTP로 90 ms였고(처음 397, 020 뒤 191; CPython 452, C 9.1, 123), 022 뒤 코어는 7.4 ms다(같은 프로세스의 C 9.9): 많이 찾는 저장된 리스트를 배열과 칸의 색인으로 펴고, 열을 값만큼 좁히고, 루프 묶음을 레지스터의 기계어로 돈다(리눅스 AArch64의 JIT; 다른 곳은 실행기, 답은 같다). 다만 C를 넘은 것은 미리 읽기 덕이고, 끄면 15.7 ms로 C의 1.57배다. 일곱 가지 일(실수 루프, 배낭, 정렬, 조인, 텍스트, 행렬, BFS)로 C와 견주면 124에서는 3–300배 느렸고, 023의 계층과 표현(HASHED: 차례를 어긴 키는 해시 표의 칸에, 위치가 필요할 때 정렬)으로 지금은 1.19–5.58배(기하 평균 1.92)다(125). Pure의 표현과 뜻대로 손으로 쓴 바닥은 C의 0.90배이고 뜻의 검사는 끌어내면 0–3%라, 막는 것은 실행 계층이다(124). 모든 블록을 SSA로 올리는 최적화 계층(023)의 첫 걸음들이 들어갔고, 텍스트(실행기로 돈다)와 BFS(계층이 덧붙이기를 못 한다)가 남았다. 기계어를 만들지 않는 길로, 루프를 회차 256개의 묶음으로 돌고 연산마다 빌드 때 만든 C 루프를 쓰는 묶음 실행기(024)를 시제품으로 재니 연산을 따로 해석하면 roots 0.99배, 배낭 0.71배(4바이트 레인), BFS 1.03배, join 1.77배, 행렬 곱 2.50배이고, 행렬 곱은 곱해 더하기를 합치면 0.94배였다(계층 1.20–2.46배, 기하 평균 1.92 대 1.58, 합친 꼴로 1.24; 041, 손으로 짠 천장). 1단계(세는 루프의 레인 꼴 몸체, 회차 1024개씩)가 들어가 knapsack이 실행기에서 1,041 ms에서 13.6 ms가 됐다(계층 7.5, 안쪽 루프는 1.17배). 묶음 루프가 없는 모듈의 실행기는 명령과 사이클이 그대로다(126). 2단계(중첩 루프를 레인마다 함께, `x = x + e`·`min`·`max` 모으기)로 roots가 9.13 ms가 되어 기계어 없이 계층(9.57)보다 빠르고, matmul은 914.9에서 50.0 ms다(계층 14.3, 127). 덧붙이기, 점화식, 칸에 더하기, 걷기를 묶어 join이 실행기의 423에서 161 ms가 됐다(계층 74). 남은 값은 덧붙이기 하나 약 100 ns(실행기와 같다), join의 점화식 회차당 약 60 ns, 레인이 적은 중첩 몸체의 해석이고, BFS는 묶이지 않아 기계어를 아직 걷어낼 수 없다(128). TPC-C −2.8%(기준 3% 안), 은행 질의의 명령 수는 1% 안. 같은 계산이 Pi(JIT)·WSL2·M3 Mac에서 같다. 같은 탐색을 보기로 하면 쓰기는 CPU를 나누는 만큼만 줄고(이벤트로 하면 0.2%로 멈춘다), 본문이 있는 보기는 보기의 스레드(`--view-workers`)가 계산해 연결 스레드를 막지 않는다(쓰기 최대 지연이 CPU만 나눌 때의 1.0배, 연결 스레드 하나에서 묻는 연결 둘이 1.95배) | [018](docs/design/018-lifetimes-and-computation.md), [020](docs/design/020-computing-faster.md), [117](docs/research/117-transient-states.md), [118](docs/research/118-computing-in-one-event.md), [119](docs/research/119-views-that-compute.md), [121](docs/research/121-computing-faster.md), [122](docs/research/122-views-off-the-connection-threads.md), [021](docs/design/021-past-c.md), [123](docs/research/123-past-c.md), [022](docs/design/022-arrays-and-registers.md), [124](docs/research/124-against-c.md), [023](docs/design/023-optimizing-tier.md), [125](docs/research/125-tier-first-steps.md), [024](docs/design/024-batch-executor.md), [126](docs/research/126-batch-executor-stage-one.md), [127](docs/research/127-batch-executor-stage-two.md), [128](docs/research/128-batch-executor-tables-and-costs.md) |
| 보기 밀기 | 보기의 `GET`에 `Accept: text/event-stream`이면 그 값을 민다(SSE, 브라우저의 `EventSource` 그대로). 같은 보기와 인자를 보는 구독자는 계산 한 번을 나누고, 값이 바뀔 때만, 많아야 10 ms마다 한 번 보낸다. 느린 구독자에게는 가장 새 값 하나만 기다리게 한다. 모듈이 바뀌어도 같은 이름의 보기로 이어 가고, 토큰을 빼면 끊는다. 구독자 1,000에 쓰기 초당 100번까지 신선도 p99 11 ms 이하, 초당 1번이면 100 ms 폴링의 약 10분의 1 CPU(이벤트 하나와 요청 하나의 값이 같아 메시지 수의 비다), 구독자 1만에 구독자마다 0.3–0.4 KiB, 1만에게 퍼지기까지 p99 45 ms | [019](docs/design/019-pushing-views.md), [120](docs/research/120-pushing-views.md) |
| 영속 | 선언한 상태가 곧 영속 상태다. 목록은 이미지와 로그(저장마다 바뀐 기록 한 항목, 재사용하는 할당 구간, 앞 항목의 다이제스트로 이어짐)에, 나머지는 트리 저장소에(커밋당 한 연속 구간과 동기화 한 번). 이벤트 묶음마다 저장 한 번, 이미지는 소유자 밖에서 체크포인트. `kill -9`와 커밋 각 단계의 죽음 뒤 확인 응답한 이벤트가 모두 남는다. 레코드·키가 바뀐 모듈로 바꿔도 데이터가 이름으로 옮겨진다 | [076](docs/research/076-one-sync-commit.md), [081](docs/research/081-tail-records.md), [092](docs/research/092-tpcc.md), 008 상태 영속·교체 |
| 인터페이스 | 한 명세가 TUI(셀 단위 갱신), CLI, 문장(NUI), 웹 GUI가 된다. 저장하면 실행 중인 앱이 상태를 이어 새 모듈로 바뀐다(`aup watch`, `vega … --watch`) | Vega `docs/design/001-first-component.md`, Vega `make check`(가상 터미널, 헤드리스 Chromium 포함) |
diff --git a/docs/design/022-arrays-and-registers.md b/docs/design/022-arrays-and-registers.md
index 53a75f6..1cb0fff 100644
--- a/docs/design/022-arrays-and-registers.md
+++ b/docs/design/022-arrays-and-registers.md
@@ -189,6 +189,10 @@ C의 1.6배 느리다. 기준은 [023](023-optimizing-tier.md)의 묶음으로 �
다시 했다. 2의 거듭제곱 곱을 시프트로 바꾸며 그 검사가 자주 포기하게 되자 aup의 검사(`PURE_JIT=all`, routes.aup의 깊이 우선
탐색)가 잡았다. 포기할 수 있는 산술은 결과를 x16에 만들고 검사 뒤에 옮긴다. `module_assumptions`에 그 꼴(`LOCALS_SOURCE`)을
더했다: 고치기 전 빌드에서 틀리고 고친 빌드에서 맞는다.
+- **장부에서 더 고친 것**(2026-10-06, [128](../research/128-batch-executor-tables-and-costs.md)). 묶음 실행기의 무작위 시험이
+ 덧붙이기를 섞자 둘이 나왔다. HASHED에 덧붙이는 C 호출이 레지스터에 있는 DENSE 장부의 증가를 모른 채 메모리를 봐서 `MEMORY`
+ 결함이 한 회차 늦었다(`794801c`: 처음 덧붙일 때 장부의 여유를 본다). 그리고 레지스터가 모자라 장부를 두지 못한 리스트의 낡은
+ 개수(x30)를 장부를 써 돌릴 때 리스트에 썼다(`dcfbc53`: 장부를 두는 리스트만 써 돌린다).
## 단계
diff --git a/docs/design/024-batch-executor.md b/docs/design/024-batch-executor.md
index b1f8b3a..bfbe1e9 100644
--- a/docs/design/024-batch-executor.md
+++ b/docs/design/024-batch-executor.md
@@ -1,6 +1,6 @@
# 024 — 묶음 실행기: 기계어 없이 루프를 C 가까이
-작성일: 2026-10-05. 상태: 제안과 시제품([041](../../research/experiments/041-batches/)). 1단계를 만들었다([126](../research/126-batch-executor-stage-one.md): knapsack이 실행기에서 1,041 ms → 13.6 ms, 안쪽 루프는 계층의 1.17배로 기준 미달). 2단계(중첩 루프를 함께, 모으기)를 만들었다([127](../research/127-batch-executor-stage-two.md): roots 9.13 ms로 계층 9.57보다 빠름, matmul 50 ms).
+작성일: 2026-10-05. 상태: 제안과 시제품([041](../../research/experiments/041-batches/)). 1단계를 만들었다([126](../research/126-batch-executor-stage-one.md): knapsack이 실행기에서 1,041 ms → 13.6 ms, 안쪽 루프는 계층의 1.17배로 기준 미달). 2단계(중첩 루프를 함께, 모으기)를 만들었다([127](../research/127-batch-executor-stage-two.md): roots 9.13 ms로 계층 9.57보다 빠름, matmul 50 ms). 4단계의 앞 절반(덧붙이기), 5단계(점화식), 칸에 더하기, 걷기를 만들었다([128](../research/128-batch-executor-tables-and-costs.md): join이 실행기의 423에서 161 ms, 계층 74). 남은 값은 덧붙이기 하나 약 100 ns, join의 점화식 회차당 약 60 ns, 레인이 적은 중첩 몸체의 해석이고, BFS는 아직 묶이지 않는다(바뀌는 개수, 커지기만 하는 걸러내기, 중첩 걷기가 필요하다).
[023](023-optimizing-tier.md)의 계층은 루프 묶음을 SSA로 올려 AArch64 기계어를 실행 시간에 만든다. 일곱 가지 일의 기하 평균을
C의 3–300배에서 1.92배로 줄였지만([125](../research/125-tier-first-steps.md)), 실행 가능한 메모리에 코드를 쓰고 리눅스 AArch64에서만
diff --git a/docs/research/128-batch-executor-tables-and-costs.md b/docs/research/128-batch-executor-tables-and-costs.md
new file mode 100644
index 0000000..4c3e7b2
--- /dev/null
+++ b/docs/research/128-batch-executor-tables-and-costs.md
@@ -0,0 +1,147 @@
+# 128 — 묶음 실행기 4·5단계, 칸에 더하기, 걷기: 표를 만드는 루프가 묶이고, 남은 값이 보인다
+
+2026-10-06. [024](../design/024-batch-executor.md)의 4단계 앞 절반(덧붙이기), 5단계(점화식), 3단계의 일부(칸에 더하기), 그리고
+`EACH`의 묶음. 코드는 `ca4978c`, `44f9ced`, `f290225`, `92c060a`이고, 그 사이에 JIT의 장부에서 버그 둘을 고쳤다(`794801c`,
+`dcfbc53`). [126](126-batch-executor-stage-one.md), [127](127-batch-executor-stage-two.md)과 같은 방법으로 쟀다
+(`041-batches/runs.sh`, 빌드 `b4` = `92c060a`, 11번의 중앙값). 묶음이 무엇에 값을 내는지는 따로 지은 작은 모듈로 갈라 쟀다
+(`041-batches/costs/`).
+
+## 쉬운 말로
+
+- **표를 만드는 루프가 묶인다.** matmul, knapsack, join은 처음에 MINSTD 점화식으로 수를 뽑아 리스트에 더한다. 지금까지 이 루프는
+ 실행기에서 돌았다. 이제 점화식은 묶음 앞에서 회차마다 돌고(실행기와 같은 계산), 나머지 몸체는 묶음으로 돌고, 덧붙이기는 묶음의
+ 끝에서 회차 차례대로 한다. join의 `totals[region].amount = totals[region].amount + …`(칸에 더하기)도 묶인다. 묶음으로 join이
+ 실행기만의 423.4 ms에서 161.2 ms가 됐다. 계층(기계어)은 74.4 ms다.
+- **아직 기계어를 걷어낼 수 없다.** 024의 기준은 roots, knapsack, matmul, join, BFS 각각 계층보다 느리지 않은 것이다. roots만
+ 맞는다. 나머지가 어디에 시간을 쓰는지 쟀다. 가장 큰 것 셋은 이렇다. 덧붙이기 하나에 약 100 ns(실행기와 같다, 기계어는 5.6 ns),
+ join의 점화식 한 회차에 약 60 ns, matmul의 안쪽 회차에서 명령을 해석하는 값. BFS는 지금의 묶음 규칙으로는 묶이지 않는다(아래,
+ 열린 문제).
+- **명령을 해석하는 값은 레인이 많으면 작다.** 묶음마다 명령 하나에 약 20 ns다. 레인 1024개로 나누면 회차당 0.02 ns다. 그래서
+ 몸체를 미리 풀어 두는 것(pre-decode)보다 레인마다 드는 값(덧붙이기, 찾기, 모으기의 끝)을 줄이는 것이 먼저다. 레인이 적은 곳
+ (matmul의 안쪽 루프 200, BFS의 간선 약 10)에서만 해석 값이 보인다.
+
+## 묶음
+
+| 일 | 기계어 (JIT) | 묶음 (`PURE_JIT=0`) | 실행기만 | C | 묶음의 명령 / 실행기만¹ |
+|---|---:|---:|---:|---:|---:|
+| roots | 9.57 ms | **9.22** | 130.2 | 6.86 | 1.75억 / 27.56억 |
+| knapsack | 7.93 | 15.07 | 1,039.0 | 6.23 | 4.72억 / 216.88억 |
+| matmul | 14.23 | 46.07 | 911.8 | 7.51 | 11.18억 / 214.37억 |
+| join | 74.4 | 161.2 | 423.4 | 31.5 | 28.77억 / 76.70억 |
+| sort | 35.8 | 74.3 | 77.1 | 26.9 | 14.09억 / 15.03억 |
+| BFS | 7.95 | 96.9 | 92.7 | 10.0 | 46.71억 / 47.12억 |
+| words | 214.2 | 214.9 | 209.1 | 38.4 | 39.82억 / 39.34억 |
+
+¹ `suite-run` 세 바퀴의 사용자 모드 명령(`icount.c`, 126과 같이 C와 읽기도 포함).
+
+- BFS와 words는 묶음으로 바뀐 것이 거의 없다. 둘 다 이제 묶음 걷기가 하나씩 있어(BFS는 그래프를 읽는 핸들러의
+ `for r in load.rows`, words는 끝의 걷기) 모듈 전체가 실행기의 사본(`run_batches`)을 돈다. 사본에서 묶음 아닌 코드는 명령이
+ 약 2% 많다(126). words는 프로세스의 명령이 1.2% 많다. BFS는 읽기의 걷기가 묶여 프로세스의 명령이 0.9% 적지만, 재는 탐색은
+ 사본으로 돈다. 탐색의 시간 차(96.9 대 92.7, 4.5%)는 그 2%보다 크고 `b3`에서는 반대(92.5 대 95.8)였으니, 나머지는 놓인
+ 자리로 본다.
+- knapsack은 `b3`(`f290225`)에서 14.71 ms였다. 명령은 같고(4.71억 대 4.72억) 사이클이 1.7% 많다. 놓인 자리다.
+
+`b2`(127)에서 `b4`로 묶음의 명령: roots 1.73억 → 1.75억, knapsack 4.65억 → 4.72억, matmul 12.06억 → 11.18억, join 76.69억 → 28.77억,
+sort 15.02억 → 14.09억. 묶음 루프가 없는 모듈의 실행기는 그대로다(126의 두 벌).
+
+## 만든 것
+
+- **덧붙이기(4단계의 앞 절반).** 묶음의 몸체가 수와 bool의 항목을 리스트에 더하거나 덧붙일 때, 그 리스트를 몸체가 달리 읽지 않고
+ (색인도 걷기도 없이) 중첩 루프가 없으면 묶는다. 항목은 묶음의 끝에서 회차 차례대로 실행기의 걸음(`pure_run_step`)으로 만든다.
+ 그래서 이미 있는 키, 메모리 몫, 되돌리기 기록이 실행기의 것 그대로다.
+- **점화식(5단계).** 회차가 쓰기 전에 읽는 지역 변수로, 쓰는 곳마다 `x = (x의 식)`이고, 그 식이 수, 필드, 상태, 루프의 값, 몸체가
+ 쓰지 않는 지역 변수로만 되고, 분기 안에 있지 않은 것. 로드 때 그 쓰기를 `PLAN_RECUR`로 표시한다. 묶음마다 식을 걸음(스택의 수,
+ x, 루프의 값, 연산)으로 한 번 풀고, 묶음 앞에서 회차마다 실행기와 같은 계산으로 돈다. 다른 곳에서 x를 읽는 값은 레인마다 기록해
+ 두고, 묶음의 패스는 그 쓰기를 건너뛴다. 어떤 회차의 걸음이 결함을 내면(수가 아니게 되거나 0으로 나누기) 묶음이 회차들을 돌려준다.
+- **칸에 더하기(3단계의 일부).** `L[k].f = L[k].f + e`에서 k를 두 번 같게 계산하고, k와 e가 L을 읽지 않으면, 패스는 두 번째 k와
+ 그 찾기, 칸 읽기를 건너뛰고 행과 e를 남긴다. 묶음의 끝에서 회차 차례대로 실행기처럼 더하고 쓴다. 더한 값이 수가 아니면 그 회차에서
+ 실행기의 결함이고, 열이 좁으면 넓힌다. 묶음이 그 칸을 달리 읽거나 쓰면 회차들을 돌려준다.
+- **걷기.** `EACH`의 몸체가 묶음 꼴이면, 항목 1024개씩을 한 묶음으로 돈다. 레인은 항목의 자리이고(걷는 리스트의 `ROW`는 아핀),
+ 작업량은 열 때 센다. 묶음이 끝나면 걷기의 자리를 묶음 뒤로 옮긴다. 걷는 리스트에 덧붙이는 몸체는 묶지 않는다.
+- **세 번째 출구.** 묶음의 끝에서 회차 차례대로 하는 일(덧붙이기, 칸에 더하기)은 어떤 회차에서 결함이 날 수 있다. 그때는 묶음이
+ 회차들을 돌려주지 않고, 그 회차의 실행기 결함으로 끝낸다. 이벤트가 되돌려지므로 남는 것은 결함의 종류와 명령, 그때까지의 작업량
+ 뿐이고, 묶음은 그 셋을 실행기와 같게 낸다.
+
+## JIT에서 고친 것
+
+무작위 시험이 덧붙이기를 섞자, 묶음이 아니라 022의 장부에서 기계어가 실행기와 다른 곳이 둘 나왔다.
+
+- **HASHED에 덧붙일 때의 메모리 몫**(`794801c`). 장부가 DENSE 리스트의 증가를 레지스터에 두는 묶음에서, HASHED 리스트에
+ 덧붙이는 C 호출은 앱의 증가에 장부의 몫이 아직 없는 채로 메모리를 봤다. 그래서 `MEMORY` 결함이 한 회차 늦게 났다(DENSE, FLAT,
+ HASHED에 덧붙이는 루프, `memory 6000`에서 작업량 3562 대 3525). 이제 처음 덧붙일 때 장부의 여유를 본다. `pure-run`에 `memory N`
+ 명령을 더했다.
+- **장부를 두지 않는 리스트의 개수**(`dcfbc53`). 레지스터가 모자라 장부를 두지 못한 리스트도 살아 있는 개수(x30, 열세 번째
+ 레지스터)를 쥐고 있다가, 덧붙이기가 장부 없는 길로 간 뒤 그 낡은 개수를 리스트에 썼다(`t.has`를 읽는 두 겹 루프 뒤에 덧붙이는
+ 루프, `PURE_JIT=all`에서 개수 36이 1). 이제 장부를 두는 리스트만 써 돌린다.
+
+## 무엇에 값을 내는가
+
+같은 빌드에서 `costs/time.c`(모듈 하나에 `go`를 일곱 번, 중앙값)로, 세 길 각각을 세 번 돌렸다
+(`2026-10-06-costs.jsonl`). 그리고 묶음으로 돌 때 표본을 떠서(`PURE_JIT=0`, 2 kHz) 함수와 줄로 나눴다.
+
+| 모듈 | 하는 일 | 기계어 | 묶음 | 실행기만 |
+|---|---|---:|---:|---:|
+| `ops` | 1,024,000 회차, 아핀 `let` 여섯과 `max` 모으기 | 4.27 ms | 9.81 | 156.3 |
+| `nest` | 200 × 200 × 200, 안쪽 루프를 레인 200개가 함께 | 23.08 | **10.20** | 653.1 |
+| `adds` | DENSE에 키 5만 개를 차례로 | 0.28 | 5.18 | 5.17 |
+| `probes` | HASHED에 10만 개, 100만 번 찾기 | 56.3 | 85.1 | 256.1 |
+
+- **명령의 해석: 묶음마다 명령 하나에 약 20 ns.** `ops`의 `let`을 하나, 여섯, 열둘로 바꿔 쟀다(묶음 1000개): 9.40, 9.80,
+ 10.30 ms. 모으기를 `total = total + 1`로 바꾸면 4.51, 4.92, 5.42 ms. 어느 쪽이든 명령 44개가 0.90 ms, 묶음마다 명령 하나에
+ 약 20 ns다. 레인 1024개면 회차당 0.02 ns다.
+- **모으기의 끝: 회차당 4–9 ns.** 그러면 `ops`의 나머지는 모으기다. 합이면 회차당 약 4.4 ns, `max`면 약 9.2 ns다(기계어는 몸체
+ 전체가 4.2 ns). 표본으로 보면 합의 63%가 끝에서 회차 차례대로 합치는 루프(레인을 하나씩, 회차의 차례를 계산하고, 검사하며),
+ 20%가 묶음을 열 때 모으기의 레인을 비우는 루프다(벡터가 되지 않았다: 쓰기가 `b->n`과 겹칠 수 있어 회차마다 다시 읽는다).
+ `max`는 레인마다의 비교가 더 든다. 그런데 `min`과 `max`의 합치기는 차례가 답을 바꾸지 않는다(−0과 +0 사이도 같다).
+ 정수의 합도 2^53 안이면 그렇다. 차례대로 해야 하는 것은 분수의 합뿐이다. roots에서 이 끝이 묶음 시간의 약 8%다.
+- **함께 도는 중첩 루프는 기계어보다 빠르다.** `nest`의 안쪽 회차(800만)가 레인당 약 1.3 ns, 기계어는 2.9 ns다.
+- **덧붙이기: 하나에 약 100 ns, 실행기와 같다.** 기계어는 5.6 ns다. 표본의 40%가 실행기의 걸음(`pure_run_step`), 약 45%가
+ 리스트의 더하기와 그 검사(`pure_list_add`, `arrays_fit`, `member_add`, `pure_list_contains`, `dense_holds`)다. 항목마다 실행기의
+ 걸음 하나, 메모리 몫과 되돌리기 기록 하나, 칸마다의 검사를 따로 한다.
+- **HASHED 찾기.** `probes`에서 표본의 41%가 해시 칸의 탐사(`hashed_slot`, `pure_list_lookup`)이고, 대부분 칸을 견주는 줄이다
+ (표의 칸 2^18개가 L2보다 크다: 캐시 미스). 38%가 키의 산수(나누기와 `floor`), 10%가 덧붙이기다. 기계어도 같은 미스를 낸다.
+
+같은 표본을 묶음의 일에 떴다(`b4`, Pure의 몫 가운데 비율, 대략):
+
+- **join (161 ms)**: 점화식의 걸음 약 41%(110만 회차, 회차당 약 60 ns: 걸음 열둘을 하나씩 디스패치하고, 스택이 메모리에 있고,
+ 사슬 안에 나누기가 있다), HASHED 찾기 약 16%, 레인의 산수와 칸 모으기 약 18%, 칸에 더하기 약 9%, 덧붙이기 약 9%.
+- **matmul (46 ms)**: 중첩 몸체 약 67%(안쪽 회차마다 명령 15개를 레인 200개에: 해석 값만 셈하면 15 × 20 ns ÷ 200, 레인당 약 1.5 ns),
+ 표를 만드는 루프 약 30%(덧붙이기 약 20%, 점화식 약 10%).
+- **knapsack (15 ms)**: 덧붙이기 약 28%(약 4 ms), 나머지가 안쪽 루프.
+- **roots**: 모으기의 끝 약 8%.
+
+## BFS: 열린 문제
+
+BFS는 묶음 96.9 ms, 실행기만 92.7 ms(같은 일, 위), 기계어 7.95 ms, C 10.0 ms다. 묶이지 않는다. 033의 `graph.aup`에서,
+
+- 바깥 루프는 `if i >= queue.count { break }`와 `queue.first + i`로 큐를 읽으면서 같은 큐에 덧붙인다. 4단계의 규칙(덧붙이는
+ 리스트를 몸체가 달리 읽지 않는다)에 걸린다.
+- 안쪽 걷기(노드의 간선)는 회차가 약 10개라 묶음의 최소(16)에 못 미치고, `seen.has`로 읽는 리스트에 `seen.add`로 더한다.
+
+024가 적어 둔 길은 둘이다. **바뀌는 개수**: 묶음을 열 때의 `queue.count`까지만 묶는다(BFS의 한 층, 프런티어). **커지기만 하는
+걸러내기**: 묶음 전체에 `has`를 먼저 묻고, 거짓인 레인만 회차 차례대로 다시 물어 더하고 덧붙인다. 여기에 바깥 레인마다 다른
+간선 걷기를 레인으로 펼치는 것(중첩 `EACH`)이 더해져야 한다. 지금의 중첩은 `LOOP` 한 겹뿐이다. 셋 다 만들어야 BFS가 묶음이 된다.
+그리고 그때 레인당 값이 기계어(간선 하나에 명령 약 11개, 022)보다 낮을지는 재 봐야 안다. **JIT를 걷어낼지는 이것에 달려 있다.**
+
+## 뜻과 맞음
+
+- 생성기의 `lanes` 꼴에 덧붙이기(`e`, `d`, 회차가 떨어진 핸들러, 메모리 몫 아래), 칸에 더하기, 점화식(`x1`, 덧붙이기와 쓰기와
+ 칸에 더하기 앞), 걷기(`f`, `g`, `h`, `g`의 조각)를 더했다. 시드 1, 2, 3, 5, 23의 프로그램 665개가 다섯 길(묶음, 실행기만, 기계어의 두
+ 모드, 계층 없는 기계어)에서 같았다. 시드 1에서 걷기의 묶음이 1,891개, 칸에 더하기를 품은 묶음이 278개 돌았다. 명령 목록에 `memory 6000`
+ 구간을 더해, 메모리 몫이 덧붙이기의 한가운데에서 다하는 경우도 견준다.
+- 고정 소스: `BATCH_SOURCE`가 세 칸에 차례로 더하기와 조각의 걷기를 담고, `RECUR_SOURCE`가 회차마다 두 번 바뀌는 점화식, 어떤
+ 회차의 0 나누기와 수를 넘는 곱을 작업량과 메모리 몫 아래에서 담는다.
+- 단위 시험이 세 JIT 모드와 ASan(실행기 모드)에서 통과했다. sanitized `pure-run`(ASan, UBSan)으로 무작위 프로그램과 고정 소스
+ 137개를 돌려도 오류가 없었고, 출력은 실행기만으로 돈 것과 같았다.
+
+## 다음
+
+재 본 것으로 차례를 정한다. 기계어와의 차이(기계어를 걷어내는 기준)로 보면 join 87 ms, BFS 89 ms, matmul 32 ms, knapsack 7 ms다.
+
+1. **한꺼번에 덧붙이기.** 묶음의 끝에서 항목을 실행기의 걸음으로 하나씩 만들지 않고, `module-list.c`의 한 호출로 레인의 칸을
+ 열에 쓴다. 메모리 몫과 되돌리기 기록은 묶음에 한 번, 키의 차례(DENSE)와 중복은 레인의 검사로. join, matmul, knapsack에 함께
+ 걸린다(대략 15, 9, 4 ms).
+2. **점화식의 걸음을 레지스터 꼴로.** 피연산자(상수, x)를 연산에 붙이고, 스택을 레지스터에 두고, 걸음마다의 디스패치를 줄인다.
+ join의 약 65 ms가 여기에 있다. 상수로 나누기의 준비된 역수는 실행기의 `floor(a / m)`와 비트까지 같다는 것을 먼저 보여야 한다.
+3. **모으기의 끝.** `min`, `max`, 2^53 안의 정수 합은 차례 없이(벡터로) 합치고, 레인을 비우는 루프를 벡터로.
+4. **BFS의 세 가지**(위). 그 뒤에 matmul의 합친 연산(곱해 더하기).