CODE #2: TIMING AND OPTIMIZE YOUR PYTHON PROGRAMS FOR BLAZING SPEED
Learning to use time.time(), time.perf_counter(), multithreading and the JAX, NumPy and Numba libraries to boost pure Python performance

Using a logo doesn’t mean endorsement from the maintaining team. Logos used are: Numba, NumPy, Python, JAX, LLVM. Code was generated with LLMs for a benchmarking in Part VIII of the article.
CODE #2: TIMING AND OPTIMIZE YOUR PYTHON PROGRAMS FOR BLAZING SPEED
Learning to use time.time(), time.perf_counter(), multithreading and the JAX, NumPy and Numba libraries to boost pure Python performance
I. INTRODUCTION
Last time, we looked at assembly outputs of ARM64 GCC compilers, for a printf() script written in C, using Godbolt (Compiler Explorer) [11].
It was a low-level, hands-on approach. I plan in this series to cover such technical sides of programming, but not always that deep. For the sake of balance, I would rather dedicate this piece to discussing a trick I found interesting.
It’s to time your Python programs and scripts!
- This can be done with several functions found within the time module.
- This flexibility means you can more precisely adjust your benchmarking.
- It allows you to compare different sessions of a similar script or program on a single device, or to scale with different devices or resource allocations.
- This is a good way to train yourself to think more in terms of optimization — comparing different implementations of a similar algorithm — , one of the trade-offs of Python being virtualization (of C), and thus, overhead.
But how do you concretely go on, and do that?
II. SIMPLE IMPLEMENTATION
The most simple way to get started, and one I used for a specific case, benchmarking API fetching times for algorithmic mapmaking, consists in this short sample of code [1] :
# Source - https://stackoverflow.com/a/1557584
# Posted by rogeriopvl, modified by community. See post 'Timeline' for change history
# Retrieved 2026-06-19, License - CC BY-SA 3.0
# Adapted by Emilia Lilith-Lolita Hoarfrost, June 19, 2026.
import time
start_time = time.time()
if __ name __ == '__ main __':
main()
print("--- %s seconds ---" % (time.time() - start_time))
Note: I took the liberty to adapt something which may have been deprecated Python (there was no “if name == ‘ main ’:”) at first. But feel free to experiment as it may depend on your own implementation context, for instance due to an unmaintained library you may be using an old Python version.
According to the responder, “This assumes that your program takes at least a tenth of second to run.” [1]
Of course, you can format for a more readable output compared to scientific notation — though it’s useful to possess for compactness of infinitesimal variations and for very critical applications. Or for very precise benchmarking.
But if you’re ever tasked with so critical code, that it is mandatory to use scientific notation, perhaps your organization (or yourself) should lean into languages with less overhead or virtualization runtime, for instance C++ or Rust (as opposed to C which may mean more memory leaks).
III. MORE UP-TO-DATE TIMING
“[time.perf_counter() → float] Return the value (in fractional seconds) of a performance counter, i.e. a clock with the highest available resolution to measure a short duration. It does include time elapsed during sleep. The clock is the same for all processes. The reference point of the returned value is undefined, so that only the difference between the results of two calls is valid.” [2]
import time
y = 1.5
def perf():
t1 = time.perf_counter()
for x in range(100000):
x ** y
pass
t2 = time.perf_counter()
time.sleep(1)
t3 = time.perf_counter()
loop_time = t2 - t1
sleep_time = t3 - t2
total_time = t3 - t1
print(f"Loop time: {loop_time:.6f} seconds")
print(f"Sleep time: {sleep_time:.6f} seconds")
print(f"Total time: {total_time:.6f} seconds")
perf()
This example takes partial inspiration from a Stack Overflow thread [3]. What the script does can be delineated in several steps.
- It imports the time module.
- It declares the y variable, float of value 1.5. Note that you could remove the y entirely from the script and it would still showcase the behavior of time.perf_counter(), or even make it an int. But floats are different from integers. Floats are implemented differently based on architecture instruction sets. This is multiplied by the amount of x in range() operations, to show a sometimes critical gap in performance measured when y is int VS float.
- We define the perf() function which is later called.
- The perf() function declares three timers: one before the loop, one after the loop and before the sleep, and one after both the loop and the subsequent sleep. Note that time.sleep() has this definition in the documentation:
“Suspend execution of the calling thread for the given number of seconds. The argument may be a floating-point number to indicate a more precise sleep time.
If the sleep is interrupted by a signal and no exception is raised by the signal handler, the sleep is restarted with a recomputed timeout.
The suspension time may be longer than requested by an arbitrary amount, because of the scheduling of other activity in the system.” [2]
- Then, operations are done to store in float variables a few timers (loop_time, sleep_time, total_time), before being printed and formatted. It stores up to the sixth decimal to show more significant results (because modern hardware is that powerful…), but you can adapt as you need.
IV. IDENTIFYING A SIMPLE BOTTLENECK
For educational purposes, the following sample will contain a way that is optimal, but also one that is suboptimal, of calling a global variable to loop through and operate on this variable.
import time
N = 10_000_000
y = 100
def global_lookup():
total = 0
for i in range(N):
total += y # global lookup each iteration
return total
def local_lookup():
y_local = y
total = 0
for i in range(N):
total += y_local # local lookup (faster)
return total
def bench(function):
t1 = time.perf_counter()
function()
return time.perf_counter() - t1
runs = 5
g = 0
l = 0
for _ in range(runs):
g += bench(global_lookup)
l += bench(local_lookup)
print(f"Global: {g / runs:.6f}s")
print(f"Local: {l / runs:.6f}s")
print(f"Speed-up: {g / l:.3f}x")
On my local implementation, across 10 iterations of the script, the speed-up average can be rounded as 1,04. So basically, local look-up is to be about 5% more efficient. Surely this doesn’t seem impressive.
But it’s about the philosophy of programming, something common to every programmer, and every program being used. Seeking every possible ounce of optimization, on something deployed at scale, every single day, hour, second. This can be tremendously powerful for very different applications, considering the ripe ecosystem of Python libraries.
What does this program do, in several steps?
- It imports the time module, again.
- It instanciates two variables, an integer y = 100, which will be our simple global variable. And N, of value 10 millions, to be the common number of iterations across the simulation.
- Then, global_lookup() and local_lookup() are defined to use the local and global variables across N.
- bench() is defined to start a timer, having entered a function as argument this function is called, and then is returned a new timer compared with the timer instanciated prior to the execution of the entered function.
- runs = 5 is an integer defining a number of iterations to smoothe out possible differentials of performance across runs. Changing it to 10, I reached a mean of 1,04 so still close to a 5% increase in performance.
- Then, g and l are initialized variables to store the amount of current runs for both the global and local look-ups. We use them to account for bench() function instances.
- Are printed the times of global_lookup() runs normalized by runs and the same for local_lookup(), and the differential is displayed as percentage accounting up to the third decimal.
V. Numba and NumPy
# 1) IMPORTS
# ----------------------------
import time
import numpy as np
from numba import njit
import math
# 2) CONFIG
# ----------------------------
N = 500_000
RUNS = 100
theta = 0.5
cos_t = math.cos(theta)
sin_t = math.sin(theta)
positions = [[1.0, 2.0] for _ in range(N)]
numpy_array = np.array(positions, dtype=np.float64)
# 3) PURE PYTHON ROTATION
# ----------------------------
def pure_python_rotate(data, cos_t, sin_t):
result = []
for x, y in data:
x_new = x * cos_t - y * sin_t
y_new = x * sin_t + y * cos_t
result.append([x_new, y_new])
return result
# 4) NUMPY ROTATION
# ----------------------------
def numpy_rotate(arr, cos_t, sin_t):
x = arr[:, 0]
y = arr[:, 1]
x_new = x * cos_t - y * sin_t
y_new = x * sin_t + y * cos_t
return np.stack((x_new, y_new), axis=1)
# 5) NUMBA ROTATION
# ----------------------------
@njit
def numba_rotate(arr, cos_t, sin_t):
out = np.empty_like(arr)
for i in range(arr.shape[0]):
x = arr[i, 0]
y = arr[i, 1]
out[i, 0] = x * cos_t - y * sin_t
out[i, 1] = x * sin_t + y * cos_t
return out
# 6) WARM-UP (important for Numba)
# ----------------------------
numba_rotate(numpy_array, cos_t, sin_t)
# 7) BENCHMARK FUNCTION
# ----------------------------
def benchmark(func, *args):
times = []
for _ in range(RUNS):
start = time.perf_counter()
func(*args)
end = time.perf_counter()
times.append(end - start)
return sum(times) / len(times)
# 8) RUN BENCHMARKS
# ----------------------------
t_python = benchmark(pure_python_rotate, positions, cos_t, sin_t)
t_numpy = benchmark(numpy_rotate, numpy_array, cos_t, sin_t)
t_numba = benchmark(numba_rotate, numpy_array, cos_t, sin_t)
# 9) RESULTS
# ----------------------------
print("\n--- Results (average over runs) ---")
print(f"Pure Python: {t_python:.6f} sec")
print(f"NumPy : {t_numpy:.6f} sec")
print(f"Numba : {t_numba:.6f} sec")
# 10) SPEED-UPS
# ----------------------------
def speedup(base, other):
return base / other
print("\n--- Speedups ---")
print(f"NumPy : {speedup(t_python, t_numpy):.2f}× faster than Python")
print(f"Numba : {speedup(t_python, t_numba):.2f}× faster than Python")
print(f"Numba : {speedup(t_numpy, t_numba):.2f}× faster than NumPy")
According to NVIDIA [4, 5]:
“NumPy is a powerful, well-optimized, free open-source library for the Python programming language, adding support for large, multi-dimensional arrays (also called matrices or tensors). NumPy also comes equipped with a collection of high-level mathematical functions to work in conjunction with these arrays. These include basic linear algebra, random simulation, Fourier transforms, trigonometric operations, and statistical operations.
NumPy stands for ‘numerical Python’, and builds on the early work of the Numeric and Numarray libraries with the goal to give fast numeric computation to Python. Today NumPy has numerous contributors and is sponsored by NumFOCUS.
As the core library for scientific computing, NumPy is the base for libraries such as Pandas, Scikit-learn, and SciPy. It’s widely used for performing optimized mathematical operations on large arrays.” [4]
Whereas for Numba [5]:
“Numba translates Python byte-code to machine code immediately before execution to improve the execution speed.
Numba can be used to optimize CPU and GPU functions using callable Python objects called decorators. A decorator is a function that takes another function as input, modifies it, and returns the modified function to the user. This modularity reduces programming time and increases Python’s extensibility.
Numba also works with NumPy, an open-source Python library of complex mathematical operations designed for processing statistical data. When invoked with a decorator, [Numba] translates a subset of Python and/or NumPy code into bytecode automatically optimized for the environment. It uses LLVM, an open source API-oriented library for programmatically creating machine-native code. Numba offers several options for quickly parallelizing Python code for various CPU and GPU configurations, sometimes with only a single command. When used in conjunction with NumPy, Numba generates specialized code for different array data types and layouts to optimize performance.” [5]
![Figure 1. Simple chart to compare NumPy and Numba. Generated partly with Perplexity. Uses data from [6].](https://miro.medium.com/v2/resize:fit:521/1*bLlaKuerUkjrW4QNgsMctg.png)
Figure 1. Simple chart to compare NumPy and Numba. Generated partly with Perplexity. Uses data from [6].
VI. Using JAX
Apparently, there is a JAX library that also helps to optimize Python code. But it seems to be catering to neural networks and doing JIT compiling. We may touch upon it in a different section.
There are 10 steps in different commented blocks to guide reading, but let’s discuss the steps in detail.
- Imports are made (numpa, numpy, time and math, a library for operations).
- Global variables are declared, notably the positional array positions[], the theta small angle for rotation calculations that remain small, and the initialization of NumPy’s array inheriting the priorly mentioned positional array. N corresponds to iterative steps in benchmarked loops, and runs correspond to passes of the algorithm to smoothe out potential performance differentials. The idea being to neuter effect size biases.
- The math library is being used to calculate the rotation using a simple formula. From step 3 to step 5, the rotations are defined to use differently specific implementations.
- Step 6 is a bit particular: it warms-up Numba.
- Step 7 is defining the benchmarking function to account for runs and for N-sized loops.
- Step 8 runs the benchmarking functions.
- Step 9 formats results.
- Step 10 prints results.
Because of the runs numbering 100, there should be more consistency in this assessment compared to ones from prior sections. Again, assessments depend on implementations, so my own machine may run differently to yours.
NumPy faster than Python by a factor of 26,919. Numba faster by a factor of 122,351 (not accounting for pre-warm-up time; not to mention there is actually some inconsistency with two statistical outliers in runs 1 and 9, where it was respectively 76,28 and 77.92, but because they’re so similar I think it’s a similar thing occurring) and 4,573 times faster to optimize an operation with Numba rather than simply with NumPy.
Additional tip I wasn’t necessarily planning to discuss, but I guess it can be useful to speed up your development process. To do the benchmarking for this section, I actually also used this Python script (courtesy of ChatGPT my beloved).
import subprocess
OUTPUT_FILE = "times.txt"
SCRIPT = "times.py"
RUNS = 10
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
for i in range(RUNS):
f.write(f"\n================ RUN {i+1} ================\n\n")
result = subprocess.run(
["python", SCRIPT],
capture_output=True,
text=True
)
f.write(result.stdout)
if result.stderr:
f.write("\n--- STDERR ---\n")
f.write(result.stderr)
print(f"Done. Results saved to {OUTPUT_FILE}")
It is just a helper script that runs the other script a definite number of times, opening gracefully — that is, not using deprecated functions, but instead using “with” — a file. In case of errors, it will write out a message in stderr, otherwise by the end it informs the user of the end of execution and file path to seek the given results.
A tip that can also be useful to combine with any aforementioned and to come sections, is to have a spinner in CLI represent on-going execution, in case you’re sitting in front of a CLI, or perhaps even print each time a run is completed.
It is working on Windows 11, and supports multithreading so that you can have a thread executing the spinner in the terminal while also executing other functions in the back. This feedback informs you on the state of execution of your entire program. You could perhaps also time the execution failures with a timestamping system, and failure cases with different return values. The executed thread right now uses sleep() to provide a working template.
import sys
import time
import threading
class Spinner:
def __init__(self, message="Loading..."):
self.frames = ["|", "/", "-", "\\"]
self.message = message
self.running = False
self.thread = None
def _spin(self):
i = 0
while self.running:
frame = self.frames[i % len(self.frames)]
sys.stdout.write(f"\r{frame} {self.message}")
sys.stdout.flush()
time.sleep(0.1)
i += 1
def start(self):
if self.running:
return
self.running = True
self.thread = threading.Thread(target=self._spin)
self.thread.start()
def stop(self):
self.running = False
if self.thread:
self.thread.join()
sys.stdout.write(" completed!")
sys.stdout.flush()
# -------------------
# Example usage
# -------------------
if __name__ == "__main__":
spinner = Spinner("Processing script")
spinner.start()
time.sleep(3)
spinner.stop()
Using JAX to optimize machine learning
What is JAX? A library used for optimizing machine learning.
“[*] JAX provides a unified NumPy-like interface to computations that run on CPU, GPU, or TPU, in local or distributed settings [distributed here means for instance a cluster of machines that count as united being together].
[*] JAX features built-in Just-In-Time (JIT) compilation via Open XLA, an open-source machine learning compiler ecosystem.
[*] JAX functions support efficient evaluation of gradients via its automatic differentiation transformations.
[*] JAX functions can be automatically vectorized to efficiently map them over arrays representing batches of inputs.” [7]
# 1) IMPORTS
# -----------------------
import time
import gc
import numpy as np
import jax
import jax.numpy as jnp
from jax import jit, grad, lax
# 2) Config
# -----------------------
N_SAMPLES = 2000
N_FEATURES = 32
STEPS = 1000
LR = 0.01
TRIALS = 100
print("Device:", jax.devices()[0])
# 3) Dataset
# -----------------------
np.random.seed(0)
X = np.random.randn(N_SAMPLES, N_FEATURES).astype(np.float32)
true_W = np.random.randn(N_FEATURES, 1).astype(np.float32)
y = X @ true_W + 0.1 * np.random.randn(N_SAMPLES, 1).astype(np.float32)
X_j = jnp.array(X)
y_j = jnp.array(y)
# 4) Model
# -----------------------
def loss(W):
pred = X_j @ W
return jnp.mean((pred - y_j) ** 2)
grad_loss = jit(grad(loss))
@jit
def step(W):
g = grad_loss(W)
return W - LR * g
# scan version (fully compiled loop)
def scan_body(W, _):
g = grad_loss(W)
return W - LR * g, None
@jit
def train_scan(W):
return lax.scan(scan_body, W, None, length=STEPS)[0]
# 5) Python baseline
# -----------------------
def python_train():
W = np.zeros((N_FEATURES, 1), dtype=np.float32)
gc.disable()
start = time.perf_counter()
for _ in range(STEPS):
y_pred = X @ W
error = y_pred - y
grad_W = (X.T @ error) / N_SAMPLES
W -= LR * grad_W
gc.enable()
return time.perf_counter() - start
# 6) JAX step-loop
# -----------------------
def jax_step_train():
W = jnp.zeros((N_FEATURES, 1))
# warmup compile
W = step(W).block_until_ready()
start = time.perf_counter()
for _ in range(STEPS):
W = step(W)
W.block_until_ready()
return time.perf_counter() - start
# 7) JAX scan-loop (best version)
# -----------------------
def jax_scan_train():
W = jnp.zeros((N_FEATURES, 1))
# warmup compile
W = train_scan(W).block_until_ready()
start = time.perf_counter()
W = train_scan(W)
W.block_until_ready()
return time.perf_counter() - start
# 8) RUN
# -----------------------
py_times = []
jit_times = []
scan_times = []
print(f"\n--- Python ({TRIALS}) ---")
for _ in range(TRIALS):
py_times.append(python_train())
print(f"\n--- JAX JIT step ({TRIALS}) ---")
for _ in range(TRIALS):
jit_times.append(jax_step_train())
print(f"\n--- JAX scan ({TRIALS}) ---")
for _ in range(TRIALS):
scan_times.append(jax_scan_train())
# 9) RESULTS
# -----------------------
print("\n================ SUMMARY ================")
print(f"Python avg: {np.mean(py_times):.6f}s")
print(f"JAX step avg: {np.mean(jit_times):.6f}s "
f"(speedup {np.mean(py_times)/np.mean(jit_times):.2f}x)")
print(f"JAX scan avg: {np.mean(scan_times):.6f}s "
f"(speedup {np.mean(py_times)/np.mean(scan_times):.2f}x)")
Step 1 imports time for the time.perf_counter() timestamps. gc is a library to control the garbage collector, which is useful for memory inspection or manual memory clean-ups, but can introduce lag spikes (notorious in Minecraft PvP is the JVM garbage collection, which can be manipulated with arguments). NumPy is just for efficient array calculus. JAX is also imported. jax.numpy or jnp is different: it “closely mirrors the NumPy API and provides easy entry into JAX.” [7]. From JAX are also taken jit (for just-in-time compilation, grad for derivatives calculus [derivatives are like, in a sine wave, the coefficient at which a discrete point of the continous function dramatically differs from a previous point: constant functions, where f(x) = c (NOT depending on x!) is derivated by f’(x) = 0.]. As for lax: “jax.lax is a library of primitives operations that underpins libraries such as jax.numpy. Transformation rules, such as JVP and batching rules, are typically defined as transformations on jax.lax primitives.” [8].
Step 2 sets up global variables, as well as prints the first index in the array of jax.devices()[]. Here one can configure the amount of samples, features, steps, the LR (Learning Rate), and trials (100 to limit effect size bias) that compare between JAX and NumPy to see how fast each is going.
Step 3 puts a NumPy pseudo-random seed of 0 every iteration, so that speeds are relevant to compare. This ensures reproducibility. Then, the snippet sets up a synthetic dataset (artificial data produced algorithmically, not from real events) for a linear regression problem. A ground truth is generated.
“In the field of data science, ground truth data represents the gold standard of accurate data. It enables data scientists to evaluate model performance by comparing outputs to the “correct answer” (data based on real-world observations). This validates that machine learning (ML) models produce accurate results that reflect reality.” [9]
The classic linear regression formula is used with “y = X @ true_W + 0.1 np.random.randn(N_SAMPLES, 1).astype(np.float32)”, although the 0.1 … part of the equation adds Gaussian noise (factor epsilon). Without noise, the relationship would be far too easy for the model to understand. Then we convert that to JAX arrays for optimal calculus.
Step 4 defines a loss function using current weights W, as input. “pred = X_j @ W” makes a prediction. Then, the mean squared error (MSE), also known as mean squared deviation, is calculated. It’s the difference between the predictions and the answers. Squaring makes all numbers positives. A lower score means a better model. “grad_loss = jit(grad(loss))” calculates the derivative (which could be gradient too), and does something to convert to just-in-time compilation. The step function is defined to take W as argument, as well as gradient of loss on the W value, to return “*W — LR g”, which moves the weights, scaled by the Learning Rate value in the opposite direction of the error to improve the model.**
Then, the scanbody(W, ) function is iterated on a loop whose index doesn’t matter, hence the convention of underscore “_”. It does *“W — LR g” again, and is a special helper function required by lan.scan. It calculates the new weights to give the updated weights, and the absence of things to remember, that is None (again, it’s a special helper, so it’s boilerplate). The following block is compiled optimally by the @jit decorator, and defines a function to train and scan the W variable. “return lax.scan(scan_body, W, None, length=STEPS)[0]” runs the scanbody(W, ) function over STEPS times, replacing a classical for loop**.
Step 5 sets up the linear regression model using Python and NumPy with a standard for loop. This is meant to be slower than the JAX implementations, then timed and compared. gc.disable() is interesting: we disable the garbage collector, which may artificially slow down the loop. Both models use the same Learning Rate and the same STEPS. Then we enable back the garbage collector after each trial iteration, so that the next time there are more resources to allocate to the loop of this NumPy gradient regression model.
Steps 6 and 7 do a similar thing, except that step 7 is faster because it relies on XLA compilation, ideal for “[taking] models from popular ML frameworks such as PyTorch, TensorFlow, and JAX, and [optimizing] them for high-performance execution across different hardware platforms including GPUs, CPUs, and ML accelerators.” [10].
Steps 8 and 9 consist of running the different trials and printing results once per trial and averaged after the hundredth trial.
@jit
def train_scan(W):
return lax.scan(scan_body, W, None, length=STEPS)[0]
This is the snippet that really makes all the difference.
Anyway, here’s the result of comparing all 100 trials on my local implementation (JAX steps are like 2.1 times better than the NumPy regression model, JAX scans are 2,7 times better):
Python avg: 0.013007s
JAX step avg: 0.006163s (speedup 2.11x)
JAX scan avg: 0.004753s (speedup 2.74x)
VII. Multithreading optimization
We’ve seen above with the spinner that multithreading can be used to have a little more information on the CLI during execution. But it can actually process instructions faster.
Figure 2. Multithreading use for Python. Generated partially via ChatGPT, rendered by PlantUML Web Server. “CODE #2: TIMING AND OPTIMIZE YOUR PYTHON PROGRAMS FOR BLAZING SPEED”, Emilia Lilith-Lolita Hoarfrost, Medium, June 19, 2026.
import sys, time, socket, threading
HOST, PORT = "127.0.0.1", 50007
def server():
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
while True:
c, _ = s.accept()
with c:
c.recv(1024)
def start():
threading.Thread(target=server, daemon=True).start()
def ping():
t = time.perf_counter()
with socket.socket() as s:
s.connect((HOST, PORT))
s.sendall(b"x")
return time.perf_counter() - t
def run(n, thr=False):
res = [0]*n
t0 = time.perf_counter()
if thr:
def w(i):
res[i] = ping()
print(f"T{i}: {res[i]*1000:.2f}ms")
th = [threading.Thread(target=w, args=(i,)) for i in range(n)]
[t.start() for t in th]
[t.join() for t in th]
else:
for i in range(n):
res[i] = ping()
print(f"S{i}: {res[i]*1000:.2f}ms")
return time.perf_counter() - t0
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 10
print(f"\nn={n}\n")
start()
time.sleep(0.2)
t1 = run(n, False)
print(f"\nSEQ {t1:.4f}s\n")
t2 = run(n, True)
print(f"THR {t2:.4f}s\n")
print(f"speedup {t1/t2:.2f}x")
if __name__ == "__main__":
main()
The first step in the above script is importing sys, socket, time and threading. Socket is there to do networking.
“A network socket is a software structure within a network node of a computer network that serves as an endpoint for sending and receiving data across the network. The structure and properties of a socket are defined by an application programming interface (API) for the networking architecture. Sockets are created only during the lifetime of a process of an application running in the node.
Because of the standardization of the TCP/IP protocols in the development of the Internet, the term network socket is most commonly used in the context of the Internet protocol suite, and is therefore often also referred to as Internet socket. In this context, a socket is externally identified to other hosts by its socket address, which is the triad of transport protocol, IP address, and port number.
The term socket is also used for the software endpoint of node-internal inter-process communication (IPC), which often uses the same API as a network socket.” [12]
The second step is defining HOST, PORT = “127.0.0.1”, 50007 as a string and integer.
The last step is a main entry-point.
The server function is defined. It is a TCP server loop. A socket is created upon calling the function. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sets it up to re-use the same address or port. s.bind((HOST, PORT)) attaches the socket to the HOST and PORT values. The server can then be reached at this address. It is then put in listening mode by s.listen(). The operating system queues connection requests. while True: is an infinite loop, which upon a connection c, _ = s.accept(), that is to say no matter the client address, a new socket is dedicated to the client. But with c: ensures automatic closure of the socket, if errors happen for instance, to prevent resource leaks. c.recv(1024) permits to receive up to 1024 bytes from the client. It simuates a minimal I/O workload.
The start() function is defined, with threading.Thread(target=server, daemon=True).start(). It creates a new thread, running server(), and being a deamon thread. It means it stops when the main program exits.
The ping() function, timed before and after, opens the socket gracefully (“with”), connects to HOST and PORT, and “s.sendall(b“x”)” sends raw bytes to the server.
The run(n, thr=False) function chooses a sequential mode (as opposed to multithreading). Timers are put before and after. This is the function that enables the sequential mode for a single thread to run the pings or the multithreading mode, though main() tells it the bool’s value: “t1 = run(n, False) […] t2 = run(n, True)”.
On 10 packets per mode, I find 1.34 times faster to use multithreading (out of 9 attempts, so 180 combined threads, again locally, may vary in other set-ups). But for 100 packets per mode on 3 tries, I found a 8% (or around 1.08 times) speed advantage (total of 600 packets). So perhaps there’s a decreasing efficiency even with multithreading.
Edit (June 20, 2026): To clarify, in the context of sockets and network programming, because we are relying on hardware ports, a concern as to the scalability of such an optimization is that there are about “There are 65,535 possible port numbers” [13], and that “although not all are in common use[, some] of the most commonly used ports” [13] are already used by commonly used networking protocols, such as 22 SSH, 443 HTTPS, 500 ISAKMP… In fact the ephemeral ports (or perhaps RFC 6056’s dynamic ports) constitute a pool we should use more. “[The] dynamic ports consist of the range 49152–65535. However, ephemeral port selection algorithms should use the whole range 1024–65535.” [14]
VIII. Concluding
- In part I, we saw that it was possible to use timers to benchmark, and possibly improve Python execution speed, though the method is now deprecated.
- In part II, we saw one of the simplest implementations with time.time().
- In part III, we saw time.perf_counter() was a more updated and efficient way to time Python scripts.
- In part IV, we identified a bottleneck in trying to look-up a global variable iterated over a loop in comparison to a local variable. The boost was 5%, and told us something about function scopes.
- In part V, we saw that NumPy and Numba could both optimize a function doing rotations over an array. NumPy was 27 times faster than pure Python, and Numba faster by 122. Numba 4,5 times faster than NumPy.
- In part VI, we compared JAX and NumPy to calculate a gradient regression model. We saw that JAX using the most OpenXLA was the most optimal for machine-learning. JAX step method had an average speed-up of 2.1 times, JAX scan an average of 2.7 times.
- In part VII, we compared sequential versus multithreaded pings to a local host socket, to compare the difference in speed. Across a total of 180 packets, multithreading was 1.34 times faster.
- All in all, this article showed several ways to measure, benchmark and optimize your Python coding experience.
IX. REFERENCES
[1] Thread. “How do I get time of a Python program’s execution?”, john2x (October 12, 2009), HDJEMAI (June 8, 2020), Stack Overflow. https://stackoverflow.com/questions/1557571/how-do-i-get-time-of-a-python-programs-execution
[2] Documentation. “time — Time access and conversions”, Generic Operating System Services, The Python Standard Library, Python 3.14.6 documentation, last modified November 8, 2025. https://docs.python.org/3.14/library/time.html#time.perf_counter
[3] Thread. “Python: What is the time.perf_counter method’s time reference point?”, M Sty, s3dev, Stack Overflow, June 27, 2022. https://stackoverflow.com/questions/72770834/python-what-is-the-time-perf-counter-methods-time-reference-point
[4] Entry. “What Is NumPy and Why Does It Matter?”, Glossary, NVIDIA.
https://www.nvidia.com/en-us/glossary/numpy/
[5] Entry. “What Is Numba and Why Does It Matter?”, Glossary, NVIDIA.
https://www.nvidia.com/en-us/glossary/numba/
[6] Article. “Supercharging NumPy with Numba”, Abhishek Sharma, TDS Archive, Medium, March 17, 2021.
https://medium.com/data-science/supercharging-numpy-with-numba-77ed5b169240
[7] Documentation. “Quickstart: How to think in JAX”, JAX, c. 2024.
https://docs.jax.dev/en/latest/notebooks/thinking_in_jax.html
[8] Documentation. “jax.lax module”, JAX, c. 2024.
https://docs.jax.dev/en/latest/jax.lax.html
[9] Article. “What is ground truth?”, Tom Krantz , Alexandra Jonker, IBM.
https://www.ibm.com/think/topics/ground-truth
[10] Repository. “xla”, GitHub, June 19, 2026.
https://github.com/openxla/xla
[11] Article. “CODE #1: THE ASSEMBLY BEHIND PRINTING HELLO WORLD IN C”, Emilia Lilith-Lolita Hoarfrost, Medium, June 17, 2026.
[12] Entry. “Network socket”, Wikipedia, May 8, 2007— March 4, 2026.
https://en.wikipedia.org/w/index.php?title=Network_socket&oldid=1341705620
[13] Article. “What is a computer port? | Ports in networking”, Cloudflare, 2026.
[14] Request for comment. “Recommendations for Transport-Protocol Port Randomization”, Fernando Gont, Michael Larsen, RFC 6056, Internet Engineering Task Force, January 2011.
https://datatracker.ietf.org/doc/html/rfc6056
Follow, clap, highlight, repost for more content and to show appreciation. Comment to discuss. Look at the Technology list for similar-minded articles.
메타데이터
- post_id
- dd077fb377be
- slug
- code-2-timing-and-optimize-your-python-programs-for-blazing-speed-dd077fb377be
- url
- https://medium.com/@emiliahoarfrost/code-2-timing-and-optimize-your-python-programs-for-blazing-speed-dd077fb377be
- canonical_url
- https://medium.com/@emiliahoarfrost/code-2-timing-and-optimize-your-python-programs-for-blazing-speed-dd077fb377be
- author_url
- https://medium.com/@emiliahoarfrost
- status
- ok
- fetched_at
- 2026-06-26 21:52:29