1

Is it possible to iterate the generator object in Python with asyncio? I made a simple function named hash_generator() which return a unique hash. Now I decided to benchmark the loop and I get around 8 seconds for iterating to print 100,000 hashes. Can I run this in async to be able to minimize the time? I read the documentation of it but I am confused. I want to explore async and I want to begin with this problem.

import hashlib
import string
import random
import time


def hash_generator():
    """Return a unique hash"""
    prefix = int(time.time())
    suffix = (random.choice(string.ascii_letters) for i in range(10))
    key = ".".join([str(prefix), str("".join(suffix))])
    value = hashlib.blake2b(key.encode(), digest_size=6).hexdigest()
    return value.upper()


"""Iterating the hashes and printing the time it loaded"""
hashes = (hash_generator() for i in range(100000))
time_before = time.time()
[print(i) for i in hashes]
time_after = time.time()
difference = time_after - time_before
print('Loaded in {0:.2f}sec'.format(difference))
# 40503CBA2DAE
# ...
# A511068F4945
# Loaded in 8.81sec

EDIT 1

The random.choice() function is the main reason why the program was taking too long to run. I recreated the function below, with current time and random string from os.urandom (low collision) as values. I tried multithreading but instead of making the task to run as fast it's taking too slow. Any recommendation to refactor the code below is always welcomed.

import hashlib
import time
import os
import timeit


def hash_generator():
    """Return a unique hash"""
    prefix = str(time.time())
    suffix = str(os.urandom(10))
    key = "".join([prefix, suffix])
    value = hashlib.blake2b(key.encode(), digest_size=6).hexdigest()
    return value.upper()


"""Iterating the hashes and printing the time it loaded"""
print(timeit.timeit(hash_generator, number=100000), "sec")
# 0.497149389999322 sec

EDIT 2

With the help of Jack Taylor and Stackoverflowers I can see the difference by using multiprocessing over 1M iterations. I benchmark the code below.

import hashlib
import time
import os
import timeit
import multiprocessing


def hash_generator(_=None):
    """Return a unique hash"""
    prefix = str(time.time())
    suffix = str(os.urandom(10))
    key = "".join([prefix, suffix])
    value = hashlib.blake2b(key.encode(), digest_size=6).hexdigest()
    return value.upper()


# Allows for the safe importing of the main module
if __name__ == "__main__":
    start_time = time.time()
    number_processes = 4
    iteration = 10000000
    pool = multiprocessing.Pool(number_processes)
    results = pool.map(hash_generator, range(iteration))
    pool.close()
    pool.join()
    end_time = time.time()
    pool_runtime = end_time - start_time
    print('(Pool) Loaded in: {0:.5f} sec'.format(pool_runtime))

    ordinary_runtime = timeit.timeit(hash_generator, number=iteration)
    print('(Ordinary) Loaded in: {0:.5f} sec'.format(ordinary_runtime))

iteration = 10
(Pool) Loaded in: 1.20685 sec
(Ordinary) Loaded in: 0.00023 sec

iteration = 1000
(Pool) Loaded in: 0.72233 sec
(Ordinary) Loaded in: 0.01767 sec

iteration = 1000
(Pool) Loaded in: 0.99571 sec
(Ordinary) Loaded in: 0.01208 sec

iteration = 10,000
(Pool) Loaded in: 1.07876 sec
(Ordinary) Loaded in: 0.12652 sec

iteration = 100,000
(Pool) Loaded in: 1.57068 sec
(Ordinary) Loaded in: 1.23418 sec

iteration = 1,000,000
(Pool) Loaded in: 4.28724 sec
(Ordinary) Loaded in: 11.56332 sec

iteration = 10,000,000
(Pool) Loaded in: 27.26819 sec
(Ordinary) Loaded in: 132.68170 sec
Ninja Warrior 11
  • 362
  • 3
  • 17
  • 1
    No, you cannot. As there is no asynchronous operation in your function. There won't be any benefit. – Sraw May 25 '18 at 06:08
  • 2
    Asyncio will not help you because it runs single-threaded, but you could try [`concurrent.futures`](https://docs.python.org/3/library/concurrent.futures.html). – user4815162342 May 25 '18 at 06:19
  • @user4815162342 I see, thank you for the suggestion. – Ninja Warrior 11 May 25 '18 at 06:32
  • 1
    @NinjaWarrior11 actually what do you mean by "minimize time"? Term "asynchronous" never means that something will be done faster – Andrii Maletskyi May 25 '18 at 08:01
  • @AndriyMaletsky I guess what I am saying is that if I run the loop, normally it will run single process per task, in my case I want every hash_generator would be running like twice per task instead of single process(concurrency? don't know what's the programatic term for this). Instead of 8.81 seconds, the time would be cut into half if the process is doubled. Like if a video editor has a job to finish the video in 5 hours, if he hire one video editor, since they are two now doing the job it would only consume them to finish at around 2.5 hours. I feel dumb with this topic. – Ninja Warrior 11 May 25 '18 at 16:21
  • Like this https://stackoverflow.com/questions/2957116/make-2-functions-run-at-the-same-time and there's a comment by Jonas Elfström "He might want to know that because of the Global Interpreter Lock they will not execute at the exact same time even if the machine in question has multiple CPUs." – Ninja Warrior 11 May 25 '18 at 16:33
  • That comment is not always correct! Whenever Python can get away with it, it releases the GIL, and the hash-calculating functions are among those that do so. Only to notice the difference, you must increase the size of the hashed content - see, for example, [this code](https://pastebin.com/hdprsQ9F) for an example of pure-Python code sped up with simple use of threads. – user4815162342 May 28 '18 at 19:27

1 Answers1

1

It looks like you are probably better off with the sequential version. The conventional wisdom is that, in Python, with I/O-bound jobs (file reads/writes, networking) you can get a speed-up by using an event loop or multiple threads, and with CPU-bound jobs (like computing hashes) you can get a speed-up by using multiple processes.

However, I took your version and rewrote it using concurrent.futures and a process pool, and instead of speeding it up it made it 10 times slower.

Here's the code:

from concurrent import futures
import hashlib
import string
import random
import time

def hash_generator():
    """Return a unique hash"""
    prefix = int(time.time())
    suffix = (random.choice(string.ascii_letters) for i in range(10))
    key = ".".join([str(prefix), str("".join(suffix))])
    value = hashlib.blake2b(key.encode(), digest_size=6).hexdigest()
    return value.upper()

def main(workers = None):
    """Iterating the hashes and printing the time it loaded"""
    time_before = time.time()
    with futures.ProcessPoolExecutor(workers) as executor:
        worker_count = executor._max_workers
        jobs = (executor.submit(hash_generator) for i in range(100000))
        for future in futures.as_completed(jobs):
            print(future.result())
    time_after = time.time()
    difference = time_after - time_before
    print('Loaded in {0:.2f}sec with {1} workers'.format(difference, worker_count))

if __name__ == '__main__':
    main()

# 2BD6056CC0B4
# ...
# D0A6707225EB
# Loaded in 50.74sec with 4 workers

With multiple processes there is some overhead involved with starting and stopping the different processes, and with inter-process communication, which is probably why the multi-process version is slower than the sequential version even though it is using all of the CPU cores.

You could also try using clustering to split the work over multiple computers, and/or writing the algorithm in a lower-level language (Go strikes me as a good choice). But whether that would be worth your while, I don't know.

Jack Taylor
  • 5,588
  • 19
  • 35
  • 1
    Also worth considering: [PyPy](https://pypy.org/) or [Cython](http://docs.cython.org/en/latest/index.html), which can speed up your Python code without you having to rewrite it. – Jack Taylor May 25 '18 at 08:29
  • 1
    Hash calculations sometimes get a speedup even with threads, as many Python wrappers for hash digesters release the GIL. Where I work we used this to good effect to speed up checksumming multiple files and getting multiple cores utilized at once - and all in pure Python. – user4815162342 May 25 '18 at 12:21
  • 1
    Hm, I just tried using ThreadPoolExecutor instead of ProcessPoolExecutor, and it finished in 19.52 seconds. Interesting. – Jack Taylor May 25 '18 at 14:57
  • 1
    I tried that as well, with similar results. (I also removed the `print` because it's not necessary and it only adds noise to measurements.) I suspect the problem is that most of the time is spent in generating the key with `random.choice`, which is pure-Python and serialized by the GIL, and only a fraction in `hashlib`, which releases the GIL. If you make the key fixed, and bump the key size to a much large value, such as 100k bytes, then the threaded code actually becomes faster, and utilizes all the cores. Here is some [code to play with](https://pastebin.com/hdprsQ9F). – user4815162342 May 25 '18 at 18:03
  • Thank you guys for all the support, I already edited and posted the code above. – Ninja Warrior 11 May 26 '18 at 12:34