gh-150942: Speed up Py_BuildValue dict construction - #156199
Conversation
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
@tkim602 Can you show the benchmark script? |
Sure, I used the following workloads: import pyperf
runner = pyperf.Runner()
runner.timeit(
"integer dict, 3 pairs",
stmt=(
'_testcapi.py_buildvalue_ints('
'"{IIIIII}", 1001, 2001, 3001, 4001, 5001, 6001)'
),
setup="import _testcapi",
)
runner.timeit(
"integer dict, 5 pairs",
stmt=(
'_testcapi.py_buildvalue_ints('
'"{IIIIIIIIII}", '
'1001, 2001, 3001, 4001, 5001, 6001, '
'7001, 8001, 9001, 10001)'
),
setup="import _testcapi",
)
runner.timeit(
"existing objects, 3 pairs",
stmt=(
'_testcapi.py_buildvalue('
'"{OOOOOO}", k1, v1, k2, v2, k3, v3)'
),
setup=(
"import _testcapi; "
"k1=object(); v1=object(); "
"k2=object(); v2=object(); "
"k3=object(); v3=object()"
),
)
runner.timeit(
"existing objects, 5 pairs",
stmt=(
'_testcapi.py_buildvalue('
'"{OOOOOOOOOO}", '
"k1, v1, k2, v2, k3, v3, k4, v4, k5, v5)"
),
setup=(
"import _testcapi; "
"k1=object(); v1=object(); "
"k2=object(); v2=object(); "
"k3=object(); v3=object(); "
"k4=object(); v4=object(); "
"k5=object(); v5=object()"
),
)
runner.timeit(
"duplicate key, 3 pairs",
stmt=(
'_testcapi.py_buildvalue('
'"{OOOOOO}", k, v1, k, v2, k, v3)'
),
setup=(
"import _testcapi; "
"k=object(); v1=object(); v2=object(); v3=object()"
),
)I ran it with: python bench_mkdict.py --processes 10 --values 10 --min-time 0.5For the reported numbers, I ran the baseline and patched builds in A/B/B/A order and also checked the reverse comparison. |
|
@tkim602 The changes look correct to me, but I am not sure there are any places where |
|
@eendebakpt Thanks for the feedback. I'm not sure whether this rises to the level of a performance-critical use case, but I looked through the in-tree dict-producing The strongest case I found is I benchmarked the complete
The patched build was faster in both orders. so I would characterize this as a small repeatable ~2–3% improvement rather than an exact universal speedup. I also checked a GC-callback workload. The GC callback showed only about a 1% improvement. Benchmark scriptBASE=/path/to/baseline/python.exe
PATCH=/path/to/patched/python.exe
PP=/path/to/pyperf/python
SITEPKG="$("$PP" -c 'import site; print(site.getsitepackages()[0])')"
export PYTHONPATH="$SITEPKG"
run_bench() {
python_bin="$1"
output="$2"
"$python_bin" -m pyperf timeit \
--processes 10 \
--values 10 \
--min-time 0.5 \
-s 'import gc; gc.disable()' \
'gc.get_stats()' \
-o "$output"
}
run_bench "$BASE" /tmp/gcstats-a1.json
sleep 180
run_bench "$PATCH" /tmp/gcstats-b1.json
sleep 180
run_bench "$PATCH" /tmp/gcstats-b2.json
sleep 180
run_bench "$BASE" /tmp/gcstats-a2.json
"$PP" -m pyperf compare_to \
/tmp/gcstats-a1.json \
/tmp/gcstats-b1.json \
--table
"$PP" -m pyperf compare_to \
/tmp/gcstats-b2.json \
/tmp/gcstats-a2.json \
--table |
Speed up
Py_BuildValue()dictionary construction by using_PyDict_SetItem_Take2()indo_mkdict().Both the key and value returned by
do_mkvalue()are owned references. Transferring them directly to the dictionary avoids the INCREF/DECREF round trips performed byPyDict_SetItem()for each pair.The
v == NULLpath remains separate so that insertion is only attemptedafter both objects have been created successfully.
Microbenchmarks
pyperf 2.10.0on macOS x86_64, Intel Core i7-1068NG7, Apple Clang 17.Reverse-order runs were also performed.
The gains are small but repeatable across the tested
Py_BuildValue()dictionary workloads.Validation
test_capiandtest_dict-R 3:3make patchcheckgit diff --check