├── .gitignore ├── LICENSE ├── README.md └── bench.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | Copyright (c) 2024 Lucian Marin 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyBench 2 | 3 | PyBench 2.0 is a Python benchmark inspired by Geekbench. 4 | 5 | The purpose is to optimize modern CPUs for Python and make sure new versions of Python are getting faster. 6 | 7 | It can also be used as a syntetic CPU benchmark to run on computers and servers. 8 | 9 | ## Runtime 10 | 11 | ``` 12 | > python3 bench.py 13 | Compress using BZ2 algorithm: 14 | [========================================] 100.0% 0:00:15 15 | Compress using LZMA algorithm: 16 | [========================================] 100.0% 0:00:16 17 | Calculate Pi using Wallis product: 18 | [========================================] 100.0% 0:00:13 19 | Calculate Fibonacci numbers recursively: 20 | [========================================] 100.0% 0:00:17 21 | Calculate Fibonacci numbers iteratively: 22 | [========================================] 100.0% 0:00:15 23 | Multiply matrices: 24 | [========================================] 100.0% 0:00:16 25 | Benchmark time: 93.9806 seconds 26 | ``` 27 | 28 | ## Benchmark times 29 | 30 | - Python 3.12 on Apple M1 (power): 59.4037s 31 | - Python 3.12 on Apple M1 (battery): 93.9806s 32 | - Python 3.11 on Qualcomm Snapdragon 765G: 187.0722s 33 | - Python 3.11 on Intel Core (Skylake, IBRS, 3792 MHz): 205.7209s 34 | - Python 3.13 on Intel Xeon (2.20 GHz): 329.1787s 35 | 36 | Less is always better! 37 | 38 | Intel Core running at near 4 GHz powers the server hosting [Subreply](https://subreply.com/) - a tiny, but mighty social network. 39 | -------------------------------------------------------------------------------- /bench.py: -------------------------------------------------------------------------------- 1 | from bz2 import BZ2Compressor 2 | from lzma import LZMACompressor 3 | from os import get_terminal_size 4 | from random import random 5 | from string import printable 6 | from time import time, sleep 7 | import threading 8 | 9 | 10 | def progress_bar(iterable, refresh_interval=0.2): 11 | """ 12 | Async-only progress bar generator. 13 | 14 | The progress display is updated from a background thread at 15 | `refresh_interval` seconds so long-running work inside the loop won't 16 | prevent the progress display from refreshing. 17 | """ 18 | 19 | total = len(iterable) 20 | start_time = time() 21 | columns, lines = get_terminal_size() 22 | bar_width = columns - 20 if columns < 60 else 40 23 | 24 | iteration = 0 25 | stop_event = threading.Event() 26 | 27 | def show_progress(iteration_local): 28 | progress = int(bar_width * iteration_local / total) 29 | elapsed_time = time() - start_time 30 | minutes, seconds = divmod(elapsed_time, 60) 31 | hours, minutes = divmod(minutes, 60) 32 | elapsed_str = "{:01}:{:02}:{:02}".format( 33 | int(hours), int(minutes), int(seconds) 34 | ) 35 | 36 | bar = "=" * progress + " " * (bar_width - progress) 37 | percent_complete = round((iteration_local / total) * 100, 1) 38 | output = f"\r[{bar}] {percent_complete: >5}% {elapsed_str}" 39 | print(output, end='', flush=True) 40 | 41 | # handle zero-length iterables safely 42 | if total == 0: 43 | print() 44 | return 45 | 46 | def _worker(): 47 | while not stop_event.is_set(): 48 | show_progress(iteration) 49 | sleep(refresh_interval) 50 | 51 | worker_thread = threading.Thread(target=_worker, daemon=True) 52 | worker_thread.start() 53 | 54 | for i, item in enumerate(iterable, 1): 55 | yield item 56 | iteration = i 57 | 58 | # ensure final state is shown and background worker stops 59 | stop_event.set() 60 | if worker_thread is not None: 61 | show_progress(total) 62 | worker_thread.join(timeout=refresh_interval * 2) 63 | 64 | print() # newline after progress bar completion 65 | 66 | 67 | def pi_wallis(n): 68 | pi = 2. 69 | for i in progress_bar(range(1, n)): 70 | left = (2. * i) / (2. * i - 1.) 71 | right = (2. * i) / (2. * i + 1.) 72 | pi = pi * left * right 73 | 74 | 75 | def fibonacci_recursive(n): 76 | def fibonacci(n): 77 | if n <= 1: 78 | return 1 79 | else: 80 | return fibonacci(n - 1) + fibonacci(n - 2) 81 | for i in progress_bar(range(1, n)): 82 | fibonacci(i) 83 | 84 | 85 | def fibonacci_iterative(n): 86 | first, second = 0, 1 87 | for _ in progress_bar(range(2, n)): 88 | first, second = second, first + second 89 | 90 | 91 | def multiply_matrices(size): 92 | A = [[random() for _ in range(size)] for _ in range(size)] 93 | B = [[random() for _ in range(size)] for _ in range(size)] 94 | C = [[0 for _ in range(size)] for _ in range(size)] 95 | 96 | for i in progress_bar(range(size)): 97 | for j in range(size): 98 | C[i][j] = sum(A[i][k] * B[k][j] for k in range(size)) 99 | 100 | 101 | def compress(n, algo_class, algo_args=[]): 102 | algo = algo_class(*algo_args) 103 | data = printable.encode() 104 | for i in progress_bar(range(n)): 105 | algo.compress(data * n) 106 | algo.flush() 107 | 108 | 109 | def benchmarks(): 110 | print('Compress using BZ2 algorithm:') 111 | compress(n=2**10, algo_class=BZ2Compressor, algo_args=[1]) 112 | 113 | print('Compress using LZMA algorithm:') 114 | compress(n=2**11 + 2**10, algo_class=LZMACompressor) 115 | 116 | print('Calculate Pi using Wallis product:') 117 | pi_wallis(2**21 + 2**20) 118 | 119 | print('Calculate Fibonacci numbers recursively:') 120 | fibonacci_recursive(2**5 + 2**2 + 2 + 1) 121 | 122 | print('Calculate Fibonacci numbers iteratively:') 123 | fibonacci_iterative(2**19 + 2**18) 124 | 125 | print('Multiply matrices:') 126 | multiply_matrices(2**9) 127 | 128 | 129 | def main(): 130 | start_time = time() 131 | benchmarks() 132 | elapsed_time = time() - start_time 133 | print('Benchmark time:', round(elapsed_time, 4), 'seconds') 134 | 135 | 136 | if __name__ == "__main__": 137 | main() 138 | --------------------------------------------------------------------------------