Repository navigation
perf: drop locked swaps on frame push/pop, inline backward-jump trace check, decode locations lazily - #30
Draft
moreal wants to merge 3 commits into
Draft
perf: drop locked swaps on frame push/pop, inline backward-jump trace check, decode locations lazily#30moreal wants to merge 3 commits into
moreal wants to merge 3 commits into
Conversation
`set_current_frame` and `set_current_frame_nosave` publish the thread's top frame with `AtomicUsize::swap`, and the FrameObject/generator entry paths claim `InterpreterFrame::owner` with `AtomicI8::swap`. On x86 both compile to `xchg`, which carries an implicit `lock` prefix and costs a full barrier on every Python call and return. Neither needs an atomic read-modify-write. `current_frame` lives in a thread-local and is written only by its own thread; its only other reader is faulthandler's signal handler on that same thread, which sees either value. Cross-thread readers (`sys._current_frames`) go through `ThreadSlot::top_iframe`, which was already a plain store. `owner` is only written by the thread holding the frame (the running claim for generators), so a load followed by a release store is equivalent. Assisted-by: Claude Code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nqiu9Z3MepnXPBxA7jKGWq
`trace_backward_same_line` runs on every `JUMP_BACKWARD` to emit the settrace LINE event for a loop edge that stays on one line. It was an out-of-line call even with tracing off, costing a call, prologue and epilogue on each loop iteration just to read `use_tracing`. Test `use_tracing` in an always-inlined wrapper and move the rest into a cold, never-inlined function so the common path is one load and a not-taken branch. Assisted-by: Claude Code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nqiu9Z3MepnXPBxA7jKGWq
Every code object built by marshal or the compiler eagerly expanded its `linetable` into a per-instruction `(start, end)` location table, 16 bytes per instruction. Most code loaded at startup never has a traceback, `co_positions()` or line event asked of it, so the work and the allocation were wasted: `linetable_to_locations` alone was about 3% of the instructions of `rustpython -c pass`. Make `CodeObject::locations` a `CodeLocations` cell that is filled from `linetable` on the first `CodeObject::locations()` call and published with a compare-exchange, so racing readers agree on one table. The `code()` constructor and `code.replace()` still provide their tables up front, keeping their existing (if inexact) behavior. faulthandler reads locations from a signal handler, where decoding would allocate; it now uses an already decoded table when there is one and otherwise walks the linetable with the allocation-free `PyCode::addr2line`. Assisted-by: Claude Code:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nqiu9Z3MepnXPBxA7jKGWq
|
<!-- pyperformance-local-diff --> pyperformance: base vs. head vs. CPython (same runner)All three interpreters were measured back to back in this one job, so they share the exact same CPU and kernel. Ratios are
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One of checkbox below must be checked.
Summary
성능 개선 가설 세 개(작은 수정 모음)를 하위 항목마다 별도 커밋으로 구현하고 각각 측정했습니다. 기준 커밋은
7cd4a54입니다.8aeb187current_frame.swap→load+store63f6895trace_backward_same_line인라인 검사bab08631. 가설
(a)
current_frame.swap→load+store(b)
trace_backward_same_line인라인 검사(c) 시작 시간 단축
2. 구현 요약, 설계 선택, 알려진 한계
(a)
FrameSlotCache::replace_current_frame(load + store)를 추가하고set_current_frame/set_current_frame_nosave가 이를 쓰도록 했습니다.current_frame은thread_local!안에 있어 소유 스레드만 씁니다. 다른 독자는 같은 스레드의 faulthandler 시그널 핸들러뿐이며, 이전 값이나 새 값 중 하나를 보게 됩니다(찢어진 값은 없음).sys._current_frames, 스레드 간f_back)는ThreadSlot::top_iframe/top_frame을 읽는데, 이것들은 원래부터 단순store였습니다.with_frame과gen_frame_link의InterpreterFrame::owner.swap(Thread, AcqRel)도load(Acquire)+store(Release)로 바꿨습니다.owner는 프레임을 실행 중인 스레드(제너레이터는 running claim 보유자)만 쓰기 때문입니다.set_generator/clear_generator/clear_except_code의 쓰기는 실행 중이 아닐 때만 일어납니다.swap들(_signalWAKEUP, itertools의 재진입 플래그,ShuttingDown전환 등)은 실제 RMW 의미가 필요하거나 hot path가 아니라서 두었습니다.xchg가 6012개에서 5997개로 줄었습니다(인라인된 호출 지점 15곳).(b)
trace_backward_same_line을#[inline(always)]래퍼로 바꿔vm.use_tracing만 검사하게 하고, 나머지는#[cold] #[inline(never)] trace_backward_same_line_slow로 옮겼습니다.jump_relative_backward_and_trace_line도#[inline(always)]입니다.(c)
CodeObject::locations의 타입을Box<[(SourceLocation, SourceLocation)]>에서CodeLocations로 바꿨습니다.CodeLocations는AtomicPtr기반 once-cell입니다. compiler-core가no_std라OnceLock을 쓸 수 없어서once_cell::race::OnceBox처럼 직접 구현했습니다.CodeObject::locations()로 합니다. 첫 호출 때linetable에서 디코딩하고 compare-exchange로 게시하며, 경쟁한 스레드의 결과는 버립니다.linetable_to_locations(assembled.linetable)로 만들고 있었으므로 결과는 동일합니다.code()생성자와code.replace()는 예전처럼 테이블을 미리 채웁니다. 기존의 (FIXME가 달린) 동작을 그대로 유지하기 위해서입니다.PyCode::addr2line로 linetable을 순회합니다. 한계: 줄 정보가 없는 명령어(addr2line이 -1)에서는 이전 locations 테이블과 달리co_firstlineno를 출력합니다. 치명적 오류 덤프의 경계 사례입니다.locations()의 호출 지점은 모두 예외·추적·트레이스백 경로라 hot path 비용은 없습니다.-c pass프로파일(callgrind, 133.6M 명령)에서collect_inner는 포함 14.7M(11%)입니다. 그중 11.6M은 종료 시 수집(finalize_modules8.1M, 인터프리터 finalize의collect_force(2)3.5M)이고, 시작 중 자동 수집(eval_breaker_work, 2회)은 3.0M(2.3%)뿐입니다. 부트스트랩 동안 억제해도 같은 작업이 시작 직후로 밀릴 뿐이고,-c pass에서만 종료 수집에 흡수되어 이득처럼 보입니다. 벤치 게이밍에 가까우므로 구현하지 않았습니다.clear_page_erms는 커널 시간이라 이 항목 범위(명령어 수)에서 다루지 않았습니다.-c pass에서 실제로 디코딩되는 code object는 21개(0.18%)뿐입니다.3. 하위 항목별 판정과 근거
xchg1개가mov2개로 바뀜; call 2021 → 2025, 잡음 수준). 효과는 사이클 쪽에서 나옵니다. base와 a를 번갈아 11회씩 실행한 중앙값 기준: func_call −4.4%, method_call −5.7%, generator −6.6%, closure −3.3%, recursion −0.9%(최솟값 기준 −4.5%).cmp.sh단독 실행에서도 func_call −7.3%, method_call −4.9%, recursion −5.5%였습니다. recursion을 뺀 나머지는 가설 범위(3~5%)이거나 그 이상입니다.15개에는 못 미칩니다. wall-clock은 a와 b를 번갈아 11회씩 실행했을 때 loop_simple(10M) +1.1%, int_arith +2.4%, list_ops −2.2% 등으로 방향이 일정하지 않았습니다.3% 개선"은 확인하지 못했습니다.perf stat -r 10(task-clock)도 866/834, 846/853 ms(각 ±1.3%)로 구별되지 않습니다. 명령어 1.3% 감소는 이 VM의 잡음(±2%)보다 작아서 "루프 벤치 2-c pass133.9M → 131.0M(−2.2%),-c "import json, re, collections"234.5M → 228.1M(−2.7%). 가설의 전제였던 "linetable 디코딩 + 시작 GC"가 시작 비용에서 차지하는 비중은 합쳐 약 5%입니다(3.1% + 2.3%). 시작 시간의 대부분은 인터프리터 코드 실행(ExecutingFrame::run, 임포트 시스템)과 marshal 역직렬화(.pyc 17개 로드 26M, 이 빌드는 freeze-stdlib가 아님), 타입 초기화에 쓰입니다. wall-clock(-c pass30회): 중앙값 46.4 → 43.0 ms(−7%), 최솟값 41.9 → 40.6 ms.4. 측정
환경: 클라우드 VM, 4 vCPU, Linux 6.18, 하드웨어 성능 카운터 없음. rustc 1.98.1, 기본 feature(threading 포함)
cargo build --release. CPython 3.11.15. 기준 =7cd4a54, 같은 컨테이너에서 빌드했습니다. 각 단계 바이너리: base=7cd4a54, a=8aeb187, b=63f6895, c=bab0863(c의 최종 커밋은 clippy/테스트만 수정해서 측정 바이너리와 동작이 같습니다).반복당 x86 명령어 수 (
bench/instr_per_iter.sh, valgrind lackey, 3회 중앙값. 각 열은 누적 상태입니다)loop_simple을 뺀 루프 본문은 직접 작성했습니다(call:
g(i), method_call:c.m(i), gen_resume: 제너레이터 순회, pyinit_call:P(i), while_loop:while i < n: i += 1).시작 명령어 수 (valgrind lackey guest instrs, .pyc 캐시가 있는 상태, 3회 중앙값)
-c pass-c "import json, re, collections"cmp.shbase vs c(a+b+c 전체), 1차 — 주의: 이 실행에서는 patched 시작 시간 표본(58.7 ms)이 잡음으로 크게 나왔습니다(30회 측정 중앙값은 43.0 ms). 그만큼 patched 쪽에서 더 많이 빼서 patched가 약 1.5% 유리하게 나옵니다. 기하평균 patched/base = 0.948.cmp.shbase vs c, 2차 (시작 시간 표본이 정상) — 기하평균 patched/base = 0.974, base/cpy 6.04 → patched/cpy 5.88.단일
cmp.sh실행의 잡음은 ±5% 정도입니다. 예를 들어 dict_ops/exceptions/str_ops는 이 변경과 무관한데도 두 실행 사이에 ±5~7% 흔들렸습니다.cmp.shbase vs a (호출 위주)cmp.shbase vs b (b 바이너리는 a를 포함합니다. 순수 (b) 효과는 아래 a↔b 교차 실행을 보세요)번갈아 실행(11회씩, 기동 시간 포함 전체 시간)
5. 실행한 테스트
cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi: 통과 (exit 0)(cd crates/capi && cargo test): 통과 (110 passed)cargo clippy --workspace --all-targets --exclude …와(cd crates/capi && cargo clippy --all-targets): 경고 없음rustpython -m test test_sys test_threading test_sys_settrace test_traceback test_code test_frame test_gc test_importlib test_marshal test_faulthandler(release, c 바이너리): SUCCESS — run=2,850, skipped=497, 10/10 파일bytecode::tests::code_locations_decode_lazily(지연 디코딩, clone, 미리 채운 테이블)🤖 Generated with Claude Code
https://claude.ai/code/session_01Nqiu9Z3MepnXPBxA7jKGWq
Generated by Claude Code