import math
from decimal import Decimal
from itertools import islice

def blhsing(numbers):
    return Decimal(math.gcd(*numbers)).normalize().as_tuple().exponent

def batched(iterable, n):
    "Batch data into tuples of length n. The last batch may be shorter."
    # batched('ABCDEFG', 3) --> ABC DEF G
    if n < 1:
        raise ValueError('n must be at least one')
    it = iter(iterable)
    while batch := tuple(islice(it, n)):
        yield batch

def Kelly(numbers):
    E = float('inf')
    for e in map(blhsing, batched(numbers, 100)):
        if e < E:
            E = e
            if not E:
                return 0
    return E

funcs = blhsing, Kelly

from random import randrange
from timeit import timeit, default_timer as timer
from statistics import mean, fmean, stdev
import sys

d = [randrange(100) * 10 ** randrange(100) for i in range(10000)]

times = {f: [] for f in funcs}
def stats(f):
    ts = [t * 1e3 for t in sorted(times[f])[:5]]
    return f'{mean(ts):6.3f} ± {stdev(ts):4.3f} ms '
for _ in range(50):
    for f in funcs:
        t0 = timer()
        f(d)
        times[f].append(timer() - t0)
for f in sorted(funcs, key=stats):
    print(stats(f), f.__name__)

print('\nPython:', sys.version)