aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-30 08:54:29 +0100
committereunhhu <[email protected]>2026-09-30 08:54:29 +0100
commit9840d27a9e437de32256024a2fd6a3219a239815 (patch)
treeeba57012bcdaac8e019d1421370a570a3e9f70d9 /docs
parent987d4d7aa08624e28d0ea18a1e66e359a0230c80 (diff)
downloadpure-9840d27a9e437de32256024a2fd6a3219a239815.tar.gz
pure-9840d27a9e437de32256024a2fd6a3219a239815.zip
Say what snapshots cost the owner, and what reusing their nodes gave (107)
Most of it was the allocator: leaves past glibc's thread cache, freed by whichever thread let the last snapshot go. jemalloc was 11-14% faster at more memory; reusing the nodes in Pure gave a median +9.4% over six pairs, 4-8% less CPU a transaction in all six, and 1.97x the state bytes resident where it was 2.18x. TPC-C in RAM is now 4.0x a tuned SQLite at W=1 and 3.5x at W=4; finding items is the owner's largest part now.
Diffstat (limited to 'docs')
-rw-r--r--docs/research/107-snapshot-nodes-reused.md141
1 files changed, 141 insertions, 0 deletions
diff --git a/docs/research/107-snapshot-nodes-reused.md b/docs/research/107-snapshot-nodes-reused.md
new file mode 100644
index 0000000..f9b385f
--- /dev/null
+++ b/docs/research/107-snapshot-nodes-reused.md
@@ -0,0 +1,141 @@
+# 107 — 스냅숏이 놓은 노드를 목록이 다시 쓴다
+
+2026-09-30. [106](106-tpcc-conditions-first.md)의 판정 2에서 이어진다. RAM에서 TPC-C는 소유자 스레드 하나가 병목이고, 표본으로 본
+소유자의 바쁜 시간 가운데 3분의 1이 스냅숏이 공유한 노드를 복사하고 놓는 데 갔다. 그 3분의 1이 무엇인지 나눠 보고 줄였다.
+
+기준(106의 표본에서 곧바로 이어진 일이라, 측정 전에 따로 커밋하지는 않았다):
+
+- **바꿀 결정.** 스냅숏에 딸린 비용을 코어 안에서 줄일지, 할당기를 바꿀지, 둘 다 하지 않을지.
+- **넣는 조건.** 번갈아 잰 쌍에서 처리량과 거래당 CPU가 나아진다. 상태 바이트에 대한 상주 메모리가 늘지 않는다. 붙든 스냅숏이
+ 읽는 값이 그대로다(테스트, ASan과 LSan).
+
+## 쉬운 말로
+
+- **스냅숏 때문에 소유자가 쓰던 시간의 70%는 복사가 아니라 할당이었다.** 그 몫의 표본 20개 가운데 할당기가 14개, 복사가 4개였다.
+ - TPC-C의 잎(약 1.3 KB)은 glibc의 스레드 캐시(1,032 B까지)에 들지 않는다. 그래서 malloc과 free가 매번 아레나의 잠금을 잡는다.
+ - 스냅숏을 마지막으로 놓는 쪽이 연결 스레드면, 그 스레드가 소유자의 아레나에 free한다. 소유자의 malloc과 같은 잠금을 다툰다.
+- **할당기를 jemalloc으로 바꾸면 11–14% 빨랐다.** 할당이 정말 비용이었다는 확인이다. 대신 상주가 상태 바이트의 2.17배에서
+ 2.24배로 늘었다.
+- **할당기를 바꾸지 않고, 스냅숏이 놓은 노드를 목록이 다시 쓰게 했다.**
+ - 목록에는 원래 제가 놓은 노드를 다음에 쓰는 자리(spare)가 있었다. 스냅숏이 놓은 노드는 거기 오지 않고 free됐다. 이제 온다.
+ - 번갈아 잰 여섯 쌍에서 처리량의 중앙값이 +9.4%다(다섯 쌍 +9.3–13.0%, 한 쌍 −2.5%).
+ - 거래당 CPU는 여섯 쌍 모두 4–8% 줄었다.
+ - 상주는 상태 바이트의 2.17–2.18배에서 1.97–1.98배로 줄었다. 빨라지면서 메모리도 덜 쓴다.
+- **TPC-C의 RAM 비교가 바뀌었다.** W=1에서 초당 40,976건으로 조율한 SQLite(10,164건)의 4.0배다. W=4에서는 35,838건, 3.5배다.
+ 106에서는 36,567건과 32,765건이었다.
+- 소유자 표본에서 스냅숏에 딸린 몫은 바쁜 시간의 33%에서 17%로 줄었다. 할당기 표본은 14개에서 7개가 됐다.
+
+## 무엇이 비쌌나
+
+106의 소유자 표본([원시](../../research/experiments/022-tpcc-grouped/2026-09-30-owner-stacks-linux-aarch64.txt)) 가운데
+스냅숏에 딸린 20개를, 가장 안쪽에서 부른 할당기와 그 호출자로 나눴다.
+
+| 가장 안쪽 | 표본 |
+|---|---:|
+| `malloc` ← `new_node`(목록이 복사할 노드를 새로 얻는다) | 9 |
+| `free` ← `pure_retired_free`(스냅숏이 놓인 뒤 그 노드를 돌려준다) | 4 |
+| `realloc` ← `retire`(앱의 retired 벡터가 스냅숏마다 16칸부터 다시 자란다) | 1 |
+| 복사 자체(`own` 안의 memcpy)와 그 밖 | 6 |
+
+같은 서버를 할당기만 바꿔 번갈아 돌렸다(RAM, W=1, 64터미널, 20초씩).
+
+| 할당기 | 초당 거래 | 거래당 CPU | 상주 / 상태 |
+|---|---:|---:|---:|
+| glibc | 37,280 | 48.2 µs | 2.17배 |
+| jemalloc | 42,323 | 43.4 µs | 2.24배 |
+| glibc, `tcache_max=4096` | 39,799 | 47.0 µs | 2.18배 |
+| glibc | 40,515 | 46.8 µs | 2.18배 |
+| jemalloc | 45,119 | 43.5 µs | 2.24배 |
+
+jemalloc은 glibc 바로 앞 실행보다 13.5%와 11.4% 빨랐다. glibc의 스레드 캐시를 키우는 설정은 아무것도 바꾸지 않았다(이 판의
+glibc는 1,032 B를 넘는 크기를 캐시하지 않는다). 같은 glibc 두 번이 8.7% 벌어진다. 이 기계에서 한 번의 실행은 ±5%쯤 흔들린다.
+
+## 한 것
+
+[`core/module-list.c`](../../core/module-list.c), [`core/module.c`](../../core/module.c) (`6c87722`):
+
+1. **retired 항목이 제 노드를 설명한다.** 노드의 공유 세대(`made`), 크기, 잎인지를 항목에 적는다. 스냅숏을 놓을 때 넘기는 일과
+ 되쓰는 일이 식은 노드를 읽지 않는다(전에는 넘길 때마다 노드의 `made`를 읽었다).
+2. **놓인 것은 앱으로 돌아온다.** 스냅숏을 마지막으로 놓는 스레드는 아무 스냅숏도 읽지 않는 것을 free하지 않는다. 대신 앱의
+ 레지스터(앱과 그 스냅숏들이 함께 쥔다)에 둔다. 레지스터의 스핀 잠금 아래이고, 전에도 넘기는 일은 이 잠금 아래였다.
+3. **소유자가 나눠 준다.** 스냅숏을 만들 때마다 소유자는 돌아온 것을 크기가 맞는 목록의 spare로 나눠 준다. 목록은 최근에 만든
+ 노드 수만큼 쥔다(돌아올 때마다 반으로 줄여 센다, 적어도 8개). 남는 것과 text는 free한다.
+4. **spare는 포인터 배열이다.** 노드 안에 끼운 연결이면 나눠 줄 때마다 식은 노드에 써야 한다.
+5. 앱의 retired 벡터는 칸을 두고 되쓴다.
+
+처음 판(커밋하지 않았다)은 spare를 노드 안의 연결로 두고, 돌아올 때마다 수요를 0으로 지웠다. 세 쌍에서 +7.4%에 그쳤다. 표본을
+보니 나눠 주는 고리가 식은 노드를 읽고 쓰느라 바빴고(표본 4개), 수요를 지우는 바람에 돌아온 것의 상당수를 free했다(표본 5개).
+위의 1, 3, 4가 그것을 고친 것이다.
+
+공유되지 않은 노드를 놓을 때(`drop_node`)는 전처럼 8개까지만 spare로 쥔다. 스냅숏을 만들지 않는 앱에서는 수요가 반으로 줄
+기회가 없기 때문이다.
+
+## 안전
+
+되쓰는 노드는 전에 free하던 바로 그것이다. 아무 스냅숏도 읽지 않는다는 판정(`pure_retired_pass`)은 바꾸지 않았다.
+
+- `snapshot_held`(이미 있던 테스트): 스냅숏 하나를 붙든 채 사건과 스냅숏을 4만 번 돌린 뒤, 붙든 스냅숏의 내용이 글자 하나 다르지
+ 않다.
+- `list_trees`: 무작위 변경 6,000번 사이에 복사본을 넷까지 붙들고, 가장 오래된 것을 놓으면 그 retired를 이제 목록이 되쓴다.
+ 붙든 복사본의 항목을 계속 확인한다. 아직 붙든 복사본의 retired를 되쓰게 바꾼 변이는 여기서 잡혔다.
+- `retired_come_back`(새 테스트): 복사본이 놓은 잎으로 다음 복사를 만든다(주소가 같다). 붙든 복사본은 제 값을 읽는다. 되쓰지
+ 않게 바꾼 변이는 여기서 잡혔다.
+- 모듈 테스트는 ASan과 LSan으로도 돈다. Pure의 `make check`, Vega와 aup의 검사가 통과했다.
+
+## 결과
+
+[원시 결과](../../research/experiments/023-node-reuse/)와 [`runs.sh`](../../research/experiments/023-node-reuse/runs.sh),
+표는 `summary.py`가 만든다. RAM(tmpfs), W=1, 64터미널, 20초씩, 전(`c9fda91`)과 후(`6c87722`)를 번갈아.
+
+| 쌍 | 초당 거래(전 → 후) | 거래당 CPU(µs) | 상주 / 상태 |
+|---|---:|---:|---:|
+| 1 | 39,304 → 38,305 (−2.5%) | 47.4 → 45.6 | 2.18 → 1.97 |
+| 2 | 38,863 → 43,811 (+12.7%) | 47.0 → 43.8 | 2.18 → 1.97 |
+| 3 | 39,952 → 43,732 (+9.5%) | 47.0 → 43.9 | 2.18 → 1.97 |
+| 4(후 먼저) | 37,931 → 41,485 (+9.4%) | 48.4 → 44.7 | 2.17 → 1.98 |
+| 5(후 먼저) | 36,817 → 41,617 (+13.0%) | 48.2 → 45.0 | 2.17 → 1.97 |
+| 6(후 먼저) | 38,679 → 42,292 (+9.3%) | 47.9 → 44.7 | 2.18 → 1.97 |
+
+첫 쌍의 "후" 실행에서는 소유자가 코어의 0.83만 썼다(다른 실행은 0.87–0.91). 그 실행에서도 거래당 CPU는 3.8% 줄었다.
+
+**106의 RAM 행렬을 다시.** SQLite는 그대로다.
+
+| | W=1 | W=4 |
+|---|---:|---:|
+| Pure, 16터미널 | 38,320 (106: 35,672) | 33,057 (32,765) |
+| Pure, 64터미널 | 40,976 (36,567) | 35,838 (32,255) |
+| 조율한 SQLite의 최고 | 10,164 | 10,168 |
+| Pure의 상주 / 상태 | 2.01 (2.23) | 1.94 (2.10) |
+
+같은 거래 2만 건 뒤 세 쪽의 표가 모두 같았고, 모든 실행 뒤 일관성 조건이 성립했다.
+
+**소유자 표본**(RAM, W=1, 64터미널, 80개씩).
+
+| 소유자가 하던 일 | 106(전) | 지금 |
+|---|---:|---:|
+| 항목 찾기 | 24 | 20 |
+| 요청 기다리기 | 19 | 17 |
+| 스냅숏이 공유한 노드를 복사하기 | 15 | 10 |
+| 스냅숏 게시와 놓기 | 5 | 1 |
+| 해석기 | 5 | 10 |
+| 항목 바꾸기 | 3 | 10 |
+| 항목 읽기 | 3 | 0 |
+| 답 넘기기 | 3 | 7 |
+| 저장과 그 밖 | 3 | 5 |
+| (할당기가 가장 안쪽인 표본) | (14) | (7) |
+
+스냅숏에 딸린 몫(복사와 게시)이 바쁜 시간의 33%에서 17%가 됐다. 남은 할당기 표본 7개 가운데 4개는 `new_node`의 malloc이다.
+목록이 자라 새 잎이 필요하거나, 연결 스레드가 스냅숏을 오래 붙들어 돌아온 것이 늦을 때다. 칸마다의 차이는 ±3–4개가 흔들림이다.
+
+## 판정
+
+- 넣었다. 처리량 중앙값 +9.4%, 거래당 CPU −4–8%, 상주/상태 −10%. jemalloc의 이득(11–14%)에 가깝고, 메모리는 jemalloc과
+ 반대로 줄었다. Pure는 할당기를 바꾸지 않는다.
+- 106의 기준으로 소유자는 여전히 병목이다. W=4, 64터미널에서 코어의 0.92를 쓰고, 처리량은 16터미널보다 8.4% 많다(기준 10%).
+
+## 다음
+
+- 소유자의 바쁜 시간은 이제 찾기 32%, 바꾸기·복사·해석기 각 16%, 답 넘기기 11%, 저장 8%다. 가장 큰 몫은 B+트리를 내려가는
+ 찾기다. 한 사건 안에서 같은 목록을 여러 번 찾는다(New-Order는 재고를 열 번). 찾은 자리를 기억하는 손가락은 읽기에만 있다.
+- 남은 복사는 저장마다 스냅숏을 게시하는 한 든다. 줄이려면 잎을 통째로 복사하지 않는 방법이 필요하다.
+- SD 카드에서는 소유자가 코어의 7–15%만 쓰므로(106) 이 변경이 처리량을 바꿀 까닭이 없다. 재지 않았다.