diff options
| author | eunhhu <[email protected]> | 2026-10-05 23:22:55 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-10-05 23:22:55 +0100 |
| commit | fffe01904984aebd10b237e400859b5ff86f0729 (patch) | |
| tree | 907c53f1f55956bbcd7e1c71afc64fa1cfca2899 | |
| parent | 2609cbc2347c2e709652c3d0d6f44e52dafba194 (diff) | |
| download | pure-fffe01904984aebd10b237e400859b5ff86f0729.tar.gz pure-fffe01904984aebd10b237e400859b5ff86f0729.zip | |
Research 127: the batch executor's stage 2 (nested loops' rounds run together lane by lane, breaks, reductions folded in the rounds' order) and the measured passes after it (finite checks in the arithmetic's loop, tests as four-byte masks, the loop's value made once): roots 9.13 ms without machine code, below the tier's 9.57; matmul 50 (the tier 14.3: its table's loop and 15 instructions a nested round left); 685 random programs alike; 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/127-batch-executor-stage-two.md | 65 |
3 files changed, 67 insertions, 2 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md index 6662e91..87f644f 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). 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) | +| 수명과 계산 | 값의 수명은 선언이 말한다: `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) | | 보기 밀기 | 보기의 `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 92e065a..6a59f7d 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배로 기준 미달). +작성일: 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). [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/127-batch-executor-stage-two.md b/docs/research/127-batch-executor-stage-two.md new file mode 100644 index 0000000..1b361ee --- /dev/null +++ b/docs/research/127-batch-executor-stage-two.md @@ -0,0 +1,65 @@ +# 127 — 묶음 실행기 2단계: 중첩 루프를 함께, 모으기, 그리고 roots가 기계어를 넘다 + +2026-10-05. [024](../design/024-batch-executor.md)의 2단계와 그 뒤의 다듬기(`4a39bf0`, `bec7ce6`). [126](126-batch-executor-stage-one.md)과 +같은 방법으로 쟀다(`041-batches/runs.sh`, 빌드 `b2` = `bec7ce6`, 11번의 중앙값). + +## 쉬운 말로 + +- **roots가 기계어 없이 계층보다 빠르다.** 실행기만으로 130.6 ms인 roots가 묶음으로 9.13 ms다. 023의 계층(기계어)은 9.57 ms, C는 + 6.86 ms다. 기계어를 만들지 않는 실행기가 이 일에서 계층을 넘은 것은 처음이다. +- **matmul은 914.9 ms에서 50.0 ms가 됐다.** 계층은 14.3 ms라 아직 3.5배다. 남은 것은 둘이다. 하나는 행렬을 채우는 루프로, 점화식이 + 있어 묶지 못하고 실행기에서 약 12 ms가 든다. 다른 하나는 안쪽 루프의 회차마다 명령 15개를 해석하는 값이다(레인이 200개뿐이다). +- **바뀐 것.** 묶음의 몸체 안 루프가 레인마다 함께 돈다. 회차 t는 회차가 t보다 많고 빠져나가지 않은 레인들이 돈다. `BREAK`는 레인마다 + 빠져나가게 한다. `x = x + e`, `min`, `max`는 모으기다. 레인마다 제 몫을 모으고, 묶음의 끝에서 회차 차례대로 합친다. +- **묶음 루프가 없는 모듈의 실행기는 그대로다.** join과 sort의 명령 수는 묶음을 켜도 같다(76.69억, 15.02억). + +## 묶음 + +| 일 | 기계어 (JIT) | 묶음 (`PURE_JIT=0`) | 실행기만 | C | 묶음의 명령 / 실행기만 | +|---|---:|---:|---:|---:|---:| +| roots | 9.57 ms | **9.13** | 130.6 | 6.86 | 1.73억 / 27.56억 | +| knapsack | 7.52 | 13.80 | 1,040.8 | 6.22 | 4.65억 / 216.88억 | +| matmul | 14.27 | 50.03 | 914.9 | 7.55 | 12.06억 / 214.36억 | +| join | 78.6 | 424.8 | 424.7 | 29.3 | 같다 | +| sort | 36.7 | 78.2 | 79.1 | 26.9 | 같다 | + +## 만든 것 + +- **함께 도는 중첩 루프.** 묶음의 몸체에 있는 `LOOP`의 회차를 레인마다 함께 돈다. 레인마다 회차 수가 다르면(`for k in 0 .. i`) + 회차 t의 마스크는 회차가 남은 레인이다. `BREAK`를 탄 레인은 그 루프의 남은 회차에서 빠지고, 루프가 끝나면 다른 레인과 함께 바깥 + 몸체를 이어 간다. 슬롯은 회차가 열릴 때 쓰인다(회차 0에서는 아래 끝 그대로라서 −0도 그대로다). 안쪽 회차마다 그 루프의 작업량 + (블록 길이 + 1)을 레인마다 센다. 묶음의 끝에서 남은 예산과 견주고, 넘으면 아무것도 바꾸지 않고 실행기에 넘긴다. 한 겹까지만 묶는다. +- **모으기.** 몸체에서 `x = x op e`(op는 `+`, `min`, `max`)로만 읽고 쓰는 지역 변수는 모으기다. 레인마다 op의 항등원에서 시작한다. + 합의 항등원은 −0이다(−0 + e는 e다). 끝에서 회차 차례대로 합친다. 한 레인이 한 번만 더하면 회차마다 더한 것과 비트까지 같다. 여러 + 번 더하는 레인이 있으면, 모두 정수이고 2^53 안일 때만 받는다(그래야 묶는 차례가 답을 바꾸지 않는다). 아니면 묶음이 회차들을 + 돌려준다. +- **계획.** 로드 때 지역 변수마다 그 루프의 범위에서 "확실히 쓴 뒤 읽는가"를 본다. 안쪽 루프의 몸체는 제 범위를 갖는다. 그래서 안쪽 + 회차를 넘어 이어지는 값(roots의 `r`, matmul의 `s`)은 레인의 상태가 되고, 바깥 회차를 넘는 값은 모으기여야 한다. +- **레인 재사용.** 안쪽 회차가 끝날 때마다, 지역 변수와 모으기와 마스크와 중첩 상태가 쥐지 않은 레인 버퍼와 마스크를 다시 빈 목록에 + 넣는다(쓸어 담기). 그래서 회차가 64번이어도 레인 버퍼 24개 안에서 돈다. + +## 다듬기: 측정이 가리킨 것 + +2단계를 처음 만들었을 때 roots는 16.8 ms였다. 프로파일에서 가장 큰 것은 유한 검사였다(17%). 벡터화는 돼 있었지만 산수와 따로 +한 번 더 도는 루프였다. 검사를 산수 루프 안에서 지수 비트로 하게 했다. 둘째는 수 비교가 8바이트 0/1을 내서, 분기가 레인마다 값의 +꼴을 물어 다시 읽던 것이었다(8%). 비교가 4바이트 0/1 레인을 내게 해서 그대로 마스크가 되게 했다. 셋째는 바깥 루프의 값(아핀)을 안쪽 +회차마다 수 레인으로 다시 만들던 것이었다(7.6%). 묶음마다 한 번만, 그것도 필요할 때만 만들게 했다. 처음에는 묶음마다 미리 만들게 +해서 knapsack이 13.8에서 17.9 ms로 느려졌다(그 루프는 필요 없다). 그 밖에 아무 레인도 빠지지 않은 동안은 살아 있는 레인의 마스크를 +만들지 않고, 작은 정수를 세는 모으기는 2^53 검사를 건너뛴다. roots 16.8 → 9.1 ms, matmul 68 → 50 ms. + +## 뜻과 맞음 + +- 생성기에 `nest` 꼴을 더했다. 바깥 루프 안에 회차 수가 레인마다 다른 안쪽 루프가 있고, 그 안에 `break`, 회차를 넘는 지역 변수, + 분수가 섞인 안팎의 모으기(`ra`, `rb`, `rc`)가 있다. 시드 1, 2, 3, 5, 23의 프로그램 685개가 세 길(묶음, 실행기만, 기계어)에서 같았다. + 시드 23에서는 137개 중 53개 모듈에서 묶음 4만 1,554개(419만 회차)가 돌았다. 그중 4만 142개가 중첩 루프를, 4만 944개가 모으기를 + 품었다. +- `NESTED_SOURCE`: Newton 걸음과 `break`, 회차마다 분수 하나의 합, 레인마다 0–6회차인 안쪽 루프에서 어떤 레인의 회차에만 나는 0 + 나누기, `max`와 `min`, 레인마다 분수 여럿의 합(묶음이 돌려준다), −0의 합, 격자의 합을 담았다. 작은 예산 둘도 돈다. +- 단위 시험(실행기 모드)이 통과했다. + +## 다음 + +- **matmul.** 안쪽 회차마다 해석하는 명령을 줄인다. 하나는 회차에 따라 바뀌지 않는 계산(`i * n`)을 루프 밖으로 끌어내는 것이고, + 하나는 합친 연산이다. "읽어서 곱해 더하기"는 한 연산이 될 수 있다. +- **점화식(5단계)과 덧붙이기(4단계).** matmul, knapsack, join의 표를 만드는 루프가 여기에 걸린다. 지금은 실행기에서 돈다. +- **join과 BFS.** 키로 모으기와 차례대로 흩어 더하기(3단계), `EACH`와 덧붙이기(4단계)가 필요하다. |
