# 110 — i64가 수의 경로에 더한 것: 시간 대신 명령을 센다 2026-09-30. [016](../design/016-bool-and-i64.md)의 2단계(`i64`)가 수의 경로를 느리게 하지 않는지 잰다. 기준은 구현하기 전에 016에 적어 커밋했다(`9a03e72`). 모두 초록이고, 은행 질의([091](091-queries.md))와 TPC-C(RAM, W=1)를 전과 후 번갈아 여섯 쌍 재서 중앙값이 3% 넘게 느려지지 않는다. 전은 `9a03e72`(i64 전, bool까지)의 pure-serve다. 두 쪽이 같은 바이트의 모듈을 돌린다. i64 전과 후의 aup가 `tpcc.aup`, `bank.aup`, 은행의 질의 넷, 예제 18개를 같은 바이트로 빌드하므로 다른 것은 런타임뿐이다. ## 쉬운 말로 - **기준은 통과했다.** TPC-C 처리량의 중앙값 −0.6%, 은행 질의 0.0%에서 +2.2%. 방해받은 실행은 없었다. - **그런데 순위 질의가 여섯 번 모두 느렸다.** 까닭을 찾다가, 이 Pi에서 시간 비교는 3% 안쪽을 가리지 못한다는 것을 확인했다. 같은 두 빌드를 다시 재면 차이가 바뀌고, 셋을 둘씩 재면 앞뒤가 맞지 않는다. 아무것도 바꾸지 않은 질의가 3.5% "빨라지기도" 했다. 프로세스마다, 빌드마다 코드와 데이터가 놓이는 자리가 달라서다. - **실행한 명령의 수는 흔들리지 않는다.** 한 스레드로 실은 이체의 표를 읽는 질의는 프로세스가 달라도 명령이 0.03% 안에서 같다. 이 Pi에는 perf가 없어서 `perf_event_open`만으로 서버의 명령을 세는 도구와, 명령 2만 개마다 어디를 실행하는지 뜨는 샘플러를 만들었다. - **명령으로 보니 둘은 달랐다.** 순위 질의는 명령이 0.4% 늘었을 뿐이다(느려진 것은 자리 탓이다). 반대로 지점별 합계는 명령이 3.6% 늘었는데 시간에는 드러나지 않았다. 표는 줄마다 명령 20개가 늘었다. - **두 곳을 고쳤다**(`fcc1f67`). i64 폴드는 잎 단위로 돌지 않는데도 잎 단위의 폴드가 항목마다 i64인지 물었다. 그 길은 수만 받게 했다. 그리고 칸의 종류가 하나 늘자 칸을 읽는 함수가 인라인되지 않아 항목을 읽을 때마다 호출이 하나 생겼다. 수·bool· i64의 칸은 8바이트를 그대로 읽게 해 분기가 전처럼 하나가 되고 다시 인라인된다. 늘어난 명령이 절반쯤 줄었다(표 +1.3%, 지점별 합계 +1.2%). ## 결과 원시 결과와 명령은 [025-i64-cost](../../research/experiments/025-i64-cost/)에 있다(`runs.sh`, `ab.py`, `ab-sessions.sh`, `count.py`, 그리고 도구 `pcount.c`와 `psample.c`). **여섯 쌍**(TPC-C는 64터미널 20초, 은행 질의는 10만 계좌에 질의마다 15번 물은 중앙값. 후는 `fc65d66`). 부하 평균 0.4–1.8, 소유자의 코어 몫은 모든 실행에서 0.90–0.92였다. | 쌍 | 초당 거래(전 → 후) | 거래당 CPU(µs) | 소유자의 코어 몫 | |---|---:|---:|---:| | 1 | 42,603 → 42,974 (+0.9%) | 44.2 → 44.9 | 0.90 → 0.90 | | 2 | 44,695 → 43,683 (−2.3%) | 43.3 → 43.7 | 0.92 → 0.91 | | 3 | 43,401 → 44,502 (+2.5%) | 44.4 → 44.0 | 0.90 → 0.92 | | 4 | 44,200 → 43,816 (−0.9%) | 43.0 → 44.5 | 0.90 → 0.91 | | 5 | 44,277 → 43,592 (−1.5%) | 44.1 → 44.6 | 0.91 → 0.91 | | 6 | 43,843 → 44,342 (+1.1%) | 43.7 → 43.9 | 0.91 → 0.91 | 중앙값은 44,022 → 43,750(−0.6%)이고, 거래당 CPU는 43.9 → 44.3 µs(+0.8%)다. | 은행 질의 | 중앙값(ms, 전 → 후) | |---|---:| | 지점별 합계(`per_branch`) | 5.27 → 5.30 (+0.6%) | | 흑자 계좌 수(`in_credit`) | 0.84 → 0.84 (0.0%) | | 큰 이체의 조인(`large`) | 0.38 → 0.385 (+1.3%) | | 순위(`ranks`) | 6.69 → 6.84 (+2.2%, 여섯 번 모두 후가 느림) | **시간의 흔들림.** 두 서버를 함께 띄우고 같은 계좌를 실은 뒤 질의를 번갈아 물었다(`ab.py`, 12라운드, 라운드마다 30번). 한 쪽에 프로세스 하나씩이다. | 질의 | 9a03e72 → fc65d66 | 9a03e72 → 600f576 | 600f576 → fc65d66 | 9a03e72 → fc65d66 (다시) | |---|---:|---:|---:|---:| | `per_branch` | +1.6% | +0.1% | 0.0% | +1.8% | | `in_credit` | −0.1% | +4.7% | −6.5% | +0.7% | | `large` | +1.4% | −3.1% | 0.0% | +1.7% | | `ranks` | +2.8% | +1.6% | −1.2% | +2.8% | 가운데 둘을 더하면 오른쪽 끝이 나와야 하지만 맞지 않는다(`ranks`는 +0.4%여야 할 것이 +2.8%). `600f576`은 값 칸을 겹치기만 한 커밋이다. 한 프로세스가 가진 자리의 운이 그 비교 전체에 실린다. 그래서 세션마다 새 프로세스를 띄워 여덟 번 쟀다 (`ab-sessions.sh`). `per_branch` −0.2%, `in_credit` −3.5%, `large` +1.0%, `ranks` +1.2%(여덟 번 모두 후가 느림, +0.8%에서 +2.6%). 흑자 계좌 수는 명령이 0.6% 늘었는데(아래) 시간은 3.5% 줄었다. 이 크기의 차이는 시간으로는 가릴 수 없다. **명령 수**(`count.py`, 서버의 모든 스레드의 사용자 모드 명령, 한 번 물을 때. 새 프로세스 둘의 평균, 부하와 상관없다). `none`, `field`, `found`는 이체 2만 건 위의 표다. 남는 줄이 없는 것, 1만 줄에 필드 하나, 1만 줄에 계좌를 키로 찾아 읽는 것이다. | 질의 | 9a03e72 | fc65d66 | fcc1f67 | |---|---:|---:|---:| | `per_branch` | 26.67M | 27.64M (+3.6%) | 27.00M (+1.2%) | | `in_credit` | 3.45M | 3.47M (+0.6%) | 3.47M (+0.8%) | | `large` | 1.420M | 1.451M (+2.2%) | 1.438M (+1.3%) | | `ranks` | 24.08M | 24.17M (+0.4%) | 24.17M (+0.4%) | | `none` | 0.600M | 0.608M (+1.2%) | 0.606M (+0.9%) | | `field` | 8.579M | 8.778M (+2.3%) | 8.695M (+1.4%) | | `found` | 15.443M | 15.845M (+2.6%) | 15.640M (+1.3%) | 이체를 읽는 질의(`large`, `none`, `field`, `found`)는 두 프로세스의 차이가 0.03% 안이다(`none`은 명령 하나까지 같다). 계좌를 읽는 질의는 1% 안팎으로 다르다. 계좌는 여덟 스레드로 실어 들어온 순서가 매번 다르고, 그래서 트리의 잎이 다르다(이체는 한 스레드로 싣는다). 조용할 때 잰 사이클로 `ranks`는 15.07M → 15.49M(+2.8%)이었다. 명령은 0.4%만 늘었으니 나머지는 자리다. **표본**(`count.py --profile field`, 300번 물은 동안 명령 20,011개마다 하나, 빌드마다 약 8만 6천 개). 고치기 전(`fc65d66`)에는 `cell_at`이 따로 불리는 함수로 나타난다(0% → 2.98%). 전에는 `pure_list_get` 안에 인라인되어 있었다. 기계어로 보면 칸의 종류를 묻는 검사가 `cbz` 하나에서 네 명령이 되며 커졌다. 고친 뒤(`fcc1f67`)에는 다시 0%다. **고친 뒤의 여섯 쌍.** 처음 잰 것(`fcc1f67-interfered`)은 둘째 라운드부터 다른 세션의 빌드가 부하 평균을 4–12로 올렸다. 소유자의 코어 몫이 0.55–0.89로 떨어졌고, 쌍마다 −29%에서 +80%까지 흔들린다. 판정에 쓰지 않는다. 지우지 않고 남긴다. Pi가 1분 넘게 조용해졌을 때 같은 바이너리로 다시 쟀다(`fcc1f67`, 부하 평균은 0.3에서 시작해 1.4–3.9로 올랐다). | 쌍 | 초당 거래(전 → 후) | 거래당 CPU(µs) | 소유자의 코어 몫 | |---|---:|---:|---:| | 1 | 17,749 → 43,659 | 62.1 → 42.9 | 0.54 → 0.90 (전이 방해받음) | | 2 | 44,346 → 43,576 (−1.7%) | 43.0 → 43.6 | 0.90 → 0.90 | | 3 | 44,382 → 44,206 (−0.4%) | 43.1 → 42.6 | 0.91 → 0.91 | | 4 | 43,832 → 44,604 (+1.8%) | 43.4 → 42.7 | 0.90 → 0.91 | | 5 | 43,447 → 43,905 (+1.1%) | 43.7 → 43.1 | 0.90 → 0.90 | | 6 | 31,885 → 32,117 (+0.7%) | 47.7 → 47.1 | 0.73 → 0.73 (둘 다 방해받음) | 여섯 쌍의 중앙값은 43,640 → 43,782(+0.3%)다. 깨끗한 네 쌍(2–5)의 거래당 CPU는 43.25 → 42.9 µs(−0.8%)다. 은행 질의의 중앙값은 모두 후가 같거나 빠르다(`ranks` 6.84 → 6.80, `per_branch` 5.37 → 5.21, `large` 0.40 → 0.39, `in_credit` 1.00 → 0.85). 다만 방해가 전의 실행에 더 걸렸다(첫 라운드의 전, `in_credit`의 전 셋). 그래서 빠른 쪽으로 기운 숫자다. 이 비교에서는 명령 수가 더 믿을 만하다. ## 판정 - **016의 2단계는 들어간다.** 기준을 채웠다(`fc65d66`의 여섯 쌍). 고친 뒤(`fcc1f67`)는 수의 경로에서 그보다 명령이 적고, 시간으로도 전과 가려지지 않는다(깨끗한 네 쌍 −1.7%에서 +1.8%). - **남은 것**은 명령 +0.4%에서 +1.4%다. 여러 함수에 한두 명령씩 흩어져 있다. 칸의 종류 검사(`text` 표시 한 바이트에서 종류 번호의 비교로), 커진 `run`의 새 경우들이다. 사이클로는 전과 가려지지 않는다. ## 다음 - **수의 경로를 볼 때는 명령도 센다.** 시간의 3% 기준은 그대로 두되, 이 Pi에서 1–3%는 자리의 흔들림이다. 명령 수(`count.py`)는 결정적이라 1% 차이도 보인다. 다음 기준부터 "명령이 1% 넘게 늘지 않는다"를 함께 적는다. - **표본을 뜨는 도구가 생겼다.** 109가 멈춘 곳(캐시보다 큰 목록을 찾는 걸음)을 다시 볼 때 소유자의 표본을 스무 개가 아니라 수만 개 뜰 수 있다. 명령이 아니라 사이클로 뜨면 기다리는 곳이 보인다(`psample.c`의 이벤트만 바꾸면 된다). - 남은 1%를 걷으려면 필드마다 `text` 표시 한 바이트를 레이아웃에 다시 두면 된다. 지금은 하지 않는다.