Skip to content

perf: thread-local GC young lists and a dedicated dealloc for exact int/float/complex - #32

Draft
moreal wants to merge 3 commits into
mainfrom
perf/gc-dealloc
Draft

moreal wants to merge 3 commits into
mainfrom
perf/gc-dealloc

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

성능 실험 ④ GC 추적 스레드 로컬화와 타입별 해제 특화의 구현과 측정 결과입니다. 커밋 3개로 나뉘어 있고, 각 커밋의 효과를 따로 쟀습니다.

1. 가설 (원문 인용)

정수·실수 생성+해제 명령어 수가 크게 줄고(목표 100개 미만), obj_alloc, nqueens, float_arith가 1.1~1.3배 빨라집니다.

제안:

  • (a) GC 0세대 목록을 스레드 로컬로 옮겨 소유 스레드의 추적·해제에서 전역 락을 없앱니다.
  • (b) 자식 참조가 없는 불변 exact 타입(int, float 등)은 __del__·약한 참조·trashcan 검사를 건너뛰고 프리리스트로 바로 가는 전용 해제 경로를 둡니다.
  • (c) new_int/new_float 생성 경로(프리리스트 pop, 헤더 초기화)를 다듬습니다.

2. 구현 요약, 설계 선택, 한계

(a) Keep gen0 in thread-local young lists

  • 인터프리터에 attach된 스레드는 자기 young 리스트(gc_state::young::LocalYoung)를 가지며, 락이나 원자적 RMW 없이 push와 unlink를 합니다. 이 방식이 안전한 이유: stop-the-world는 attach된 스레드를 safepoint에서만 멈추는데, 리스트 연산 안에는 safepoint가 없습니다. 그 밖의 접근자(detach된 소유자, 수집기, 스레드 종료 처리)는 모두 리스트별 lock을 잡고, 수집기는 world가 멈춘 상태에서만 접근합니다. "attach 여부"는 새 캐시 포인터 CURRENT_THREAD_STATE의 relaxed load 한 번으로 확인합니다.
  • 수집 시 병합: 수집 시작 시(STW 직후) 모든 young 리스트를 공유 gen0 리스트로 splice합니다. 그 뒤의 수집기 코드, gc.get_objects(), gc.freeze()는 전과 같이 공유 리스트만 봅니다. 객체는 GC_LOCAL_YOUNG(=4) 세대 표시와 함께, 수집 중이 아닐 때는 쓰이지 않는 gc_refs 필드에 리스트 id를 담습니다. 헤더 크기는 바뀌지 않습니다.
  • 다른 스레드에서 해제되는 객체: 그 스레드는 unlink할 수 없습니다. 그래서 __del__, weakref 콜백, 필드 drop은 그 스레드에서 즉시 수행하고(의미 보존), 메모리만 소유자의 husk 큐로 돌려줍니다. 해당 리스트에서 unlink하고 해제하는 쪽은 소유자(다음 추적 할당 때), 수집 시작, 스레드 종료 처리입니다. 이 경로를 위해 PyInner::dealloc을 drop_into_raw + RawAlloc::free로 나눴습니다.
  • 살아 있는 객체를 다른 스레드에서 untrack하는 경우(C API PyObject_GC_UnTrack로만 도달 가능)는 STW 후 splice하고 처리합니다. 드문 경로입니다.
  • 카운트: 리스트마다 증감분을 따로 두고, ±64를 넘으면 공유 counts[0]에 합칩니다. 임계치 판정은 공유값 + 자기 증감분으로 하고, gc.get_count()는 모든 증감분을 더해 보고합니다.
  • STW를 부르는 비수집 경로(get_objects, freeze, 위 C API 경로)는 먼저 collecting 락을 detach 상태로 기다려 잡습니다. 처음에는 이 처리가 없어서 vm::interpreter::tests를 병렬로 돌리면 교착이 생겼습니다. STW 요청자 둘이 동시에 들어오면 진 쪽이 attach된 채 admission 락을 기다리기 때문입니다. 수정 후 해당 테스트를 반복 실행해 통과를 확인했습니다.
  • fork 후에는 살아남지 못한 스레드들의 리스트를 공유 리스트로 회수합니다.
  • 대안(CPython 3.13t의 mimalloc 힙 순회)은 채택하지 않았습니다. RustPython도 mimalloc을 쓰지만, GC 객체를 별도 힙에 모으고 블록마다 GC 객체인지 판별하는 구조가 필요합니다. 할당 경로와 수집기 전체를 다시 설계해야 하는 규모입니다. 이번 방식은 기존 수집기를 그대로 두고 핫 경로의 락만 없앱니다.
  • 한계: (1) 소유 스레드가 할당을 하지 않으면 다른 스레드가 돌려준 husk의 메모리는 다음 수집이나 스레드 종료 때까지 남습니다(gc.disable() 상태면 더 오래). (2) VM이 없는 스레드가 다른 스레드의 young 객체를 살아 있는 상태로 untrack하는 경우는 STW를 할 수 없어 안전하지 않습니다. 인터프리터 내부에는 이런 호출 경로가 없고 debug_assert로 막아 두었습니다. (3) VM 없이 get_objects를 부르면 young 객체가 보이지 않습니다(STW 불가). (4) non-threading 빌드는 기존 경로를 그대로 씁니다.

(b) Give exact int, float and complex a dealloc of their own

  • PyPayload::TRIVIAL_EXACT_DEALLOC을 추가하고 int, float, complex에 설정했습니다. 이 타입들의 클래스는 static/immutable이라 __del__을 가질 수 없고(설정 시 TypeError), weakref를 지원하지 않으며(TypeError 확인), dict/slots가 없습니다. 페이로드도 참조를 갖지 않으므로 GC 추적 대상이 아니고 clear할 것도 없습니다(HAS_TRAVERSE/HAS_CLEAR가 false인지 const assert로 검사).
  • vtable의 dealloc이 이 경우 trivial_exact_dealloc를 가리킵니다. 클래스가 정확히 T::class()이면 곧바로 프리리스트로 보내고, published 객체는 QSBR 지연 해제가 되는 PyInner::dealloc으로 보냅니다. 서브클래스 인스턴스는 default_dealloc으로 넘어갑니다. default_dealloc 안에서 일찍 분기하지 않고 vtable에서 함수를 고르는 이유는, 범용 경로의 6-레지스터 prologue를 피하기 위해서입니다.

(c) Build exact ints and floats without touching their class's refcount

  • into_ref가 int 타입(immortal이 아님)을 clone하고, 프리리스트에서 재사용하면 다시 drop했습니다. 정수를 하나 만들 때마다 lock xadd가 두 번 들어갔습니다. PyRef::new_trivial_exact는 프리리스트 husk가 이미 가진 클래스 참조를 그대로 이어받습니다. 두 dealloc 경로 모두 exact 인스턴스만 프리리스트에 넣으므로 가능합니다. dict/추적 검사도 생략합니다.
  • int/float/complex 프리리스트가 Cell::take/set으로 Vec을 빼고 다시 넣던 방식을 FreeList::push_to/pop_from의 제자리 접근으로 바꿨습니다.

3. 판정: 가설 확인 (단, 메커니즘에 관한 단서가 있음)

  • 정수 생성+해제 명령어 수: 185 → 약 77~96 (목표 100 미만 달성). callgrind 호출당 값 기준으로 into_ref<PyInt> 67 → 40, 정수 dealloc 96 → 57 (b) → 37 (c)입니다. new_int 래퍼(약 19~22)를 거치는 경로는 약 96입니다. 실수도 같은 구조입니다(body_float 반복당 1046 → 864).
  • wall-clock(cmp.sh, 기준 대비): obj_alloc 1.12×, nqueens 1.13×, float_arith 1.14× (1차 측정 1.13/1.12/1.12×). 가설 범위(1.1~1.3×) 안이고, 28개 벤치 기하평균은 0.933(약 1.07×)입니다.
  • 단서: obj_alloc과 nqueens의 이득은 (a)에서 오며, 명령어 수로는 설명되지 않습니다. (a)는 추적 할당당 명령어를 58 → 84로 늘립니다. 대신 추적+해제마다 lock 접두 RMW 34개를 없앱니다. valgrind는 lock cmpxchg를 명령어 하나로 세므로 이 효과가 드러나지 않습니다. 실제로 전체 명령어 수는 obj_alloc +0.8%, nqueens +1.2%인데도 perf의 track/untrack 비중은 14.3% → 2.0%(obj_alloc), 12.0% → 1.4%(nqueens)로 줄었고, wall-clock도 1112% 줄었습니다. 반면 float_arith와 int_arith의 이득은 (b)와 (c)의 명령어 감소(전체 −20%, −13%)에서 옵니다.
  • recursion은 두 번 모두 느리게 나왔습니다(1.056, 1.085; 0.13초 벤치에서 약 12ms 차이). 하지만 명령어 수는 −0.3%이고 callgrind 프로파일은 같습니다. 코드 배치 영향이나 VM 노이즈로 보이지만, 원인을 확정하지는 못했습니다. kwargs_call(1.038 → 0.977)과 property(0.992 → 1.037)는 두 측정 사이에서 방향이 바뀐 노이즈입니다. 이 VM에서는 같은 바이너리도 min과 median이 약 10% 벌어집니다.
  • 정수 dealloc 빠른 경로에 남은 비용(perf 기준 float_arith의 약 10%)은 호출자의 lock xadd decref 직후 refcount 워드를 읽는 지점에 몰려 있습니다. 해제 로직이 아니라 원자적 참조 카운트 자체의 비용이며, 편향 참조 카운트 세션의 영역입니다.

4. 측정

환경: 같은 컨테이너(4 vCPU VM, HW 카운터 없음 → perf는 -e cpu-clock), rustc 1.98.1, cargo build --release(기본 threading 빌드). 기준 = 분기 시점 커밋 7cd4a54(별도 worktree/target). CPython 3.11.15.

bench/cmp.sh BASE PATCHED (5회 최소값, 시작 시간 차감), 최종 브랜치 기준

startup: base=.045 patched=.044 cpy=.014

bench cpython base patched base/cpy patched/cpy patched/base
attr_rw 0.080 0.684 0.668 8.60 8.40 0.977
builtin_call 0.222 1.563 1.392 7.05 6.28 0.891
closure 0.094 0.624 0.584 6.63 6.20 0.936
dict_ops 0.161 0.895 0.919 5.54 5.69 1.027
exceptions 0.044 0.316 0.245 7.14 5.52 0.773
fannkuch 0.408 2.088 1.736 5.12 4.26 0.832
float_arith 0.118 0.687 0.604 5.84 5.13 0.879
func_call 0.111 0.749 0.747 6.78 6.76 0.998
generator 0.129 0.806 0.740 6.23 5.72 0.918
generators 0.171 0.729 0.707 4.25 4.12 0.970
global_access 0.065 0.389 0.326 5.97 4.99 0.836
int_arith 0.235 0.689 0.611 2.93 2.59 0.886
kwargs_call 0.104 1.002 0.978 9.59 9.36 0.977
list_ops 0.106 0.773 0.749 7.26 7.04 0.969
listcomp 0.120 0.768 0.706 6.39 5.88 0.920
method_call 0.107 0.824 0.775 7.72 7.26 0.940
nbody 0.023 0.121 0.117 5.35 5.17 0.966
nqueens 0.091 0.664 0.589 7.26 6.45 0.887
obj_alloc 0.105 0.838 0.751 7.96 7.13 0.896
property 0.092 0.467 0.485 5.10 5.29 1.037
pystone 0.093 0.594 0.588 6.36 6.29 0.990
recursion 0.026 0.134 0.146 5.23 5.68 1.085
richards 0.053 0.296 0.284 5.62 5.40 0.962
slots_alloc 0.094 0.549 0.501 5.84 5.33 0.912
str_dict_ops 0.148 0.924 0.881 6.24 5.95 0.954
str_ops 0.158 0.662 0.661 4.20 4.19 0.998
super_call 0.119 0.815 0.822 6.83 6.89 1.008
tuple_unpack 0.188 1.238 0.980 6.57 5.20 0.792

기하평균: patched/base 0.933, base/cpy 6.11, patched/cpy 5.71.

(같은 비교의 1차 측정(교착 수정 전 커밋 (c)): 기하평균 0.932, obj_alloc 0.883, nqueens 0.891, float_arith 0.892, recursion 1.056, kwargs_call 1.038.)

반복당 x86 명령어 수 (bench/instr_per_iter.sh, valgrind lackey, f(200000) − f(0))

루프 본문 CPython 3.11 base (a) (a)+(b) (a)+(b)+(c)
loop_simple (x = i) 207 673 676 629 583
body_int (x = i * 7) 367 1115 1116 1029 928
body_float (y = x * 1.0000001) 305 1046 1052 963 864
body_tuple (t = (i, n)) 422 1722 1745 1700 1670
body_list (l = [i]) 461 1575 1606 1564 1530
body_obj (P(i, i), __init__에서 속성 2개) 1643 5186 5275 5242 5215

호출당 명령어 수 (callgrind, body_int/body_list)

함수 base 최종
into_ref<PyInt> 67 40
정수 dealloc 96 57 (b) → 37 (c)
정수 생성+해제 합 약 163 (+ new_int 22 = 185) 약 77 (+ new_int 19 = 96)
track_new_object 58 (lock 접두 RMW 3개) 84 (lock 접두 RMW 0개)
untrack 65 (lock 접두 RMW 3~4개) 55 (0개)

전체 명령어 수 (callgrind, 벤치 전체 실행)

bench base (a) (a)+(b) (a)+(b)+(c) 최종/base
obj_alloc 5.308G 5.392G 5.370G 5.352G +0.8%
nqueens 4.113G 4.149G 4.147G 4.163G +1.2%
float_arith 6.901G 6.920G 6.275G 5.517G −20.1%
int_arith 8.051G 8.136G 7.779G 6.984G −13.2%

단계별 wall-clock (wall.sh, 교차 실행 11회 최소값 ms, 시작 시간 포함)

bench base (a) (a)+(b) (a)+(b)+(c)
obj_alloc 886 755 801 765
nqueens 707 652 644 653
float_arith 743 713 702 667
int_arith 716 708 724 669

perf (-e cpu-clock, self 비중)

bench base 최종
obj_alloc: untrack_object + track_new_object 7.34% + 6.99% untrack_young 0.80% + track_new_object 1.17%
nqueens: 같은 항목 6.56% + 5.47% 0.61% + 0.83%
float_arith: float dealloc 11.56% 10.18% (대부분 decref 직후 refcount load)

5. 실행한 테스트와 결과

  • cargo test --workspace --exclude rustpython_wasm --exclude rustpython-venvlauncher --exclude rustpython-capi: 전부 통과(rustpython-vm lib 102개 포함). 첫 실행은 위에서 설명한 교착으로 vm::interpreter::tests에서 멈췄고, 수정 후 전체를 다시 돌려 통과했습니다. 수정 후 vm::interpreter::tests는 10회 반복해 모두 통과했습니다. 중간에 한 번 _opcode 스냅숏 테스트 8개가 실패했는데, 실행 도중 소스를 수정해 생긴 일이었고 재실행에서 통과했습니다.
  • (cd crates/capi && cargo test): 110개 통과.
  • clippy(AGENTS.md의 두 명령): 경고 없음.
  • cargo check -p rustpython-vm --no-default-features --features compiler (non-threading): 경고 없음.
  • cargo run --release -- -m test: test_gc, test_weakref, test_finalization, test_threading, test_thread, test_threading_local, test_queue, test_concurrent_futures, test_int, test_float, test_complex, test_generators, test_sys, test_os 모두 SUCCESS. (test_capi는 이 저장소의 Lib/test에 없어서 기준 빌드에서도 import 단계에서 실패하므로 제외했습니다.)
  • 직접 작성한 교차 스레드 스트레스 스크립트(짧게 사는 스레드가 만든 객체의 retire, 3 producer/3 consumer 간 순환 객체 전달, 동시 gc.collect(0)/gc.get_objects(0), 다른 스레드에서 마지막 참조를 끊을 때의 weakref 콜백): release 빌드에서 반복 통과했고, debug 빌드(debug_assert 활성)에서도 통과했습니다.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GKcHXRR8VEWRsXXMxp1nWK

Every tracked allocation pushed onto the one shared gen0 list under its
write lock and bumped a shared atomic count, and every dealloc took the
lock again to unlink: three to four lock-prefixed read-modify-writes per
object lifetime, on the hottest allocation path.

A thread attached to an interpreter now keeps its own young list and
touches it with no lock and no atomic read-modify-write. A stop-the-world
cannot park an attached thread outside a safepoint, so nothing else reads
the list while its owner uses it. Everyone else (the owner while detached,
a collector, thread exit) takes the list's own lock, and a collector only
does so with the world stopped.

A collection starts by splicing every young list into the shared gen0
list, so the rest of the collector, gc.get_objects() and gc.freeze() keep
walking the shared lists only. An object carries its list id in gc_refs,
which is unused outside a collection, under a new GC_LOCAL_YOUNG
generation.

A thread that deallocates an object linked into another thread's young
list cannot unlink it. It still runs the finalizer, clears weakrefs and
drops the fields right away, then hands the bare memory back through the
owner's husk queue; the owner unlinks and frees husks on its next tracked
allocation, a collection does at its start, and thread exit does before
the list is retired. Untracking a live object in another thread's list
(only reachable through the C API) stops the world and splices first.

Each young list keeps its own count delta and folds it into the shared
gen0 count once it drifts by 64, so the collection threshold stays within
that much per thread of the truth; gc.get_count() adds the deltas in.

gc.get_objects(), gc.freeze() and that C API path stop the world
outside a collection, so they take the collecting lock first (waiting
detached): two requesters stopping the world at once deadlock, because
the loser waits for the admission lock while still attached.

Assisted-by: Claude Code
Claude-Session: https://claude.ai/code/session_01GKcHXRR8VEWRsXXMxp1nWK
default_dealloc checks for everything any type may need: the class's
__del__ slot, a weakref list, GC tracking and the trashcan, a clear
function, then instance dict and member slots before the freelist.
An exact int, float or complex needs none of it. Their classes are
static and immutable, so they have no __del__ and no dict and never
gain one, they do not support weakrefs, and the payloads hold no
references, so they are never tracked and have nothing to clear.

PyPayload::TRIVIAL_EXACT_DEALLOC says so for a payload, and the vtable
then points at trivial_exact_dealloc: for an instance of exactly
T::class() it goes straight to the freelist (or PyInner::dealloc for a
published object); a subclass instance, which can have all of those,
falls through to default_dealloc. Picking the function in the vtable
rather than branching early inside default_dealloc keeps the common
case out of the generic path's stack frame.

Assisted-by: Claude Code
Claude-Session: https://claude.ai/code/session_01GKcHXRR8VEWRsXXMxp1nWK
PyPayload::into_ref cloned the class for PyRef::new_ref, which checks it
for an instance dict and a heap type, and when the object came off the
freelist dropped the clone again because the husk already pointed at
the same class: an atomic increment and decrement of int's refcount
for every int built, on a line every thread building ints shares.

For a payload that sets TRIVIAL_EXACT_DEALLOC, into_ref now goes to
PyRef::new_trivial_exact. Its class has no dict and its instances are
never tracked, so there is nothing to check, and since both dealloc
paths only put exact instances on the freelist, a reused husk already
holds the class reference the new object needs and keeps it.

The int, float and complex freelists also stop moving their vector out
of the thread-local Cell and back on every push and pop, which copied
it twice and dropped the empty list left behind; FreeList::push_to and
pop_from work on it in place.

Assisted-by: Claude Code
Claude-Session: https://claude.ai/code/session_01GKcHXRR8VEWRsXXMxp1nWK
@moreal moreal added the run:pyperformance Run the base/head/CPython pyperformance comparison job label Sep 24, 2026
@codspeed

codspeed Bot commented Sep 24, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 14.41%

⚡ 4 improved benchmarks
✅ 58 untouched benchmarks
⏩ 4 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ nbody.py[rustpython] 1.2 s 1 s +19.69%
⚡ mandelbrot.py[rustpython] 695.4 ms 593.8 ms +17.13%
⚡ rustpython[frozenset.py] 454.9 µs 411.3 µs +10.6%
⚡ rustpython[addition.py] 2.3 ms 2.1 ms +10.53%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing perf/gc-dealloc (dcf31e1) with main (7cd4a54)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@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.16x 3.90x
Benchmarks passed 27 27
Geometric mean, head/base (27 common) 0.964
Benchmark CPython 3.14.7 base head base/CPython 3.14.7 head/CPython 3.14.7 head vs. base
mdp 1.97 sec +- 0.07 sec (timeout) (timeout) - - -
nbody 154 ms +- 8 ms 419 ms +- 14 ms 352 ms +- 3 ms 2.72x 2.29x -16.0%
scimark 490 ms +- 27 ms 991 ms +- 46 ms 835 ms +- 51 ms 2.02x 1.70x -15.7%
spectral_norm 160 ms +- 7 ms 485 ms +- 6 ms 415 ms +- 11 ms 3.03x 2.59x -14.4%
unpack_sequence 45.0 ns +- 3.1 ns 187 ns +- 7 ns 165 ns +- 13 ns 4.16x 3.67x -11.8%
float 95.8 ms +- 1.0 ms 268 ms +- 3 ms 248 ms +- 3 ms 2.80x 2.59x -7.5%
fannkuch 496 ms +- 14 ms 1.71 sec +- 0.06 sec 1.60 sec +- 0.04 sec 3.45x 3.23x -6.4%
raytrace 353 ms +- 8 ms 1.66 sec +- 0.01 sec 1.59 sec +- 0.02 sec 4.70x 4.50x -4.2%
chaos 77.4 ms +- 3.2 ms 294 ms +- 3 ms 282 ms +- 3 ms 3.80x 3.64x -4.1%
json_dumps 12.1 ms +- 0.1 ms 32.1 ms +- 0.5 ms 31.0 ms +- 0.2 ms 2.65x 2.56x -3.4%
go 126 ms +- 2 ms 645 ms +- 79 ms 628 ms +- 18 ms 5.12x 4.98x ~
regex_v8 24.5 ms +- 0.3 ms 84.9 ms +- 1.3 ms 82.8 ms +- 1.0 ms 3.47x 3.38x ~
deltablue 3.63 ms +- 0.06 ms 22.0 ms +- 0.6 ms 21.5 ms +- 0.3 ms 6.06x 5.92x ~
logging 8.22 us +- 0.14 us 32.0 us +- 0.3 us 31.5 us +- 0.3 us 3.89x 3.83x ~
unpickle_pure_python 260 us +- 18 us 1.35 ms +- 0.03 ms 1.33 ms +- 0.01 ms 5.19x 5.12x ~
generators 37.3 ms +- 1.5 ms 205 ms +- 13 ms 202 ms +- 5 ms 5.50x 5.42x ~
nqueens 115 ms +- 3 ms 482 ms +- 7 ms 475 ms +- 6 ms 4.19x 4.13x ~
json_loads 28.5 us +- 0.8 us 49.9 us +- 0.6 us 49.3 us +- 0.3 us 1.75x 1.73x ~
pickle_pure_python 380 us +- 13 us 1.68 ms +- 0.02 ms 1.66 ms +- 0.04 ms 4.42x 4.37x ~
hexiom 6.96 ms +- 0.16 ms 31.5 ms +- 0.4 ms 31.2 ms +- 0.2 ms 4.53x 4.48x ~
deepcopy 342 us +- 8 us 1.31 ms +- 0.01 ms 1.30 ms +- 0.01 ms 3.83x 3.80x ~
typing_runtime_protocols 229 us +- 8 us 852 us +- 12 us 848 us +- 8 us 3.72x 3.70x ~
comprehensions 20.5 us +- 0.6 us 98.8 us +- 2.0 us 98.4 us +- 1.4 us 4.82x 4.80x ~
telco 9.93 ms +- 0.31 ms 659 ms +- 4 ms 658 ms +- 7 ms 66.36x 66.26x ~
richards 49.5 ms +- 1.0 ms 227 ms +- 2 ms 228 ms +- 3 ms 4.59x 4.61x ~
regex_compile 134 ms +- 5 ms 515 ms +- 3 ms 522 ms +- 21 ms 3.84x 3.90x ~
coroutines 32.4 ms +- 1.2 ms 166 ms +- 7 ms 170 ms +- 7 ms 5.12x 5.25x ~
xml_etree 153 ms +- 5 ms 730 ms +- 10 ms 757 ms +- 9 ms 4.77x 4.95x +3.7%

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