From 9840d27a9e437de32256024a2fd6a3219a239815 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Wed, 30 Sep 2026 08:54:29 +0100 Subject: 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. --- docs/research/107-snapshot-nodes-reused.md | 141 +++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/research/107-snapshot-nodes-reused.md (limited to 'docs') 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) 이 변경이 처리량을 바꿀 까닭이 없다. 재지 않았다. -- cgit v1.2.3