# 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()