The perfect placeholder function for Python

Yash Sanghvi
Analytics Vidhya
Published in
4 min readAug 31, 2020
Pi — Free for commercial use image from maxpixel

I am ashamed to tell you to how many figures I carried these calculations [of Pi], having no other business at that time

-Isaac Newton

Quite often, we need to compare two machines or two architectures for computation speed. Sometimes, we need to estimate the maximum percentage time reduction possible by parallelizing a sequential process. Sometimes, we just need to run a computationally intensive task on a CPU or a thread, or, in other words, keep that CPU or thread busy. In such cases, we often need a placeholder function that is easy to implement, has the execution time in the required range, and is, depending on the computation, easy to parallelize. Turns out that what Newton did to keep himself busy, is also a very suitable task to keep your machine busy.

The function:

In this post, I’ll introduce my favorite function for all such computations. It is based on the Leibniz formula for Pi:

Leibniz formula for Pi

As you can see, this is an infinite series, whose terms progressively get smaller. We harness this property to create a function that would accurately output the first n decimal places of Pi, n being the input.

def get_pi_n_decimals(n):
pi_by_4 = 0
max_denominator = 10**(n+3)
max_iter = int((max_denominator-1)/2) #denominator = 2*iter+1
for i in range(max_iter):
pi_by_4 += ((-1)**i)*(1/(2*i+1))*(10**(n+3))
return int(pi_by_4 * 4/1000)

The underlying idea is that a term like 1/10001 will have very little impact on the first decimal place. Given n, we compute the terms of the series till we reach a denominator of 10^(n+3) . The terms after that won’t affect the nth decimal place significantly, if at all. We return an integer containing all the first n decimal places of Pi.

Here’s the output when this function is executed on my machine:

What’s so special about this function?

Several salient features make this function ideal as a placeholder for computation benchmarking. I’ll list them one by one.

Execution time available in all orders of magnitude

If you pay close attention, the execution time increases by an order of magnitude with a unit increase in n. On my machine, the execution time in milliseconds has as many digits as n, at least for the first few iterations. That makes this very useful. Have a constraint on the time for which the function should execute? Just set n accordingly. It doesn’t matter if you have an ultra-high-speed supercomputer or a single-core machine with Intel Pentium. You can always find an ideal n.

Easy to parallelize:

Because this function contains a for loop, it is very easy to parallelize. We can simply split the iterable in different chunks, and use the chunks as inputs. An example using processes is shown below:

from multiprocessing import Process, Pipedef get_pi_subrange(n,subrange,conn):
pi_by_4 = 0
for i in subrange:
pi_by_4 += ((-1)**i)*(1/(2*i+1))*(10**(n+3))
conn.send([int(pi_by_4 * 4)])
conn.close()
def get_pi_n_decimals_par(n, n_chunks):
max_denominator = 10**(n+3)
max_i = int((max_denominator-1)/2)
ranges = []
for i in range(n_chunks):
r_i = range(int(i*max_i/n_chunks),int((i+1)*max_i/n_chunks))
ranges.append(r_i)
# create a list to keep all processes
processes = []
# create a list to keep connections
parent_connections = []

for subrange in ranges: #This loop can be parallelized
parent_conn, child_conn = Pipe()
parent_connections.append(parent_conn)

process = Process(target=get_pi_subrange, args=(n, subrange, child_conn,))
processes.append(process)

# start all processes
for process in processes:
process.start()
# make sure that all processes have finished
for process in processes:
process.join()

ans = 0
for parent_connection in parent_connections:
ans += parent_connection.recv()[0]
return int(ans/1000)

No delays or network calls

This function is completely CPU-intensive. The execution time is the computation time.

Limited memory consumption

The function successively adds the output of each loop to the final answer. This ensures that the memory usage remains limited. What also helps is that each successive term keeps getting smaller and smaller in magnitude.

No imports required

You don’t need to import any extra libraries like NumPy. You can run this directly if you have python installed. This avoids extra variables in the benchmarking process, like the availability of the library in the machine, its version, etc.

Easily verifiable output:

Though this is a placeholder function, there is some directed computation happening. And the output is a universally known mathematical constant, Pi. So it is easy to verify whether the computation was executed without any errors or not.

Easy to understand and translate

This function, given the Leibniz formula, can easily be understood and translated in other languages, like C, Java, etc. Of course, you will have to respect the language-specific constraints, like the maximum value of an integer in that language and so on.

The Bottom Line:

Pi is beautiful. And useful. And convenient. And …

Just like Pi, I can go on and on. But coming back to the function, it proves to be ideal for several use-cases. It has several of the attributes, if not all, that you would want a placeholder function to have. It is flexible and scalable, and quite easy to understand, remember, and implement. So go ahead and use this function whenever you need to perform some comparisons or when you just want to keep the machine or a thread busy.

--

--