From 57ca99e5f4cdce7923e55347ed505e3ac43c3055 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Sun, 4 Oct 2026 15:06:22 +0100 Subject: Say what views that compute cost the writes beside them (018 stage 3, research 119) --- docs/research/119-views-that-compute.md | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/research/119-views-that-compute.md (limited to 'docs/research/119-views-that-compute.md') diff --git a/docs/research/119-views-that-compute.md b/docs/research/119-views-that-compute.md new file mode 100644 index 0000000..313fe10 --- /dev/null +++ b/docs/research/119-views-that-compute.md @@ -0,0 +1,82 @@ +# 119 — 계산하는 보기 + +2026-10-04. [018](../design/018-lifetimes-and-computation.md)의 3단계(보기의 본문)를 잰다. 첫 기준은 구현하기 전에 적어 +커밋했다(`4d93488`). 첫 측정에서 그 하나가 처음부터 채울 수 없는 조건이었음을 보고, 다시 재기 전에 두 번째 기준을 적어 +커밋했다(`94dc1c7`). + +## 쉬운 말로 + +- aup의 보기가 이제 계산한다: `view distance(start: u32, goal: u32) { … return … }`. 본문은 상태를 바꾸지 않고, 자기 `let`과 + 자기 리스트만 바꾼다. 스냅숏을 읽는 연결 스레드에서 돌아서 소유자 스레드를 막지 않는다. +- **같은 탐색을 이벤트로 하면 쓰기가 멈춘다(0.2%). 보기로 하면 쓰기는 계속된다.** 다만 탐색이 CPU를 쓰는 만큼 쓰기는 줄어든다: + 코어 셋에서 연결 둘이 쉬지 않고 탐색하면 쓰기가 30%로 준다. 같은 탐색을 다른 프로세스가 같은 코어에서 해도 69%로 준다. +- **처음 적은 기준 하나는 채우지 못했다**: "보기를 묻는 동안 쓰기 처리량이 95% 이상". 쓰기가 이미 코어를 다 쓰고 있어서 처음부터 + 채울 수 없는 조건이었다. 다시 적은 기준(쓰기의 p99가 CPU만 나눌 때의 2배를 넘지 않는다)은 채웠다(1.51배). +- **연결 스레드가 막는 것이 꼬리에 남는다.** 보기를 계산하는 연결 스레드는 그동안 자기의 다른 연결에 답하지 못한다. 쓰기의 최대 + 지연이 보기 하나의 길이(약 100–190 ms)까지 간다. 그리고 두 연결이 같은 연결 스레드에 떨어지면(커널이 정한다) 두 탐색이 차례로 + 돈다: `--threads 2`에서 세 라운드 가운데 두 번이 그랬다. 그래서 "연결 스레드 둘에서 1.6배"는 중앙값으로 채우지 못했다(1.0배, + 흩어진 라운드에서는 2.0배). +- 다음은 긴 보기를 연결 스레드 밖으로 넘기는 것이다. + +## 결과 + +원시 결과와 명령은 [034-view-bodies](../../research/experiments/034-view-bodies/)에 있다(`views.py`, `graph.aup`). + +**방법.** `pure-serve`(연결 스레드 셋, 저장소는 RAM, 코어 0–2)에 `graph.aup`: 노드 2만 개, 간선 10만 개의 무작위 그래프(시드 119)와 +`distance` 보기(본문이 BFS)와 점수 입력(`score`). 부하 도구(코어 3)가 16연결로 점수를 10초 동안 보낸다. 넷을 번갈아 여섯 라운드: +쓰기만(writes), 연결 둘이 같은 호스트의 `distance`를 쉬지 않고 물으며(both), 그 둘이 같은 코어의 다른 호스트에 물으며(apart: CPU만 +나눈다), 그 둘이 같은 탐색을 이벤트로 보내며(events: 소유자에서). 묻는 쪽은 미리 고른 출발 노드 64개에서 묻고, 모든 답을 그 +출발들로 미리 만든 모델과 견준다(첫 측정은 답을 볼 때 모델을 만들어 Python을 쟀다: 아래). + +| 중앙값 | 쓰기(초당) | 쓰기 p50 | 쓰기 p99 | 쓰기 최대 | 보기(초당) | +|---|---:|---:|---:|---:|---:| +| writes | 123,502 | 116 µs | 256 µs | 5.6 ms | — | +| both | 36,809 (29.8%) | 152 µs | 1,232 µs | 98–188 ms | 43 또는 22 | +| apart | 85,179 (69.0%) | 144 µs | 816 µs | 6–9 ms | 22 또는 40 | +| events | 187 (0.2%) | 82 ms | 94 ms | 86–139 ms | 25 | + +- both와 apart는 라운드마다 크게 흔들린다(both 25,320–85,461, apart 54,757–90,794). 두 묻는 연결이 같은 연결 스레드에 떨어진 라운드는 + 보기가 초당 22번(차례로), 다른 스레드에 떨어진 라운드는 43번(나란히)이고, 쓰기는 그 반대로 움직인다. CPU를 나누는 몫이 라운드마다 + 다르다. +- 쓰기 p99는 both가 apart의 1.51배다(두 번째 기준 2배 안). 최대 지연은 다르다: both는 보기 하나의 길이만큼, apart는 몇 ms다. 보기를 + 계산하는 연결 스레드에 붙은 쓰기 연결이 그동안 기다린다. 그런 쓰기는 1% 미만이다. +- events에서는 탐색 하나(약 80 ms)마다 소유자가 쓰기를 받지 못한다. + +**연결 스레드의 수.** 묻는 연결 둘, 각각 세 라운드(`threads-1`, `threads-2`). + +| | 라운드 | 중앙값 | +|---|---|---:| +| `--threads 1` | 23.6, 23.6, 23.7 | 23.6 | +| `--threads 2` | 46.8, 23.7, 23.7 | 23.7 (1.0배) | +| `--threads 3`(위의 사전 측정, 한 라운드) | 묻는 연결 하나 23.2, 둘 46.4 | 2.0배 | + +`--threads 2`의 둘째와 셋째 라운드에서 두 연결은 같은 스레드에 떨어졌다(`SO_REUSEPORT`가 연결을 나눈다). 보기 하나는 약 43 ms다. + +**첫 측정**(`2026-10-04-views-first.jsonl`). 쓰기는 44.6%, apart 46.0%, 보기는 묻는 연결 하나 13.5, 둘 23.3(1.73배)이었다. 묻는 쪽이 +답마다 새 출발의 모델을 Python으로 만들어서(약 30 ms), 보기의 처리량이 아니라 Python을 쟀다. 쓰기를 줄인 탐색의 양도 그만큼 +적었다. 결과는 두 번째 측정(위)을 쓴다. + +**검사.** +- `test-module`의 `view_bodies`: 본문의 지역 칸과 세는 반복, 걷기로 채우는 키 있는 이벤트의 리스트가 평가마다 새로 시작한다(앱에서 + 두 번). 끝나지 않는 반복은 `work`, 예산을 넘는 리스트는 `memory`로 값이 없고 상태는 그대로다. 한 스냅숏을 스레드 셋이 2,000번씩 + 동시에 읽어도 답이 맞고 저마다의 리스트가 비어서 끝난다. 상태를 쓰는 본문, 저장되는 리스트에 넣는 본문, 반복 밖의 `break`는 로드가 + 거절한다. +- Pure `tests/check.py`의 `server_view_bodies`: aup의 `routes.aup`를 띄워, 64연결이 동시에 무작위 인자로 물은 `distance` 1,280개가 + 모두 모델과 같다. `reach`(인자 없는 본문)가 모델과 같다. `--work 2000`이면 422 `work`, `--memory 2000`이면 503 `memory`다. +- aup `tests/check.py`: `computing`의 `reach`가 무작위 그래프 20개의 모든 단계에서 모델과 같다. 진단 8건. + +## 판정 + +- **018의 3단계는 들어간다.** 첫 통과 조건(쓰기 95%)은 채우지 못했다. 그 조건은 쓰기가 코어를 다 쓰는 동안에는 채울 수 없었다. + 두 번째 조건(쓰기 p99, CPU만 나누는 것의 2배 안)은 채웠다. 들이는 근거는 018에 적은 대안과의 비교다: 같은 계산을 핸들러에서 + 하면 쓰기가 0.2%로 멈춘다. 보기로 하면 CPU를 나누는 만큼만 준다. +- 연결 스레드 수의 기준(둘에서 1.6배)은 중앙값으로 채우지 못했다. 보기가 연결 스레드에서 돌고, 연결이 어느 스레드에 떨어질지는 + 커널이 정하기 때문이다. + +## 다음 + +- **긴 보기를 연결 스레드 밖으로.** 본문이 있는 보기(또는 실행량이 큰 보기)를 연결 스레드가 아니라 보기의 스레드들에 넘겨 계산하고 + 답만 돌려받는다. 그러면 연결 스레드가 막는 꼬리(쓰기 최대 지연)가 없어지고, 보기는 연결의 자리와 상관없이 나란히 돈다. 기준을 + 적고 잰다. +- 다음 작업인 SSE(보기의 변화를 미는 것)는 바뀔 때마다 보기를 다시 계산한다. 본문이 있는 보기를 미는 비용은 위의 보기의 스레드와 + 함께 다룬다. -- cgit v1.2.3