aboutsummaryrefslogtreecommitdiffstats
path: root/docs/research
diff options
context:
space:
mode:
Diffstat (limited to 'docs/research')
-rw-r--r--docs/research/121-computing-faster.md160
1 files changed, 160 insertions, 0 deletions
diff --git a/docs/research/121-computing-faster.md b/docs/research/121-computing-faster.md
new file mode 100644
index 0000000..db99568
--- /dev/null
+++ b/docs/research/121-computing-faster.md
@@ -0,0 +1,160 @@
+# 121 — 계산을 빠르게
+
+2026-10-04. [020](../design/020-computing-faster.md)의 1단계(키로 찾기를 싸게)를 잰다. 기준은 구현하기 전에 적어
+커밋했다(`1d8c1f1`). 처음 만든 것은 `9484700`이고, 잰 뒤에 손가락을 하나로 되돌리고(`316d701`) 루프의 끝 비교를 접었다
+(`dde73e4`). 판정은 `dde73e4`로 한다. 만들면서 설계와 달라진 것은 020의 "1단계에서 달라진 것"에 적었다.
+
+## 쉬운 말로
+
+- **BFS(노드 10만, 간선 100만) 한 번이 397 ms에서 187 ms가 됐다(2.1배).** 기준(200 ms 이하)을 채웠다. 같은 일을 CPython은 441 ms,
+ C는 9 ms에 한다. 이제 CPython보다 2.4배 빠르고, C의 약 20배다.
+- **나눠 말하면**: 약 395 ms에서 204 ms까지는 길을 바꾼 것이다. 이벤트의 키 있는 리스트가 키를 비트(작은 정수일 때)나 표로도 갖고
+ (`seen.has`가 트리를 내려가지 않는다), 키로 넣기를 한 번 내려가서 한다. 204 ms에서 185 ms까지는 거의 다 컴파일러다: 리스트의
+ 파일(`module-list.c`) 하나를 `-O3`로 빌드했다. 자식 찾기를 세기로 바꾼 것은 `-O3`와 짝일 때만 싸다.
+- **지금의 앱도 빨라졌다**: TPC-C가 4.1%, 은행 질의 넷이 1.0–5.1%. 명령 수는 일곱 질의 모두 기준(늘면 1% 안) 안이다(가장 는 것이
+ +0.24%, 준 것은 −10.8%까지).
+- **처음 만든 것은 명령 수의 기준을 어겼다**(`field` +2.94%). 하나씩 바꾼 빌드로 세어 보니 원인은 스레드마다 손가락을 넷으로 늘린
+ 것이었다. 그것은 BFS도 빠르게 하지 않았다. 하나로 되돌렸다(원래 하나였다).
+
+## 결과
+
+원시 결과와 명령은 [036-faster-loops](../../research/experiments/036-faster-loops/)에 있다(`runs.sh`, `bfs-run.c`, `pairs.py`,
+`same_answers.py`, `bfscount.py`). 전은 `1d8c1f1`(기준의 커밋, 코드는 `313074a`와 같다), 후는 `dde73e4`다. 처음의 후(`9484700`)로 잰
+것은 `2026-10-04-four-*`에 있다.
+
+### BFS 한 번, 코어만 (`bfs-run`)
+
+`bfs-run.c`: 033의 `graph.aup`를 코어로만 돌린다(호스트도 저장소도 없다). 무작위 그래프(노드 100,000, 간선 999,937, 시드 121)를
+행으로 넣고, 정해진 출발 일곱 곳에서 BFS를 한 번씩 하고 그 중앙값을 낸다. 전과 후를 번갈아 여섯 쌍(`runs.sh core`).
+
+| 쌍 | 1 | 2 | 3 | 4 | 5 | 6 | 중앙값 |
+|---|---:|---:|---:|---:|---:|---:|---:|
+| 전 | 436.1 | 397.1 | 396.2 | 393.5 | 397.5 | 400.6 | 397.3 ms |
+| 후 | 186.5 | 188.0 | 186.5 | 186.7 | 188.1 | 187.6 | 187.2 ms |
+
+처음의 후(`9484700`, 손가락 넷)는 여섯 쌍의 중앙값이 185.3 ms(전 396.2 ms)였다.
+
+어디서 줄었는가(같은 `bfs-run`, 한 번씩 쟀다, 만든 차례대로):
+
+| 단계 | 중앙값 |
+|---|---:|
+| 전 | 394 ms |
+| 소속 표(열린 주소의 표만)와 손가락 | 286 ms |
+| 작은 정수 키는 비트로 | 207 ms |
+| 키로 넣기를 한 번 내려가서(`pure_list_add`) | 204.5 ms |
+| 항목 필드의 text 여부를 로드할 때, 손가락 넷과 앞으로 옮기기 | 약 206 ms |
+| 재귀 없는 루프 | 203.7 ms |
+| 자식 찾기를 세기로 | 204.7 ms |
+| 잎의 울타리 | 206.7 ms (뺐다) |
+| `module-list.c`를 `-O3`로 | 185.7 ms |
+| 손가락을 하나로 되돌리기 (`316d701`) | 183.2–184.1 ms |
+| 루프의 끝 비교를 접기 (`dde73e4`) | 186.5–189.8 ms |
+
+- 표만으로 286 ms였던 것은 키 하나를 찾는 데 캐시 미스가 하나 남기 때문이다(10만 키의 표는 2 MiB). 노드 번호는 작은 정수라 비트로
+ 12.5 KB이고 L1/L2에 든다.
+- 마지막 줄은 일을 줄이고 시간을 늘렸다. `bfscount.py`(025의 `pcount`)로 세면 탐색 하나에 명령은 888M에서 867M으로 2.4% 줄고
+ 사이클은 440M에서 448M으로 1.9% 는다. 같은 바이너리에서는 늘 그렇다(번갈아 다섯 번). 일이 아니라 코드의 자리와 분기 예측의 몫이다.
+ 일이 적고 코드가 짧은 쪽을 남겼다(은행 질의의 명령 수도 줄었다: `field` +0.80%에서 +0.24%).
+- 남은 187 ms의 대부분은 간선의 B+트리를 걷는 메모리 미스(새 노드마다 잎과 그 위의 몇 단)와 실행기의 명령 디스패치(명령 하나에
+ 약 6 ns)다. 코어만의 미세 측정(같은 빌드, 반복 하나에): 세기만 하는 반복 29에서 24 ns, 리스트 걷기 45에서 41 ns, 덧붙이기
+ 163에서 128 ns.
+
+### BFS 한 번, HTTP로 (033의 `bfs.py`)
+
+118과 같다: 무작위 그래프(노드 100,000, 간선 1,000,000, 시드 118)를 `pure-serve`(RAM, 코어 0–2)에 넣고 출발 다섯 곳에서 BFS를 한
+이벤트씩, C와 CPython이 같은 탐색을 한다(`runs.sh bfs before`, `runs.sh bfs after`). 세 쪽의 거리와 닿은 노드 수(99,996)가 같다.
+
+| 중앙값 | C | CPython | Pure 전 | Pure 후 |
+|---|---:|---:|---:|---:|
+| BFS 한 번 | 9.0 ms | 449 ms | 397.5 ms | **190.9 ms** |
+
+Pure는 C의 44배에서 21배로, CPython의 0.88배에서 0.43배로 왔다. HTTP로 잰 것과 코어만 잰 것의 차이(약 4 ms)는 요청과 저장이다.
+
+### 지금의 앱: TPC-C와 은행 질의, 명령 수
+
+`runs.sh pairs`(033과 같다: TPC-C는 RAM, W=1, 64터미널, 20초; 은행 질의), 전과 후를 번갈아 여섯 쌍. `runs.sh counts`: 025의
+`count.py`로 은행 질의 일곱의 묻기 한 번당 사용자 모드 명령 수(전, 후, 전, 후).
+
+처음의 후(`9484700`, 손가락 넷):
+
+| | 전 | 후 | 바뀜 |
+|---|---:|---:|---:|
+| TPC-C 중앙값(초당) | 43,181 | 44,729 | +3.6% (여섯 쌍 모두 +2.8–+5.2%) |
+| per_branch | 5.29 ms | 5.25 ms | −0.9% |
+| in_credit | 0.845 ms | 0.825 ms | −2.4% |
+| large | 0.38 ms | 0.37 ms | −2.6% |
+| ranks | 6.80 ms | 6.73 ms | −1.0% |
+| 명령 수: field | | | **+2.94%** |
+| 명령 수: found | | | −5.8% |
+| 명령 수: 나머지 다섯 | | | −1.5–+0.9% |
+
+**명령 수의 기준(늘면 1% 안)을 `field`가 어겼다.** `field`는 거래 2만 행 가운데 `delta > 0`인 행의 `aid`를 읽는 표다(행마다
+셀 하나). 두 빌드의 답은 일곱 질의 모두 바이트까지 같다(같은 일을 한다). 샘플로 본 함수의 몫은 바뀌지 않은 함수까지 움직여서
+(`put_bytes` 9.7%에서 6.9%) 이 크기를 가르지 못했다. 그래서 하나씩 바꾼 빌드로 셌다(전 대비):
+
+| 빌드 | field | found | large |
+|---|---:|---:|---:|
+| 후(손가락 넷, `-O3`) | +2.94% | −5.8% | +0.3% |
+| 후, `module-list.c`를 `-O2`로 | +3.61% | +18.7% | +8.0% |
+| 후, 손가락 없이 | +7.51% | −4.2% | +0.6% |
+| 옛 `module.c` + 새 리스트 | +2.04% | −6.4% | −0.2% |
+| 그 위에서 `-O2` | +2.71% | +18.1% | +7.5% |
+| 그 위에서 옛 이분 탐색 | +2.29% | −6.3% | +0.2% |
+| 그 위에서 손가락 하나 | **−0.31%** | **−9.8%** | **−3.0%** |
+
+- 옛 코드에도 손가락이 하나 있었다(스레드마다 마지막 잎). 넷(리스트마다 하나, 맞은 것을 앞으로)은 그것보다 명령이 더 들었다.
+ 코어만의 BFS도 하나로 빠르다(세 번의 중앙값: 하나 183.2 ms, 둘 185.0 ms, 넷 184.8 ms). 그래서 하나로 되돌렸다(`316d701`).
+- `-O3`는 자식 찾기를 세기로 바꾼 것과 짝이다: 가지 32개를 모두 비교하는 세기는 벡터가 될 때 싸고, `-O2`이면 `found`가 18%
+ 늘어난다.
+- 옛 `module.c`로 되돌리면 `field`가 0.9%포인트 준다. 실행기의 재귀 없는 루프는 명령마다 비교 하나(`pc == loop_end`)를 더한다.
+
+**`dde73e4`로 다시 쟀다**(여섯 쌍, 명령 수는 전과 후를 네 번씩):
+
+| | 전 | 후 | 바뀜 |
+|---|---:|---:|---:|
+| TPC-C 중앙값(초당) | 42,996 | 44,771 | **+4.1%** (여섯 쌍 모두 +3.4–+6.4%) |
+| per_branch | 5.26 ms | 5.21 ms | −1.0% |
+| in_credit | 0.845 ms | 0.83 ms | −1.8% |
+| large | 0.39 ms | 0.37 ms | −5.1% |
+| ranks | 6.78 ms | 6.68 ms | −1.5% |
+
+| 명령 수(중앙값) | 바뀜 | 네 쌍 |
+|---|---:|---|
+| per_branch | −0.97% | −0.84, +0.30, −1.32, −1.43 |
+| in_credit | −0.02% | +0.20, +2.21, −0.62, −0.82 |
+| large | −3.33% | 넷 모두 −3.33 |
+| ranks | +0.12% | +0.19, +0.89, −0.09, −0.16 |
+| none | +0.17% | 넷 모두 +0.17–+0.18 |
+| field | +0.24% | 넷 모두 +0.24 |
+| found | −10.84% | 넷 모두 −10.84–−10.85 |
+
+in_credit의 둘째 쌍(+2.21%)은 한 쌍으로는 기준을 넘는다. 그 질의는 같은 바이너리로도 실행마다 흔들린다: 전의 네 번이 3,443,329에서
+3,503,100(1.7%)까지다. 네 쌍의 중앙값으로 판정한다.
+
+### 검사
+
+- `test-module`의 `key_sets`: 키를 비트와 표로 갖는 리스트가 무작위 12만 걸음(넣기 4만 5천, 지우기 2만, 키 바꾸기 1만 3천, 되돌린 것
+ 9,799)의 매 걸음에 모델 배열과 같은 답을 낸다(`has`, 키로 넣기의 자리와 있음, 키로 찾기, 위치로 읽기, 차례로 읽기). 키가 비트에서
+ 표로 일곱 번 바뀌고, 비우기 12번(64 KiB 넘는 비트는 버리고 다시), 표의 사슬 가운데서 지우기(뒤를 당긴다), 음수와 분수 키, `-0`.
+ 스냅숏의 사본(같은 리스트, 다른 판)과 다른 리스트 넷을 사이사이 읽어 손가락이 다른 리스트나 다른 판의 잎을 내주지 않는다.
+- `keyed_journal`: 저장되는 키 있는 리스트의 키로 넣기(이미 있는 키 1,156번, 되돌린 이벤트 607번, 키 바꾸기와 지우기 포함)가 적은
+ 기록을, 통째로 저장한 항목 뒤에 다시 하면 그 리스트다(재시작 59번).
+- 두 검사가 잡는지 일부러 망가뜨려 봤다: 표에서 지울 때 뒤를 당기지 않으면 `key_sets`가, 손가락이 판을 보지 않으면 공유 검사가,
+ 키로 넣기가 기록의 길이를 더하지 않으면 `keyed_journal`이 멈춘다.
+- 두 빌드(전과 처음의 후)의 은행 질의 일곱의 답이 바이트까지 같다(`same_answers.py`).
+- Pure의 `make check`(ASan 포함), aup와 Vega의 모든 검사가 초록이다(`dde73e4`).
+
+## 판정
+
+- **020의 1단계는 들어간다.** BFS는 기준(200 ms 이하) 안이고(187.2 ms, 2.1배), TPC-C와 은행 질의의 시간은 빨라졌고, 명령 수는
+ 네 쌍의 중앙값으로 일곱 모두 1% 안이다.
+- 처음 만든 것(`9484700`)은 명령 수의 기준을 어겼다(`field` +2.94%). 원인을 하나씩 바꾼 빌드로 찾아(손가락 넷) 되돌린 뒤 다시 쟀다.
+- 줄어든 약 210 ms 가운데 약 20 ms(204에서 약 185)는 컴파일러의 `-O3`다. 그것은 세기로 바꾼 자식 찾기와 짝이라 `-O2`로 내리면
+ 명령 수가 오히려 는다(`found` +18%).
+
+## 다음
+
+- 남은 시간은 간선의 B+트리를 걷는 메모리 미스와 명령 디스패치다. 간선의 조각을 잎 단위로 걷는 것(행마다 `pure_list_get`이
+ 아니라 조각의 열을 한 번에), 이벤트의 리스트를 B+트리가 아닌 배열로 두는 것(이벤트의 리스트는 나누지도 저장하지도 않는다)을
+ 잴 수 있다. 측정이 요구할 때.
+- 020의 2단계: 긴 보기를 연결 스레드 밖에서([122](122-views-off-the-connection-threads.md)).