blob: d2608797f3a4d5eac3625696663023c8ffb2008c [file] [log] [blame]
# Measure the performance of PyMutex locks with short critical sections.
#
# Usage: python Tools/lockbench/lockbench.py [options]
#
# Options:
# --work-inside N Units of work inside the critical section (default: 1).
# --work-outside N Units of work outside the critical section (default: 0).
# Each unit of work is a dependent floating-point
# addition, which takes about 0.4 ns on modern
# Intel / AMD processors.
# --num-locks N Number of independent locks (default: 1). Threads are
# assigned to locks round-robin.
# --random-locks Each thread picks a random lock per acquisition instead
# of using a fixed assignment. Requires --num-locks > 1.
# --acquisitions N Lock acquisitions per loop iteration (default: 1).
# --total-iters N Fixed iterations per thread (default: 0 = time-based).
# Useful for measuring fairness: the benchmark runs until
# the slowest thread finishes.
#
# How to interpret the results:
#
# Acquisitions (kHz): Reports the total number of lock acquisitions in
# thousands of acquisitions per second. This is the most important metric,
# particularly for the 1 thread case because even in multithreaded programs,
# most lock acquisitions are not contended. Values for 2+ threads are
# only meaningful for `--disable-gil` builds, because the GIL prevents most
# situations where there is lock contention with short critical sections.
#
# Fairness: A measure of how evenly the lock acquisitions are distributed.
# A fairness of 1.0 means that all threads acquired the lock the same number
# of times. A fairness of 1/N means that only one thread ever acquired the
# lock.
# See https://en.wikipedia.org/wiki/Fairness_measure#Jain's_fairness_index
from _testinternalcapi import benchmark_locks
import argparse
def parse_threads(value):
if '-' in value:
lo, hi = value.split('-', 1)
lo, hi = int(lo), int(hi)
return range(lo, hi + 1)
return range(int(value), int(value) + 1)
def jains_fairness(values):
# Jain's fairness index
# See https://en.wikipedia.org/wiki/Fairness_measure
return (sum(values) ** 2) / (len(values) * sum(x ** 2 for x in values))
def main():
parser = argparse.ArgumentParser(description="Benchmark PyMutex locks")
parser.add_argument("--work-inside", type=int, default=1,
help="units of work inside the critical section")
parser.add_argument("--work-outside", type=int, default=0,
help="units of work outside the critical section")
parser.add_argument("--acquisitions", type=int, default=1,
help="lock acquisitions per loop iteration")
parser.add_argument("--total-iters", type=int, default=0,
help="fixed iterations per thread (0 = time-based)")
parser.add_argument("--num-locks", type=int, default=1,
help="number of independent locks (round-robin assignment)")
parser.add_argument("--random-locks", action="store_true",
help="pick a random lock per acquisition")
parser.add_argument("threads", type=parse_threads, nargs='?',
default=range(1, 11),
help="Number of threads: N or MIN-MAX (default: 1-10)")
args = parser.parse_args()
header = f"{'Threads': <10}{'Acq (kHz)': >12}{'Fairness': >10}"
if args.total_iters:
header += f"{'Wall (ms)': >12}"
print(header)
for num_threads in args.threads:
acquisitions, thread_iters, elapsed_ns = \
benchmark_locks(
num_threads, args.work_inside, args.work_outside,
1000, args.acquisitions, args.total_iters,
args.num_locks, args.random_locks)
wall_ms = elapsed_ns / 1e6
acquisitions /= 1000 # report in kHz for readability
fairness = jains_fairness(thread_iters)
line = f"{num_threads: <10}{acquisitions: >12.0f}{fairness: >10.2f}"
if args.total_iters:
line += f"{wall_ms: >12.1f}"
print(line)
if __name__ == "__main__":
main()