diff options
| author | eunhhu <[email protected]> | 2026-10-06 10:32:56 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-06 10:32:56 +0100 |
| commit | 5f76620996a75affaec0d97c622ed79debe0d7db (patch) | |
| tree | 76986892c71d39fca70363eb19b82e8975460bd0 | |
| parent | 644a27a732e44003a9bb05385af5eb224810a375 (diff) | |
| download | pure-5f76620996a75affaec0d97c622ed79debe0d7db.tar.gz pure-5f76620996a75affaec0d97c622ed79debe0d7db.zip | |
Research 129: the batch executor's costs cut (appends at once 5.18 to 0.65 ms, a recurrence's steps as numbered values and one loop of actions 44.4 to 21.8, reductions' ends in any order where it cannot tell 9.80 to 4.00): knapsack 10.8, matmul 36.4, join 118, sort 55.5 ms without machine code; where each work's time goes now; BFS's arithmetic (a tail of about a tenth of the edges, done batch-side; edges spread into lanes, not walked together) and the three pieces it needs; the view-workers check's flake; 024 and CHECKPOINT say so
| -rw-r--r-- | CHECKPOINT.md | 2 | ||||
| -rw-r--r-- | docs/design/024-batch-executor.md | 2 | ||||
| -rw-r--r-- | docs/research/129-batch-executor-costs-cut.md | 124 |
3 files changed, 126 insertions, 2 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md index 3d33bf4..b6a01b7 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). 덧붙이기, 점화식, 칸에 더하기, 걷기를 묶어 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) | +| 수명과 계산 | 값의 수명은 선언이 말한다: `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). 덧붙이기를 리스트의 한 호출로, 점화식을 레지스터 꼴로, 모으기의 끝을 차례 없이 하자 knapsack 10.8, matmul 36.4, join 118, sort 55.5 ms가 됐다(기계어 7.9, 14.2, 72–79, 35.6; 129). BFS는 프런티어 묶음으로 셈하면 차례대로 할 꼬리가 간선의 약 10%다(129). 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), [129](docs/research/129-batch-executor-costs-cut.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/024-batch-executor.md b/docs/design/024-batch-executor.md index bfbe1e9..91e60a5 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). 4단계의 앞 절반(덧붙이기), 5단계(점화식), 칸에 더하기, 걷기를 만들었다([128](../research/128-batch-executor-tables-and-costs.md): join이 실행기의 423에서 161 ms, 계층 74). 남은 값은 덧붙이기 하나 약 100 ns, join의 점화식 회차당 약 60 ns, 레인이 적은 중첩 몸체의 해석이고, BFS는 아직 묶이지 않는다(바뀌는 개수, 커지기만 하는 걸러내기, 중첩 걷기가 필요하다). +작성일: 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는 아직 묶이지 않는다(바뀌는 개수, 커지기만 하는 걸러내기, 중첩 걷기가 필요하다). 그 셋을 줄였다([129](../research/129-batch-executor-costs-cut.md): 한꺼번에 덧붙이기, 점화식을 레지스터 꼴로, 차례 없는 모으기; knapsack 10.8, matmul 36.4, join 118 ms, roots 8.87로 계층 9.57보다 빠름). BFS를 셈하니 차례대로 할 꼬리가 간선의 약 10%라, 꼬리를 묶음의 C로 하고 간선을 레인으로 펼치면 계층과 겨룰 만하다(약 6–7 ms 예상). [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/129-batch-executor-costs-cut.md b/docs/research/129-batch-executor-costs-cut.md new file mode 100644 index 0000000..c807df1 --- /dev/null +++ b/docs/research/129-batch-executor-costs-cut.md @@ -0,0 +1,124 @@ +# 129 — 묶음 실행기의 값 줄이기: 한꺼번에 덧붙이기, 점화식을 레지스터 꼴로, 차례 없는 모으기, 그리고 BFS의 산수 + +2026-10-06. [128](128-batch-executor-tables-and-costs.md)이 잰 값 가운데 셋을 줄였다. 덧붙이기 하나 약 100 ns, 점화식 한 회차 약 +60 ns, 모으기의 끝 회차당 4–9 ns다. 코드는 `4791c42`(덧붙이기), `9f0498b`와 `e8558e7`(점화식), `f9e3d22`(모으기)다. 128과 같은 +방법으로 쟀다(`041-batches/runs.sh`, 빌드 `b5` = `f9e3d22`, `b6` = `e8558e7`, 11번의 중앙값; 작은 모듈은 `costs/`). 그리고 BFS를 +묶는 설계가 기준을 넘을 수 있는지 먼저 셈했다(`041-batches/frontier.c`). + +## 쉬운 말로 + +- **기계어 없이 넷이 빨라졌다.** knapsack 15.1 → 10.8 ms, matmul 46.1 → 36.4, join 161 → 118, sort 74 → 55.5다. roots는 8.87 ms로 + 기계어(9.57)보다 빠르다. 기계어와의 차이는 knapsack 2.9 ms, matmul 22 ms, join 40 ms, BFS 89 ms로 줄었다. +- **바꾼 것은 셋이다.** 묶음의 덧붙이기를 항목마다 실행기의 걸음으로 하지 않고 리스트의 한 호출로 한다. 점화식의 걸음은 스택이 + 아니라 번호 붙은 값 사이의 연산이다. 모으기의 끝은 차례가 답을 바꾸지 않는 곳(`min`, `max`, 2^53 안의 정수 합)에서 차례 없이 + 벡터로 합친다. +- **BFS는 셈해 보니 가능성이 있다.** 프런티어를 묶음으로 돌면, 묶음을 열 때 `has`가 거짓이라 차례대로 다시 물어야 하는 간선이 100만 + 개 가운데 약 10만 5천 개다. 이 꼬리를 실행기가 아니라 묶음의 C로 하면 1 ms 안팎이다. 다만 레인마다 간선 수가 달라, 간선을 레인으로 + 펼쳐야 한다(함께 돌면 2.17배를 돈다). 아직 만들지 않았다. + +## 묶음 + +| 일 | 기계어 (JIT) | 묶음 (`PURE_JIT=0`) | 실행기만 | C | 묶음의 명령 / 실행기만¹ | +|---|---:|---:|---:|---:|---:| +| roots | 9.57 ms | **8.87** | 130.4 | 6.86 | 1.67억 / 27.56억 | +| knapsack | 7.93 | 10.84 | 1,042.9 | 6.24 | 3.76억 / 216.88억 | +| matmul | 14.20 | 36.41 | 912.0 | 7.55 | 9.24억 / 214.37억 | +| join | 78.7 | 118.3 | 422.2 | 30.9 | 19.07억 / 76.70억 | +| sort | 35.6 | 55.5 | 77.3 | 26.9 | 10.13억 / 15.03억 | +| BFS | 7.66 | 96.9 | 92.9 | 9.93 | 46.93억 / 47.16억² | +| words | 215.6 | 215.2 | 207.0 | 37.8 | 39.76억 / 39.34억² | + +¹ `suite-run` 세 바퀴의 사용자 모드 명령(C와 읽기 포함, 126과 같다). ² `b5`. + +- `b4`(128)에서 `b6`으로 묶음의 시간: knapsack 15.07 → 10.84, matmul 46.07 → 36.41, join 161.2 → 118.3, sort 74.3 → 55.5, roots + 9.22 → 8.87 ms. 명령은 knapsack 4.72억 → 3.76억, matmul 11.18억 → 9.24억, join 28.77억 → 19.07억, sort 14.09억 → 10.13억이다. +- 실행기만의 명령은 그대로다(roots 27.56억, knapsack 216.88억, `b4`부터 `b6`까지). 바꾼 것은 묶음과 리스트의 코드뿐이다. +- knapsack은 `b5`에서 10.17 ms였다. 명령이 같고(3.76억) 사이클이 3% 많아, 놓인 자리로 본다. 기계어의 join은 빌드가 같은 코드인데 + 72.3–78.7 ms로 오간다. + +## 바꾼 것 + +- **한꺼번에 덧붙이기**(`4791c42`). 묶음의 끝에서, 모든 덧붙이기가 이벤트의 리스트에 하나씩이고(색인 없이), 리스트가 그 꼴을 + 지키며 받을 수 있으면(`pure_list_takes`: 키 없는 FLAT은 끝에, DENSE는 지금의 개수로 담을 수 있는 키로, HASHED는 수 키로; 배열의 + 한계를 넘지 않게) 128개씩 한 번에 넣는다(`pure_list_add_many`). 메모리 몫은 넣기 전에 끝의 모습으로 셈한다. 항목 하나는 몫이 세는 + 것을 늘리기만 하므로, 끝이 몫 안이면 그 사이의 어느 회차도 넘지 않는다. 넘으면 지금처럼 회차대로 해서 그 회차에서 결함이 난다. + 열은 한 번 늘리고 한 번 넓힌다. 이미 있는 키를 만나면 그 회차의 `EXISTS`다. 리스트의 꼴을 바꾼 회차(키 있는 리스트의 첫 항목이 + DENSE나 HASHED를 만든다) 뒤에는 다시 한 번에 넣기를 시도한다. DENSE에 5만 개: 5.18 → 0.65 ms(기계어 0.28). +- **점화식을 레지스터 꼴로**(`9f0498b`, `e8558e7`). 걸음은 스택에 넣고 빼는 대신, 한 회차가 쥐는 수(걸음의 결과, 점화식의 값, 루프의 + 값, 그대로인 수)에 번호를 매기고 그 두 번호의 연산이다. 한 문장 안에서 두 번 계산하는 연산은 한 번만 한다(문장이 끝나기 전에는 + x가 그대로다). 한 회차는 묶음마다 한 번 깔아 둔 동작들을 도는 루프 하나이고, 갱신의 마지막 걸음이 x에 바로 쓴다. 그 사이에 x를 + 읽는 자리(기록)는 x가 바뀌지 않았으면 레인 배열 하나를 함께 쓴다(join의 네 자리가 한 번 저장). MINSTD 100만 회차: 44.4 → 21.8 + ms(기계어 18.5, 실행기만 73.0). x를 네 번 읽어 합하면 50.4 → 36.0 ms(기계어 19.9). +- **차례 없는 모으기**(`f9e3d22`). `min`과 `max`의 합치기는 차례가 답을 바꾸지 않는다(−0이 +0 아래). 정수의 합도 크기의 합이 2^53 + 아래면 그렇다. 그런 끝은 레인 순서로 넷씩 합친다. 레인마다의 `min`, `max` 걸음과 그 끝은 수를 그 차례의 정수로 바꿔 비교하는 + 루프라 벡터가 된다. 레인을 비우는 루프는 개수를 지역 변수로 두어 벡터가 됐다. 100만 회차의 `max`: 9.80 → 4.00 ms(기계어 + 4.27), 회차를 세는 합: 4.51 → 1.61 ms. + +## 무엇에 값을 내는가 + +`costs/time.c`, 세 길, 세 번의 중앙값(`2026-10-06-costs.jsonl`의 `b5`, 점화식은 `b6`; 괄호는 `b4`). + +| 모듈 | 기계어 | 묶음 | 실행기만 | +|---|---:|---:|---:| +| `ops` (`max` 모으기) | 4.27 ms | **4.00** (9.81) | 156.3 | +| `nest` | 23.1 | **10.17** (10.20) | 653.0 | +| `adds` | 0.28 | 0.65 (5.18) | 5.17 | +| `probes` | 56.1 | 73.0 (85.1) | 259.7 | +| `recur` | 18.45 | 21.8 | 73.0 | +| `recur4` | 19.9 | 36.0 | 181.4 | + +`b6`의 표본(묶음, Pure의 몫 가운데, 대략): + +- **join (118 ms)**: 점화식 약 35 ms(회차당 약 33 ns: 연산의 사슬에 나누기가 있고, 값이 메모리를 거쳐 다음 걸음으로 간다), 레인의 + 산수와 칸 읽기 약 32 ms, HASHED 찾기 약 24 ms, 칸에 더하기와 끝 정리 약 20 ms, 덧붙이기 약 6 ms. +- **matmul (36 ms)**: 중첩 몸체 약 32 ms(안쪽 회차마다 명령 15개를 레인 200개에), 표를 만드는 루프 약 4 ms. 기계어는 전체가 14 ms다. +- **knapsack (11 ms, `b5`)**: 안쪽 루프 약 7 ms, 미룬 쓰기의 정리 약 2 ms, 덧붙이기 1 ms 아래. +- **sort**: 묶음 밖이다. 실행기, 정렬(`make_ordered`), 라이브러리의 몫이 대부분이다. + +## BFS의 산수 + +`frontier.c`는 스위트의 그래프(노드 10만, 간선 100만)로 BFS를 프런티어 묶음처럼 돈다. 회차 `i`부터 묶음을 열 때 큐의 개수까지(최대 +B개), 각 간선의 `has`는 묶음을 열 때 묻는다. 거짓이라 한 간선이 후보이고, 후보는 회차 차례대로 다시 물어 더하거나(처음 보는 노드) +버린다(같은 묶음의 앞 회차가 더했다). + +| B | 간선 | 후보 | 더하기 | 묶음 | 함께 돌 때의 레인-회차 | +|---:|---:|---:|---:|---:|---:| +| 1024 | 999,913 | 105,232 | 99,997 | 101 | 217만 (간선의 2.17배) | +| 256 | 999,913 | 101,243 | 99,997 | 394 | 200만 (2.00배) | +| 64 | 999,913 | 100,296 | 99,997 | 1,565 | 181만 (1.81배) | + +- **꼬리는 작다.** 차례대로 해야 하는 후보가 간선의 약 10%다. 후보 하나에 DENSE 비트를 다시 묻고, 부모의 깊이를 읽고(부모는 묶음을 + 열 때 이미 있다), 비트와 깊이를 쓰고, 큐에 덧붙인다. 묶음의 C로 하면 하나에 약 10 ns, 1 ms 남짓이다. 실행기의 걸음으로 하면 + 10만 × 100 ns 이상이라 그것만으로 기계어 전체(7.7 ms)를 넘는다. 그래서 꼬리는 `at_once`처럼 묶음 쪽 C여야 한다. +- **간선은 펼친다.** 레인마다 간선 수가 다르다(평균 10). 중첩 루프처럼 함께 돌면 가장 긴 레인만큼 돌아 2.17배다. 묶음의 노드마다 + 간선 행의 범위를 구해 간선을 레인으로 이어 붙이면(024가 적은 "펼치기") 간선 하나에 레인 하나다. +- **예상.** 간선 하나에 레인의 일 몇 가지(행의 `to` 읽기, 비트 묻기, 고르기) 약 3–4 ns로 3.5 ms, 노드 하나에 간선 범위 찾기(묶은 + 리스트의 색인) 약 15–20 ns로 2 ms, 꼬리 1 ms, 해석 값은 묶음 101개라 무시할 만하다. 합해 약 6–7 ms로, 기계어의 7.7 ms(C 9.9)와 + 겨룰 수 있다. 캐시 미스(간선 열 4 MB)는 C와 기계어도 내는 값이고, 묶음은 레인이 서로 떨어져 있어 미스가 겹친다. +- **필요한 셋.** (1) `if i >= L.count { break }`이고 몸체가 L에 덧붙이기만 하면, 묶음은 열 때의 개수까지다. (2) 레인마다 다른 걷기를 + 레인으로 펼친다. (3) 커지기만 하는 걸러내기: `if !L.has(k) { … }`이고 L이 몸체에서 더해지기만 하면, 열 때 참인 레인은 건너뛰고, + 거짓인 레인은 끝에서 차례대로 다시 묻고 몸체의 덧붙이기를 한다. 몸체가 읽는 것은 열 때 있던 것이어야 한다(BFS의 + `seen[q.node]`는 프런티어의 노드라 열 때 있다; 계획이 이것을 알아볼 규칙이 필요하다). (1)과 (2)는 (3) 없이도 시험할 수 있다 + (프런티어를 돌며 간선 수를 합하는 루프). 그것부터 만들고 잰다. + +## 뜻과 맞음 + +- `APPEND_SOURCE`: DENSE 리스트에서 회차 q에, HASHED 리스트에서 회차 p에 이미 있는 키(그 회차의 결함), 회차 p부터 DENSE가 담지 + 못하는 키(그 회차부터 꼴이 바뀐다), 한 리스트에 회차마다 두 번 덧붙이기(회차대로), 넓어지는 열, 덧붙이기 한가운데에서 다하는 + 메모리 몫 셋. 세 길(묶음, 실행기만, 기계어)이 같고, `exists`, `memory`, `work` 결함이 다 난다. +- 시드 1, 2, 3, 5, 23의 무작위 프로그램(665개)이 커밋마다 다섯 길에서 같았다. 시드 23의 프로그램에서 한꺼번에 덧붙이기가 31,402번 + (항목 400만 개) 돌았고, 메모리 몫 때문에 회차대로 간 것이 292번, 이미 있는 키의 결함이 4번이었다. +- 단위 시험이 세 JIT 모드와 ASan(실행기 모드)에서, aup의 검사가 두 모드에서 통과했다. sanitized `pure-run`으로 시드 1, 5, 23의 + 프로그램과 고정 소스 넷(410개)을 돌려도 오류가 없었고 출력은 실행기만으로 돈 것과 같았다. +- Pure의 `make check`가 새벽에 다섯 번 연속 `server_view_workers`(모듈을 바꾼 뒤 스트림이 10초 안에 새 값을 주지 않음)에서 + 실패했다. 묶음을 꺼도, 바꾸기 전 커밋에서도 실패했고, 같은 코드가 09:50에는 세 번 다 통과했다. 따로 돌리면 늘 통과한다. 다른 + 세션이 기기를 쓰던 때의 타이밍 문제로 본다(이 시험은 10-05에도 같은 꼴로 한 번 실패했다). + +## 다음 + +- **모으기 인식의 빈틈.** `total = total + A + B`는 `(total + A) + B`라서 지금의 `x = x op e` 인식에 맞지 않고, 루프 전체가 + 계획에서 빠진다(같은 계산을 `total + (A + B)`로 쓰면 묶인다: 181 → 36 ms). 정수의 합에서는 둘이 같으니, 한 회차에 여러 번 더하는 + 모으기로 받을 수 있다. +- **BFS**: 위의 (1)과 (2)를 만들고 재고, 그다음 (3). +- **join**: HASHED 찾기를 레인에 대고 바로 하기(약 24 ms), 레인의 산수. +- **matmul**: 레인이 적은 중첩 몸체의 명령당 값(미리 풀어 둔 몸체, 곱해 더하기). |
