aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-10-05 13:39:09 +0100
committereunhhu <[email protected]>2026-10-05 13:39:09 +0100
commit557d988af19ae9ec0d7717ffd562ab9d460e7a38 (patch)
tree56e8c504f0f8f536e1ac114e04559b7fe63e1a58
parentb2b7f849d6ab80fceb7d3c3c312fae859613a913 (diff)
downloadpure-557d988af19ae9ec0d7717ffd562ab9d460e7a38.tar.gz
pure-557d988af19ae9ec0d7717ffd562ab9d460e7a38.zip
Research 124: 022's BFS past C only by reading ahead (15.7 ms against 9.9 without), the suite of seven kinds of work at 3-300x C, floors at 0.90x C with the meaning's checks hoisted to 0-3%; CHECKPOINT says so
-rw-r--r--CHECKPOINT.md2
-rw-r--r--docs/research/124-against-c.md89
2 files changed, 90 insertions, 1 deletions
diff --git a/CHECKPOINT.md b/CHECKPOINT.md
index 72592ee..5d4da83 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 한 이벤트 90 ms(처음 397, 020 뒤 191; CPython 452, C 9.1): 이벤트의 리스트가 배열(차례대로, 또는 키 자리에)이고, 키로 찾아 읽기가 하나이며, 루프가 있는 블록은 기계어로 돈다(리눅스 AArch64의 JIT; 다른 곳은 실행기, 답은 같다). C는 넘지 못했다: 저장된 리스트를 B+트리로 찾는 지연과 8바이트 열이 바닥이고, 손으로 쓴 바닥으로는 배열과 좁은 열과 미리 읽기가 함께 있어야 C를 넘는다(123). 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) |
+| 수명과 계산 | 값의 수명은 선언이 말한다: `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와 견주면 지금은 3–300배 느리다. Pure의 표현과 뜻대로 손으로 쓴 바닥은 C의 0.90배이고 뜻의 검사는 끌어내면 0–3%라, 막는 것은 실행 계층이다(124). 모든 블록을 SSA로 올리는 최적화 계층이 다음이다(023). 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) |
| 보기 밀기 | 보기의 `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/research/124-against-c.md b/docs/research/124-against-c.md
new file mode 100644
index 0000000..e11b695
--- /dev/null
+++ b/docs/research/124-against-c.md
@@ -0,0 +1,89 @@
+# 124 — C와 여러 일로 견주기: 022가 남긴 것과 묶음, 뜻의 값
+
+2026-10-05. [022](../design/022-arrays-and-registers.md)를 마치며 잰 것, 그리고 기준을 BFS 하나에서 여러 종류의 일로 옮기며 잰 것.
+코드와 원시 결과는 [040-against-c](../../research/experiments/040-against-c/)에 있다.
+
+## 쉬운 말로
+
+- **BFS는 C보다 빨라졌지만 깡연산으로가 아니다.** 022의 3단계를 마쳐 코어 BFS가 14.6 ms에서 7.4 ms가 됐고, 같은 프로세스의 C(`bfs.c`)는
+ 9.9 ms다. 그런데 가장 큰 한 걸음(12.2 → 8.6 ms)은 미리 읽기를 칸의 마지막 셀까지 넓힌 것이었고, C에 같은 미리 읽기를 하면 7.3 ms다.
+ 미리 읽기를 끄면 Pure는 15.7 ms로 C의 1.57배다. 사용자가 짚은 대로 이것은 BFS에 맞춘 이김이다.
+- **여러 일로 재니 지금의 Pure는 C보다 3–300배 느리다.** 실수 루프 3.3배, 정렬 4.8배, 텍스트 12배, 해시 조인 22배, 배낭 DP 300배,
+ 행렬 곱 304배. 루프 묶음만 기계어로 돌고, 나머지는 실행기다. 배낭의 시간 절반은 키로 둔 표를 찾는 데 쓴다.
+- **뜻의 값은 0에 가깝게 만들 수 있다.** 같은 일을 Pure의 표현과 뜻대로 손으로 C로 썼다. 검사를 뜻이 놓는 자리마다 두면 1.2–12배 느리고,
+ 범위로 증명되는 것을 없애고 루프마다 한 번으로 끌어내면 검사 없는 코드보다 0–3% 느리다. 그 코드는 C의 0.64–1.02배(기하 평균
+ 0.90)다. 그러니 C를 넘지 못하게 하는 것은 −0이나 결함이나 예산이 아니라 실행 계층이다.
+- **그래서 다음은 [023](../design/023-optimizing-tier.md)이다.** 모든 블록을 SSA로 올려 타입과 범위를 알고, 검사를 루프 밖으로 끌어내고,
+ 작업량을 루프마다 세고, 레지스터를 할당한다. 기준(일마다 C의 1.25배 안, 기하 평균 1.0 아래, 뜻의 검사 5% 안)은 구현 전에 커밋했다.
+- **고친 것.** 정수 산술의 결과 레지스터가 피연산자와 같을 때 범위 검사가 포기하면 실행기가 덮인 피연산자로 다시 계산하던 버그를
+ aup의 검사가 잡았다(`4537289`, 시험 `LOCALS_SOURCE`). 022 전부터 있던 것이다.
+
+## 결과
+
+### 022의 3단계 (코어 BFS, `bfs-run`, 탐색 하나의 중앙값)
+
+| 커밋 | 바꾼 것 | 시간 | 명령 | 사이클 |
+|---|---|---:|---:|---:|
+| `ddc8ba0` | 정수 가정, `pure_run_rest` | 14.6 ms | 5,780만 | 3,520만 |
+| `e8d213b` | 장부를 레지스터에 | 12.5 ms | 4,620만 | 3,030만 |
+| `4537289` | 작업량을 레지스터에, x30, 산술 다듬기 | 12.2 ms | 4,300만 | 3,030만 |
+| `1aa334d` | 정수로 미리 읽기, 칸의 마지막 셀도 | 8.6 ms | 4,170만 | 2,070만 |
+| `e9e0d22` | 간선 루프의 확인 셋을 덜기 | 7.4 ms | 3,840만 | 1,790만 |
+
+명령을 1,160만 줄인 장부(`e8d213b`)는 사이클을 500만 줄였고, 명령 320만을 줄인 `4537289`는 사이클이 그대로였다. 사이클을 쥔 것은
+간선 열의 미스였다(프로파일에서 사이클의 21%가 간선 열을 읽은 바로 뒤). 노드의 간선(약 40바이트)이 64바이트 줄을 넘는 일이 많아,
+첫 셀만 미리 읽던 것을 마지막 셀까지 넓히자 사이클이 3분의 2가 됐다.
+
+미리 읽기를 두고 끄며(`e9e0d22`, `2026-10-05-suite.jsonl`):
+
+| | Pure | `bfs.c` | `bfs.c` + 같은 미리 읽기 |
+|---|---:|---:|---:|
+| 미리 읽기 켬 | 7.58 ms | 9.89 ms | 7.29 ms |
+| 미리 읽기 끔 (`PURE_AHEAD=0`) | 15.7 ms | 10.0 ms | |
+
+022의 코어 통과 조건(같은 프로세스의 `bfs.c`보다 작다)은 글자로는 선다. 그러나 C가 하지 않는 기법으로 선 것이라 통과로 치지 않는다.
+022의 나머지 계열(HTTP, 지금의 앱, 기기 사이)은 재지 않고 기준을 023으로 옮겼다.
+
+### 묶음 (`5fffb63`, 11번의 중앙값)
+
+| 일 | Pure | C | Pure / C | 실행기만 / C |
+|---|---:|---:|---:|---:|
+| roots (실수, 나눗셈) | 22.9 ms | 6.86 ms | 3.3 | 19.3 |
+| knapsack (정수 DP) | 1,880 ms | 6.27 ms | 300 | 298 |
+| sort (키로 둔 리스트) | 128 ms | 27.0 ms | 4.8 | 4.7 |
+| join (희소한 키로 찾기) | 672 ms | 30.8 ms | 22 | 24 |
+| words (텍스트 키) | 455 ms | 38.3 ms | 12 | 12 |
+| matmul (실수 그리드) | 2,284 ms | 7.52 ms | 304 | 303 |
+| bfs (미리 읽기 없이) | 15.7 ms | 10.0 ms | 1.57 | 9.4 |
+
+모든 회차에서 Pure와 C의 답이 같다. 입력은 양쪽이 같은 MINSTD 수열로 이벤트 안에서 만든다.
+
+### 바닥과 뜻의 값 (`floor.c`, `2026-10-05-floor.jsonl`)
+
+| 일 | bare / C | hoisted / C | naive / C |
+|---|---:|---:|---:|
+| roots | 0.99 | 1.02 | 1.77 |
+| knapsack | 0.95 | 0.97 | 10.8 |
+| sort | 0.61 | 0.64 | 0.83 |
+| join | 0.95 | 0.91 | 1.57 |
+| words | 0.92 | 0.90 | 0.91 |
+| matmul | 0.96 | 0.95 | 12.1 |
+| bfs | 1.09 | 0.96 | 1.23 |
+| 기하 평균 | 0.91 | 0.90 | 2.6 |
+
+- bare는 Pure의 표현만, hoisted는 컴파일러가 남길 검사(범위가 증명하지 못하는 것, 루프마다 한 번으로 끌어낸 것), naive는 수를
+ binary64로 두고 뜻의 검사를 모두 제자리에 둔 것이다.
+- **sort와 words가 C보다 빠른 것은 C의 표준 라이브러리 몫이다**: `qsort`는 비교마다 함수를 부르고, `snprintf`는 정수 하나에 무겁다.
+ 깡연산의 몫으로 치지 않는다. 그것을 빼면(sort를 1.0으로) 기하 평균은 0.96이다.
+- join의 바닥은 처음 1.8이었다(색인의 칸에 키 8바이트, 위치는 따로). 칸에 키와 위치를 함께 두고 키를 4바이트로 좁혀 0.91이 됐다.
+
+## 판정
+
+- 022: 코어 BFS는 미리 읽기 덕으로만 C를 넘었다. 깡연산으로는 C의 1.57배다. 미달로 적는다.
+- 기준을 바꿨다: 묶음과 그 수치 기준을 023에 구현 전에 커밋했다(`ab4f8d3`, `b1bc98b`).
+
+## 다음
+
+- 023의 1단계: 블록을 SSA로 올리고 타입과 범위를 내며, IR을 해석하는 시험용 실행기로 모든 검사의 프로그램을 실행기와 견준다.
+- 기준의 긴장: 기하 평균 1.0은 sort와 words가 C의 라이브러리 몫으로 받쳐 준다. 일마다의 한계 1.25는 몇 가지에서 C에 질 수 있음을
+ 허락한다. 사용자의 판단에 따라 일마다 1.0으로 조이거나 sort를 인라인한 C와 견주도록 바꿀 수 있다.