Avoid stop-the-world when `__abstractmethods__` does not change the abstract flag
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 48/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 領域
- backend, performance
研究方向
Start by locating type_set_abstractmethods() in CPython’s C implementation and inspect how it reads and changes Py_TPFLAGS_IS_ABSTRACT around the stop-the-world section. The issue provides the proposed condition and a benchmark covering concrete, protocol, abstract, and plain classes; use those cases to assess the change. Done means unchanged abstract state avoids stopping the world while state changes preserve correct behavior.
由索引模型根據 Issue 內容生成。
描述
Summary
In a free-threaded CPython build, type_set_abstractmethods() stops the worldevery time ABCMeta writes __abstractmethods__, even when the write does notchange Py_TPFLAGS_IS_ABSTRACT.
ABCMeta performs this write for every newly created class. For a concrete subclass of an ABC, the value is normally an empty set, so the abstract flag is already clear. Nevertheless, the current implementation briefly stops all other Python threads.
Benchmark
import argparse, gc, statistics, sys, threading, time, typing
from abc import ABC, abstractmethod
from collections.abc import Sized
def make(kind, n):
if kind == "concrete":
for _ in range(n):
class X(Sized):
def __len__(self): return 0
elif kind == "protocol":
for _ in range(n):
class P(typing.Protocol):
def m(self) -> int: ...
class C(P):
def m(self) -> int: return 0
elif kind == "abstract":
for _ in range(n):
class Y(ABC):
@abstractmethod
def m(self): ...
elif kind == "plain":
for _ in range(n):
class Z: pass
else:
raise SystemExit(f"unknown kind {kind}")
def one_round(kind, k, n):
barrier = threading.Barrier(k + 1)
def worker():
barrier.wait()
make(kind, n)
ts = [threading.Thread(target=worker) for _ in range(k)]
for t in ts: t.start()
barrier.wait()
t0 = time.perf_counter()
for t in ts: t.join()
return time.perf_counter() - t0
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--kind", default="concrete")
ap.add_argument("--threads", type=int, default=1)
ap.add_argument("--n", type=int, default=2000)
ap.add_argument("--repeat", type=int, default=5)
ap.add_argument("--no-gc", action="store_true")
a = ap.parse_args()
if a.no_gc:
gc.disable()
make(a.kind, 50)
per = [one_round(a.kind, a.threads, a.n) / (a.n * a.threads) * 1e6
for _ in range(a.repeat)]
gil = getattr(sys, "_is_gil_enabled", lambda: True)()
print(f"{a.kind},{a.threads},{a.n},{statistics.median(per):.2f},{min(per):.2f},{gil}")
if __name__ == "__main__":
main()
Multiple threads run make_classes(2000) concurrently. The benchmark reports the median time per created class over three repetitions. It also includes three controls:
protocol: atyping.Protocoland a concrete subclass;abstract: an ABC subclass that remains abstract;plain: a class that does not useABCMeta.
Implementation
In type_set_abstractmethods(), compare the requested abstract state with the current Py_TPFLAGS_IS_ABSTRACT flag before entering the stop-the-world section:
if (abstract != ((type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) != 0)) {
type_lock_prevent_release(&pinned);
types_stop_world();
if (abstract)
type_add_flags(type, Py_TPFLAGS_IS_ABSTRACT);
else
type_clear_flags(type, Py_TPFLAGS_IS_ABSTRACT);
types_start_world();
type_lock_allow_release(&pinned);
}
Performance results
Each result below is the median of five outer rounds. Each benchmark invocation
uses three inner repetitions and creates 2000 classes per thread.
| Case | Threads | Baseline | Patched | Speedup | Spread, baseline / patched |
|---|---|---|---|---|---|
| concrete | 1 | 6.90 | 6.46 | 1.07x | ±24% / ±14% |
| concrete | 2 | 920.77 | 6.06 | 151.94x | ±15% / ±11% |
| concrete | 4 | 704.21 | 6.18 | 113.95x | ±3% / ±8% |
| concrete | 8 | 490.60 | 7.18 | 68.33x | ±6% / ±11% |
| concrete | 16 | 348.28 | 8.45 | 41.22x | ±6% / ±6% |
| protocol | 1 | 22.62 | 21.65 | 1.04x | ±7% / ±5% |
| protocol | 2 | 2801.11 | 1038.10 | 2.70x | ±4% / ±2% |
| protocol | 4 | 2463.42 | 1111.75 | 2.22x | ±1% / ±0% |
| protocol | 8 | 1963.49 | 1028.07 | 1.91x | ±1% / ±1% |
| protocol | 16 | 1397.80 | 783.15 | 1.78x | ±3% / ±4% |
| abstract | 1 | 6.62 | 6.93 | 0.96x | ±16% / ±12% |
| abstract | 2 | 885.09 | 894.81 | 0.99x | ±10% / ±10% |
| abstract | 4 | 636.98 | 632.91 | 1.01x | ±1% / ±3% |
| abstract | 8 | 445.71 | 461.46 | 0.97x | ±6% / ±5% |
| abstract | 16 | 329.88 | 333.72 | 0.99x | ±9% / ±3% |
| plain | 1 | 3.74 | 3.95 | 0.95x | ±20% / ±39% |
| plain | 2 | 3.46 | 3.67 | 0.94x | ±13% / ±11% |
| plain | 4 | 3.91 | 3.98 | 0.98x | ±12% / ±7% |
| plain | 8 | 4.16 | 4.32 | 0.96x | ±7% / ±7% |
| plain | 16 | 4.16 | 4.14 | 1.00x | ±13% / ±3% |
Linked PRs
- gh-161745
- 主要語言
- Python
- 星號
- 78.2k
- 分支
- 36.6k
- 平均合併
- 19 小時 57 分鐘
- 30 天內合併 PR
- 630
環境準備
在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。
- 沒有 Dockerfile 或 Docker Compose 檔案
- 有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
python/cpython 的其他 Issue
-
難度 2/5 1-3 小時 新手友好度 78/100
維護者通常 1 天內回覆
-
stdlib type-bug
難度 2/5 1-3 小時 新手友好度 75/100
維護者通常 1 天內回覆
-
extension-modules stdlib topic-XML type-bug
難度 2/5 1-3 小時 新手友好度 76/100
python/cpython#161807 · 1 則留言 ·
維護者通常 1 天內回覆
-
stdlib type-feature
難度 2/5 1-3 小時 新手友好度 78/100
python/cpython#161788 · 7 則留言 ·
維護者通常 1 天內回覆
-
extension-modules topic-IO type-bug
難度 2/5 1-3 小時 新手友好度 75/100
維護者通常 1 天內回覆
相似的 Issue
-
難度 2/5 1-3 小時 新手友好度 62/100
cpp-best-practices/project_validator#267 ·
維護者通常 1 天內回覆
-
難度 2/5 1-3 小時 新手友好度 72/100
Project-N-E-K-O/N.E.K.O#3725 ·
維護者通常 1 天內回覆
-
[Bug] Sending a Telegram message chain twice duplicates mentions可能已有人在做 @123XieC 今天認領。 未關閉
難度 2/5 1-3 小時 新手友好度 72/100
AstrBotDevs/AstrBot#11125 ·
維護者通常 1 天內回覆
-
backend bug priority: high
難度 2/5 1-3 小時 新手友好度 72/100
nooriegarg/ContribIQ#73 ·
維護者通常 1 天內回覆
-
bug p3 security
難度 2/5 1-3 小時 新手友好度 68/100
維護者通常 1 天內回覆