Skip to content

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
mainfrom
perf/quick-wins-2
Draft

moreal wants to merge 3 commits into
mainfrom
perf/quick-wins-2

Conversation

@moreal

@moreal moreal commented Sep 24, 2026

Copy link
Copy Markdown
Owner
  • Closes #xxxx

One of checkbox below must be checked.

  • I did not use AI to write the code of this patch.
  • This PR follows our AI policy

Summary

성능 개선 가설 세 개(작은 수정 모음)를 하위 항목마다 별도 커밋으로 구현하고 각각 측정했습니다. 기준 커밋은 7cd4a54입니다.

커밋 항목 판정
8aeb187 (a) current_frame.swap → load + store 가설 확인 (호출 위주 벤치 wall-clock 3~7% 개선, 명령어 수는 불변)
63f6895 (b) trace_backward_same_line 인라인 검사 부분 확인 (반복당 명령어 −9개로 예상의 60%, wall-clock 차이는 측정 잡음 이내)
bab0863 (c) 시작 시간 단축 (locations 지연 디코딩) 기각 (시작 명령어 −2.2~2.7%, 목표 −30%에 크게 못 미침)

1. 가설

(a) current_frame.swap → load + store

crates/vm/src/vm/thread.rs 1081행 부근의 cache.current_frame.swap(frame as usize, Ordering::Relaxed)은 x86에서 암묵적 락이 걸리는 xchg로 컴파일됩니다. func_call 프로파일에서 exit_iframe와 함수 호출 경로의 xchg 직후 샘플이 합쳐 약 5%. 이 값을 쓰는 것이 소유 스레드뿐인지 확인하고(sys._current_frames 같은 다른 스레드의 읽기 포함), 그렇다면 load 후 store로 바꿉니다. 같은 패턴의 다른 swap도 찾아보세요. 가설: func_call, method_call, recursion이 3~5% 빨라집니다.

(b) trace_backward_same_line 인라인 검사

crates/vm/src/frame.rs 9251행 부근의 이 함수는 추적이 꺼져 있어도 JUMP_BACKWARD마다 인라인되지 않은 호출로 불려 반복당 약 17개 명령을 씁니다. 추적 여부 검사를 호출 지점에 인라인하고 본문은 #[cold]로 분리합니다. 가설: 루프 반복당 명령어가 15개 안팎 줄어 루프 위주 벤치가 2~3% 빨라집니다.

(c) 시작 시간 단축

-c pass 실행: RustPython 약 57ms / 118~137M 명령, CPython 13ms / 31M 명령. 시작 프로파일 원인: 모든 code object의 linetable을 미리 디코딩(crates/compiler-core/src/marshal.rs의 linetable_to_locations), 시작 중 GC 수집(GcState::collect_inner, 시작 시간의 약 4%), THP huge page 폴트에 따른 커널 clear_page_erms. locations 디코딩을 필요할 때 하도록 바꾸고(트레이스백, co_positions, f_lineno 정확성 유지), 시작·임포트 부트스트랩 동안 자동 GC를 안전하게 억제할 수 있는지 검토합니다. 가설: 시작 명령어 수가 30% 이상 줄어듭니다.

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들(_signal WAKEUP, 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로 게시하며, 경쟁한 스레드의 결과는 버립니다.
  • marshal(frozen/.pyc)과 codegen 모두 지연 방식입니다. codegen도 원래 linetable_to_locations(assembled.linetable)로 만들고 있었으므로 결과는 동일합니다.
  • code() 생성자와 code.replace()는 예전처럼 테이블을 미리 채웁니다. 기존의 (FIXME가 달린) 동작을 그대로 유지하기 위해서입니다.
  • faulthandler는 시그널 핸들러 안에서 locations를 읽으므로 할당을 피해야 합니다. 이미 디코딩된 테이블이 있으면 그것을 쓰고, 없으면 할당이 없는 PyCode::addr2line로 linetable을 순회합니다. 한계: 줄 정보가 없는 명령어(addr2line이 -1)에서는 이전 locations 테이블과 달리 co_firstlineno를 출력합니다. 치명적 오류 덤프의 경계 사례입니다.
  • locations()의 호출 지점은 모두 예외·추적·트레이스백 경로라 hot path 비용은 없습니다.
  • 시작 GC 억제 검토: 기준 -c pass 프로파일(callgrind, 133.6M 명령)에서 collect_inner는 포함 14.7M(11%)입니다. 그중 11.6M은 종료 시 수집(finalize_modules 8.1M, 인터프리터 finalize의 collect_force(2) 3.5M)이고, 시작 중 자동 수집(eval_breaker_work, 2회)은 3.0M(2.3%)뿐입니다. 부트스트랩 동안 억제해도 같은 작업이 시작 직후로 밀릴 뿐이고, -c pass에서만 종료 수집에 흡수되어 이득처럼 보입니다. 벤치 게이밍에 가까우므로 구현하지 않았습니다.
  • THP clear_page_erms는 커널 시간이라 이 항목 범위(명령어 수)에서 다루지 않았습니다.
  • 변경 후 -c pass에서 실제로 디코딩되는 code object는 21개(0.18%)뿐입니다.

3. 하위 항목별 판정과 근거

  • (a) 가설 확인. 기대한 대로 반복당 명령어 수는 변하지 않았습니다(xchg 1개가 mov 2개로 바뀜; 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%)이거나 그 이상입니다.
  • (b) 부분 확인. loop_simple 674 → 665(−9), while_loop 1040 → 1033(−7), 루프가 두 개인 gen_resume은 1671 → 1655(−16)입니다. 예상한 15개에는 못 미칩니다. wall-clock은 a와 b를 번갈아 11회씩 실행했을 때 loop_simple(10M) +1.1%, int_arith +2.4%, list_ops −2.2% 등으로 방향이 일정하지 않았습니다. perf stat -r 10(task-clock)도 866/834, 846/853 ms(각 ±1.3%)로 구별되지 않습니다. 명령어 1.3% 감소는 이 VM의 잡음(±2%)보다 작아서 "루프 벤치 23% 개선"은 확인하지 못했습니다.
  • (c) 기각. -c pass 133.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 pass 30회): 중앙값 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회 중앙값. 각 열은 누적 상태입니다)

루프 본문 base a b (=a+b) c (=a+b+c)
loop_simple 674 673 665 665
while_loop 1040 1038 1033 1029
call 2021 2025 2015 2014
method_call 2285 2287 2279 2279
gen_resume 1670 1671 1655 1653
pyinit_call 4422 4421 4409 4395

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회 중앙값)

base a b c
-c pass 133,933,119 133,889,614 133,918,437 130,993,265 (−2.2%)
-c "import json, re, collections" 234,505,662 234,479,321 234,448,635 228,061,628 (−2.7%)

cmp.sh base vs c(a+b+c 전체), 1차 — 주의: 이 실행에서는 patched 시작 시간 표본(58.7 ms)이 잡음으로 크게 나왔습니다(30회 측정 중앙값은 43.0 ms). 그만큼 patched 쪽에서 더 많이 빼서 patched가 약 1.5% 유리하게 나옵니다. 기하평균 patched/base = 0.948.

startup: base=.047130857 patched=.058674812 cpy=.014268383
| bench | cpython | base | patched | base/cpy | patched/cpy | patched/base |
|---|---|---|---|---|---|---|
| int_arith | 0.242 | 0.691 | 0.656 | 2.85 | 2.71 | 0.949 |
| float_arith | 0.118 | 0.713 | 0.676 | 6.04 | 5.73 | 0.949 |
| func_call | 0.112 | 0.720 | 0.693 | 6.41 | 6.17 | 0.962 |
| method_call | 0.111 | 0.797 | 0.735 | 7.19 | 6.64 | 0.923 |
| attr_rw | 0.077 | 0.695 | 0.685 | 8.99 | 8.86 | 0.986 |
| obj_alloc | 0.103 | 0.836 | 0.809 | 8.13 | 7.86 | 0.967 |
| slots_alloc | 0.097 | 0.541 | 0.508 | 5.56 | 5.22 | 0.939 |
| dict_ops | 0.161 | 0.900 | 0.884 | 5.59 | 5.50 | 0.983 |
| str_dict_ops | 0.149 | 0.942 | 0.915 | 6.34 | 6.16 | 0.971 |
| list_ops | 0.105 | 0.796 | 0.768 | 7.56 | 7.30 | 0.965 |
| listcomp | 0.116 | 0.736 | 0.719 | 6.36 | 6.21 | 0.977 |
| generator | 0.128 | 0.793 | 0.704 | 6.21 | 5.52 | 0.889 |
| closure | 0.096 | 0.633 | 0.596 | 6.61 | 6.22 | 0.941 |
| exceptions | 0.043 | 0.304 | 0.294 | 6.99 | 6.76 | 0.968 |
| str_ops | 0.153 | 0.658 | 0.664 | 4.31 | 4.35 | 1.010 |
| kwargs_call | 0.104 | 1.021 | 0.940 | 9.80 | 9.02 | 0.921 |
| builtin_call | 0.219 | 1.552 | 1.585 | 7.10 | 7.25 | 1.021 |
| global_access | 0.069 | 0.354 | 0.337 | 5.15 | 4.91 | 0.954 |
| recursion | 0.026 | 0.128 | 0.112 | 4.89 | 4.28 | 0.875 |
| super_call | 0.130 | 0.793 | 0.820 | 6.11 | 6.32 | 1.035 |
| property | 0.093 | 0.503 | 0.433 | 5.42 | 4.67 | 0.861 |
| tuple_unpack | 0.187 | 1.266 | 1.165 | 6.78 | 6.24 | 0.920 |
| pystone | 0.097 | 0.594 | 0.587 | 6.10 | 6.03 | 0.989 |
| fannkuch | 0.419 | 2.142 | 2.058 | 5.11 | 4.91 | 0.960 |
| nqueens | 0.100 | 0.673 | 0.664 | 6.74 | 6.66 | 0.987 |
| richards | 0.053 | 0.297 | 0.266 | 5.62 | 5.02 | 0.894 |
| nbody | 0.024 | 0.129 | 0.107 | 5.40 | 4.48 | 0.828 |
| generators | 0.182 | 0.739 | 0.700 | 4.07 | 3.85 | 0.947 |

cmp.sh base vs c, 2차 (시작 시간 표본이 정상) — 기하평균 patched/base = 0.974, base/cpy 6.04 → patched/cpy 5.88.

startup: base=.046674392 patched=.043236462 cpy=.013966765
| bench | cpython | base | patched | base/cpy | patched/cpy | patched/base |
|---|---|---|---|---|---|---|
| int_arith | 0.247 | 0.673 | 0.674 | 2.73 | 2.73 | 1.002 |
| float_arith | 0.119 | 0.688 | 0.698 | 5.79 | 5.88 | 1.015 |
| func_call | 0.119 | 0.762 | 0.677 | 6.43 | 5.71 | 0.889 |
| method_call | 0.109 | 0.832 | 0.774 | 7.65 | 7.11 | 0.930 |
| attr_rw | 0.081 | 0.729 | 0.695 | 8.99 | 8.58 | 0.954 |
| obj_alloc | 0.105 | 0.850 | 0.818 | 8.10 | 7.80 | 0.963 |
| slots_alloc | 0.098 | 0.561 | 0.551 | 5.73 | 5.63 | 0.982 |
| dict_ops | 0.160 | 0.876 | 0.941 | 5.49 | 5.89 | 1.074 |
| str_dict_ops | 0.148 | 0.981 | 0.920 | 6.60 | 6.20 | 0.939 |
| list_ops | 0.108 | 0.780 | 0.805 | 7.23 | 7.46 | 1.032 |
| listcomp | 0.118 | 0.755 | 0.741 | 6.42 | 6.30 | 0.982 |
| generator | 0.129 | 0.788 | 0.728 | 6.09 | 5.63 | 0.924 |
| closure | 0.095 | 0.633 | 0.609 | 6.64 | 6.38 | 0.961 |
| exceptions | 0.044 | 0.304 | 0.318 | 6.86 | 7.19 | 1.048 |
| str_ops | 0.155 | 0.648 | 0.678 | 4.19 | 4.39 | 1.046 |
| kwargs_call | 0.105 | 0.977 | 0.974 | 9.32 | 9.29 | 0.996 |
| builtin_call | 0.221 | 1.575 | 1.623 | 7.11 | 7.33 | 1.030 |
| global_access | 0.068 | 0.358 | 0.362 | 5.24 | 5.31 | 1.013 |
| recursion | 0.026 | 0.142 | 0.129 | 5.53 | 5.05 | 0.912 |
| super_call | 0.122 | 0.830 | 0.793 | 6.81 | 6.51 | 0.956 |
| property | 0.098 | 0.457 | 0.446 | 4.67 | 4.55 | 0.975 |
| tuple_unpack | 0.185 | 1.271 | 1.188 | 6.86 | 6.41 | 0.934 |
| pystone | 0.096 | 0.617 | 0.583 | 6.41 | 6.05 | 0.944 |
| fannkuch | 0.406 | 2.111 | 2.072 | 5.20 | 5.11 | 0.981 |
| nqueens | 0.095 | 0.643 | 0.639 | 6.76 | 6.73 | 0.995 |
| richards | 0.055 | 0.299 | 0.293 | 5.47 | 5.35 | 0.978 |
| nbody | 0.023 | 0.124 | 0.115 | 5.36 | 4.95 | 0.924 |
| generators | 0.177 | 0.733 | 0.676 | 4.13 | 3.81 | 0.923 |

단일 cmp.sh 실행의 잡음은 ±5% 정도입니다. 예를 들어 dict_ops/exceptions/str_ops는 이 변경과 무관한데도 두 실행 사이에 ±5~7% 흔들렸습니다.

cmp.sh base vs a (호출 위주)

startup: base=.045858174 patched=.043333447 cpy=.013529064
| bench | cpython | base | patched | base/cpy | patched/cpy | patched/base |
|---|---|---|---|---|---|---|
| func_call | 0.109 | 0.741 | 0.687 | 6.77 | 6.28 | 0.927 |
| method_call | 0.110 | 0.793 | 0.754 | 7.21 | 6.86 | 0.951 |
| recursion | 0.026 | 0.134 | 0.126 | 5.15 | 4.87 | 0.945 |
| generator | 0.132 | 0.770 | 0.748 | 5.82 | 5.65 | 0.971 |
| closure | 0.096 | 0.629 | 0.598 | 6.54 | 6.23 | 0.952 |
| kwargs_call | 0.104 | 0.967 | 0.963 | 9.30 | 9.27 | 0.996 |
| property | 0.089 | 0.478 | 0.456 | 5.35 | 5.10 | 0.955 |

cmp.sh base vs b (b 바이너리는 a를 포함합니다. 순수 (b) 효과는 아래 a↔b 교차 실행을 보세요)

startup: base=.045984985 patched=.043716325 cpy=.014114604
| bench | cpython | base | patched | base/cpy | patched/cpy | patched/base |
|---|---|---|---|---|---|---|
| int_arith | 0.240 | 0.670 | 0.689 | 2.79 | 2.87 | 1.027 |
| float_arith | 0.116 | 0.688 | 0.678 | 5.94 | 5.85 | 0.986 |
| global_access | 0.067 | 0.367 | 0.361 | 5.49 | 5.41 | 0.985 |
| list_ops | 0.107 | 0.834 | 0.783 | 7.77 | 7.29 | 0.939 |
| listcomp | 0.118 | 0.735 | 0.740 | 6.21 | 6.25 | 1.007 |
| tuple_unpack | 0.190 | 1.256 | 1.237 | 6.60 | 6.50 | 0.985 |
| fannkuch | 0.430 | 2.069 | 2.157 | 4.82 | 5.02 | 1.042 |

번갈아 실행(11회씩, 기동 시간 포함 전체 시간)

bench base median a median ratio (median) ratio (min)
func_call 0.822 0.786 0.956 0.948
method_call 0.891 0.841 0.943 0.919
recursion 0.192 0.190 0.991 0.955
generator 0.872 0.815 0.934 0.955
closure 0.697 0.674 0.967 0.937
bench a median b median ratio (median) ratio (min)
loop_simple_big 0.835 0.844 1.011 1.017
int_arith 0.750 0.768 1.024 1.019
float_arith 0.766 0.773 1.010 0.969
global_access 0.426 0.434 1.020 0.991
list_ops 0.888 0.869 0.978 0.997
tuple_unpack 1.335 1.356 1.016 0.995

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, 미리 채운 테이블)
  • skip/expectedFailure는 추가하지 않았습니다.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nqiu9Z3MepnXPBxA7jKGWq


Generated by Claude Code

`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
@moreal moreal added the run:pyperformance Run the base/head/CPython pyperformance comparison job label Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

<!-​- 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 time / CPython 3.14.7 time -- lower is better. A head/base difference smaller than 3% is shown as ~; CI runners are not quiet enough to read more into it than that.

base head
Median slowdown vs. CPython 3.14.7 (27 common) 4.35x 4.29x
Benchmarks passed 27 27
Geometric mean, head/base (27 common) 0.993
Benchmark CPython 3.14.7 base head base/CPython 3.14.7 head/CPython 3.14.7 head vs. base
mdp 1.61 sec +- 0.01 sec (timeout) (timeout) - - -
nqueens 112 ms +- 3 ms 506 ms +- 18 ms 487 ms +- 8 ms 4.52x 4.35x -3.8%
scimark 433 ms +- 9 ms 1.00 sec +- 0.01 sec 972 ms +- 12 ms 2.31x 2.24x ~
unpack_sequence 42.4 ns +- 0.1 ns 187 ns +- 17 ns 182 ns +- 17 ns 4.41x 4.29x ~
generators 33.3 ms +- 0.6 ms 195 ms +- 2 ms 191 ms +- 3 ms 5.86x 5.74x ~
json_loads 29.6 us +- 0.3 us 51.9 us +- 0.4 us 50.9 us +- 0.4 us 1.75x 1.72x ~
telco 10.2 ms +- 0.5 ms 796 ms +- 11 ms 781 ms +- 10 ms 78.04x 76.57x ~
deltablue 3.47 ms +- 0.02 ms 21.3 ms +- 0.5 ms 20.9 ms +- 0.1 ms 6.14x 6.02x ~
fannkuch 500 ms +- 8 ms 1.71 sec +- 0.04 sec 1.68 sec +- 0.02 sec 3.42x 3.36x ~
typing_runtime_protocols 234 us +- 6 us 912 us +- 25 us 900 us +- 12 us 3.90x 3.85x ~
spectral_norm 136 ms +- 2 ms 505 ms +- 11 ms 500 ms +- 8 ms 3.71x 3.68x ~
go 130 ms +- 4 ms 637 ms +- 6 ms 631 ms +- 9 ms 4.90x 4.85x ~
hexiom 6.85 ms +- 0.06 ms 32.1 ms +- 0.4 ms 31.8 ms +- 0.3 ms 4.69x 4.64x ~
json_dumps 12.7 ms +- 0.1 ms 32.6 ms +- 0.5 ms 32.3 ms +- 0.3 ms 2.57x 2.54x ~
xml_etree 159 ms +- 3 ms 653 ms +- 8 ms 648 ms +- 5 ms 4.11x 4.08x ~
logging 8.41 us +- 0.14 us 42.1 us +- 0.5 us 41.8 us +- 0.7 us 5.01x 4.97x ~
deepcopy 355 us +- 5 us 1.49 ms +- 0.01 ms 1.48 ms +- 0.02 ms 4.20x 4.17x ~
raytrace 359 ms +- 12 ms 1.69 sec +- 0.02 sec 1.68 sec +- 0.02 sec 4.71x 4.68x ~
richards 49.9 ms +- 0.8 ms 228 ms +- 11 ms 227 ms +- 2 ms 4.57x 4.55x ~
regex_compile 164 ms +- 1 ms 591 ms +- 4 ms 589 ms +- 4 ms 3.60x 3.59x ~
float 91.0 ms +- 2.0 ms 270 ms +- 4 ms 270 ms +- 2 ms 2.97x 2.97x ~
comprehensions 20.0 us +- 0.6 us 99.8 us +- 1.9 us 100 us +- 2 us 4.99x 5.00x ~
chaos 74.3 ms +- 0.5 ms 315 ms +- 3 ms 316 ms +- 5 ms 4.24x 4.25x ~
pickle_pure_python 409 us +- 5 us 1.78 ms +- 0.02 ms 1.79 ms +- 0.04 ms 4.35x 4.38x ~
unpickle_pure_python 267 us +- 3 us 1.44 ms +- 0.02 ms 1.45 ms +- 0.02 ms 5.39x 5.43x ~
regex_v8 25.6 ms +- 2.0 ms 84.7 ms +- 1.3 ms 85.4 ms +- 0.6 ms 3.31x 3.34x ~
nbody 169 ms +- 26 ms 396 ms +- 9 ms 405 ms +- 8 ms 2.34x 2.40x ~
coroutines 34.1 ms +- 1.8 ms 172 ms +- 4 ms 178 ms +- 3 ms 5.04x 5.22x +3.5%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run:pyperformance Run the base/head/CPython pyperformance comparison job z-ca-2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants