Timing Tests of fastnumbers Functions Compared to Equivalent Solutions

In order for you to see the benefit of fastnumbers, some timings are collected below for comparison to equivalent python implementations. The numbers may change depending on the machine you are on or the Python version you are using.

Feel free to download this Jupyter Notebook and run the tests yourself to see how fastnumbers performs on your machine (it takes about 1-2 minutes total).

This notebook contains timing results for Python 3.6.

Some notes about the data

  • Each test is the time it takes for the function to run 100,000 times on a given input.
  • Each test is repeated either 5 or 100 times, and the mean ± standard deviation is reported.
  • The fastest time is shown in bold
  • The timing results for the pure-Python functions include about 10-15 ms of "function call overhead"; the fastnumbers functions do not suffer from as much overhead because they are C-extensions.
  • Python version-dependent behaviors:
    • Python 2.7 has a particularly slow int function, so the fastnumbers speedup is much larger on Python 2.7 than Python 3.x
    • Python >=3.6 is slightly slower in general than previous versions because underscores are now allowed in floats and integers which makes parsing take a bit longer due to the extra logic.

Notes about the Timing class below

The timing runner class is implemented below, and this is used in all the tests to perform the actual timing tests in the sections below. In general you can skip this implementation, but of note is the THINGS_TO_TIME tuple, which contains the values that are passed to the functions to type the various input types.


In [1]:
from __future__ import print_function, division
import re
import math
import timeit
from IPython.display import Markdown, display, clear_output

class Timer(object):
    """Class to time functions and make pretty tables of the output."""
    
    # This is a list of all the things we will time with an associated label.
    THINGS_TO_TIME = (
        ('not_a_number', 'Non-number String'),
        ('-4', 'Small Int String'),
        ('-41053', 'Int String'),
        ('35892482945872302493947939485729', 'Large Int String'),
        ('-4.1', 'Small Float String'),
        ('-41053.543034e34', 'Float String'),
        ('-41053.543028758302e256', 'Large Float String'),
        (-41053, 'Int'),
        (-41053.543028758302e100, 'Float'),
    )

    # Formatting strings.
    FUNCTION_CALL_FMT = '{}({!r})'
    
    def __init__(self, title):
        display(Markdown('### ' + title))
        self.functions = []
    
    def add_function(self, func, label, setup='pass'):
        """Add a function to be timed and compared."""
        self.functions.append((func, setup, label))

    def time_functions(self, repeat=5):
        """Time all the given functions against all input then display results."""

        # Collect the function labels to make the header of this table.
        # Show that the units are seconds for each.
        function_labels = [label + ' (ms)' for _, _, label in self.functions]
        
        # Construct the table strings, formatted in Markdown.
        # Store each line as a string element in a list.
        # This portion here is the table header only for now.
        table = Table()
        table.add_header('Input type', *function_labels)
        
        # For each value, time each function and collect the results.
        for value, value_label in self.THINGS_TO_TIME:
            row = []
            for func, setup, _ in self.functions:
                call = self.FUNCTION_CALL_FMT.format(func, value)
                try:
                    row.append(self._timeit(call, setup, repeat))
                except (ValueError, TypeError):
                    # We might send in some invalid input accidentally.
                    # Ignore those inputs.
                    break

            # Only add this row if the for loop quit without break.
            else:
                # Convert to milliseconds
                row = [(mean * 1000, stddev * 1000) for mean, stddev in row]
                # Make the lowest value bold.
                min_indx = min(enumerate(row), key=lambda x: x[1])[0]
                row = ['{:.3f} ± {:.3f}'.format(*x) for x in row]
                row[min_indx] = self.bold(row[min_indx])
                table.add_row(value_label, *row)

        # Show the results in a table.
        display(Markdown(str(table)))

    @staticmethod
    def mean(x):
        return math.fsum(x) / len(x)

    @staticmethod
    def stddev(x):
        mean = Timer.mean(x)
        sum_of_squares = math.fsum((v - mean)**2 for v in x)
        return math.sqrt(sum_of_squares / (len(x) - 1))

    @staticmethod
    def bold(x):
        return "**{}**".format(x)
    
    def _timeit(self, call, setup, repeat=5):
        """Perform the actual timing and return a formatted string of the runtime"""
        result = timeit.repeat(call, setup, number=100000, repeat=repeat)
        return self.mean(result), self.stddev(result)

class Table(list):
    """List of strings that can be made into a Markdown table."""
    def add_row(self, *elements):
        self.append('|'.join(elements))
    def add_header(self, *elements):
        self.add_row(*elements)
        seperators = ['---'] * len(elements)
        seperators = [sep + (':' if i != 0 else '') for i, sep in enumerate(seperators)]
        self.add_row(*seperators)
    def __str__(self):
        return '\n'.join(self)


import sys
print(sys.version_info)


sys.version_info(major=3, minor=6, micro=9, releaselevel='final', serial=0)

Built-in Functions Drop-in Replacement Timing Results

The following timing tests compare the performance of Python's builtin int and float functions against the implementations from fastnumbers for various input types.


In [2]:
timer = Timer('Timing comparison of `int` functions')
timer.add_function('int', 'builtin')
timer.add_function('int', 'fastnumbers', 'from fastnumbers import int')
timer.time_functions(repeat=100)


Timing comparison of int functions

Input type builtin (ms) fastnumbers (ms)
Small Int String 20.979 ± 1.132 14.467 ± 1.031
Int String 21.856 ± 0.724 15.621 ± 0.471
Large Int String 29.114 ± 0.464 30.062 ± 1.599
Int 12.319 ± 0.149 10.412 ± 0.252
Float 27.806 ± 0.204 25.298 ± 0.371

In [3]:
timer = Timer('Timing comparison of `float` functions')
timer.add_function('float', 'builtin')
timer.add_function('float', 'fastnumbers', 'from fastnumbers import float')
timer.time_functions(repeat=100)


Timing comparison of float functions

Input type builtin (ms) fastnumbers (ms)
Small Int String 18.151 ± 0.731 13.858 ± 0.245
Int String 18.776 ± 0.389 14.338 ± 0.105
Large Int String 41.426 ± 0.517 42.038 ± 0.293
Small Float String 18.502 ± 0.126 14.161 ± 0.116
Float String 43.327 ± 0.219 15.619 ± 0.044
Large Float String 74.260 ± 1.893 73.192 ± 0.971
Int 12.323 ± 0.016 10.358 ± 0.035
Float 10.979 ± 0.026 9.304 ± 0.176

Error-Handling Conversion Functions Timing Results

The following timing tests compare the performance of the fastnumbers functions that convert input to numeric types while doing error handling with common equivalent pure-Python implementations.


In [4]:
def int_re(x, int_match=re.compile(r'[-+]?\d+$').match):
    """Function to simulate fast_int but with regular expressions."""
    try:
        if int_match(x):
            return int(x)
        else:
            return x
    except TypeError:
        return int(x)

def int_try(x):
    """Function to simulate fast_int but with try/except."""
    try:
        return int(x)
    except ValueError:
        return x

timer = Timer('Timing comparison of `int` functions with error handling')
timer.add_function('int_try', 'try/except', 'from __main__ import int_try')
timer.add_function('int_re', 'regex', 'from __main__ import int_re')
timer.add_function('fast_int', 'fastnumbers', 'from fastnumbers import fast_int')
timer.time_functions()


Timing comparison of int functions with error handling

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 144.683 ± 3.347 41.749 ± 0.246 21.335 ± 0.162
Small Int String 32.662 ± 0.886 71.386 ± 1.048 13.753 ± 0.038
Int String 33.090 ± 0.127 73.380 ± 1.152 15.589 ± 0.024
Large Int String 41.284 ± 0.056 106.675 ± 1.183 30.585 ± 0.018
Small Float String 137.226 ± 3.178 46.039 ± 0.076 13.533 ± 0.010
Float String 142.712 ± 1.011 53.792 ± 0.047 13.981 ± 0.049
Large Float String 150.679 ± 0.756 53.280 ± 0.008 14.280 ± 0.034
Int 24.021 ± 0.021 92.668 ± 0.470 11.265 ± 0.029
Float 39.637 ± 0.028 113.185 ± 2.360 28.474 ± 0.022

In [5]:
def float_re(x, float_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate fast_float but with regular expressions."""
    try:
        if float_match(x):
            return float(x)
        else:
            return x
    except TypeError:
        return float(x)

def float_try(x):
    """Function to simulate fast_float but with try/except."""
    try:
        return float(x)
    except ValueError:
        return x

timer = Timer('Timing comparison of `float` functions with error handling')
timer.add_function('float_try', 'try/except', 'from __main__ import float_try')
timer.add_function('float_re', 'regex', 'from __main__ import float_re')
timer.add_function('fast_float', 'fastnumbers', 'from fastnumbers import fast_float')
timer.time_functions()


Timing comparison of float functions with error handling

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 83.510 ± 3.017 46.939 ± 0.359 20.820 ± 0.047
Small Int String 28.160 ± 0.188 79.882 ± 2.368 15.813 ± 0.051
Int String 31.155 ± 0.567 84.160 ± 2.408 16.009 ± 0.455
Large Int String 52.098 ± 0.037 129.641 ± 0.340 43.951 ± 0.474
Small Float String 29.229 ± 0.202 79.912 ± 2.453 15.724 ± 0.303
Float String 54.785 ± 1.037 121.975 ± 0.538 17.163 ± 0.004
Large Float String 84.763 ± 0.353 161.045 ± 2.012 74.414 ± 0.104
Int 22.444 ± 0.013 90.434 ± 0.368 11.567 ± 0.005
Float 21.335 ± 0.070 89.935 ± 0.230 10.479 ± 0.031

In [6]:
def real_re(x,
            int_match=re.compile(r'[-+]?\d+$').match,
            real_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate fast_real but with regular expressions."""
    try:
        if int_match(x):
            return int(x)
        elif real_match(x):
            return float(x)
        else:
            return x
    except TypeError:
        if type(x) in (float, int):
            return x
        else:
            raise TypeError

def real_try(x):
    """Function to simulate fast_real but with try/except."""
    try:
        a = float(x)
    except ValueError:
        return x
    else:
        b = int(a)
        return b if a == b else b

timer = Timer('Timing comparison of `float` (but coerce to `int` if possible) functions with error handling')
timer.add_function('real_try', 'try/except', 'from __main__ import real_try')
timer.add_function('real_re', 'regex', 'from __main__ import real_re')
timer.add_function('fast_real', 'fastnumbers', 'from fastnumbers import fast_real')
timer.time_functions()


Timing comparison of float (but coerce to int if possible) functions with error handling

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 86.011 ± 10.141 78.260 ± 0.350 22.121 ± 0.315
Small Int String 49.187 ± 2.165 65.275 ± 0.477 14.633 ± 0.022
Int String 52.408 ± 0.105 72.847 ± 1.617 16.873 ± 0.008
Large Int String 90.163 ± 0.172 102.653 ± 0.029 33.967 ± 0.067
Small Float String 49.394 ± 0.112 113.780 ± 2.718 18.058 ± 0.006
Float String 94.656 ± 0.157 165.167 ± 5.397 32.193 ± 0.042
Large Float String 174.448 ± 5.179 202.332 ± 2.477 111.109 ± 0.992
Int 44.910 ± 0.195 93.687 ± 0.066 11.376 ± 0.012
Float 75.685 ± 0.049 94.530 ± 0.395 47.864 ± 0.117

In [7]:
def forceint_re(x,
                int_match=re.compile(r'[-+]\d+$').match,
                float_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate fast_forceint but with regular expressions."""
    try:
        if int_match(x):
            return int(x)
        elif float_match(x):
            return int(float(x))
        else:
            return x
    except TypeError:
        return int(x)

def forceint_try(x):
    """Function to simulate fast_forceint but with try/except."""
    try:
        return int(x)
    except ValueError:
        try:
            return int(float(x))
        except ValueError:
            return x

timer = Timer('Timing comparison of forced `int` functions with error handling')
timer.add_function('forceint_try', 'try/except', 'from __main__ import forceint_try')
timer.add_function('forceint_re', 'regex', 'from __main__ import forceint_re')
timer.add_function('fast_forceint', 'fastnumbers', 'from fastnumbers import fast_forceint')
timer.time_functions()


Timing comparison of forced int functions with error handling

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 220.782 ± 5.873 78.113 ± 2.879 23.518 ± 0.345
Small Int String 32.161 ± 0.363 63.187 ± 1.494 14.501 ± 0.036
Int String 31.157 ± 0.242 66.524 ± 0.087 17.052 ± 0.018
Large Int String 39.329 ± 0.151 179.512 ± 1.573 33.950 ± 0.209
Small Float String 167.763 ± 1.714 117.728 ± 0.715 20.554 ± 0.053
Float String 216.373 ± 10.684 207.621 ± 10.602 32.358 ± 0.077
Large Float String 267.190 ± 6.281 244.906 ± 4.435 110.536 ± 1.295
Int 23.086 ± 0.858 99.587 ± 0.961 11.349 ± 0.143
Float 38.894 ± 0.875 110.764 ± 5.551 25.543 ± 0.194

Checking Functions Timing Results

The following timing tests compare the performance of the fastnumbers functions that check if an input could be converted to numeric type with common equivalent pure-Python implementations.


In [8]:
def isint_re(x, int_match=re.compile(r'[-+]?\d+$').match):
    """Function to simulate isint but with regular expressions."""
    t = type(x)
    return t == int if t in (float, int) else bool(int_match(x))

def isint_try(x):
    """Function to simulate isint but with try/except."""
    try:
        int(x)
    except ValueError:
        return False
    else:
        return type(x) != float

timer = Timer('Timing comparison to check if value can be converted to `int`')
timer.add_function('isint_try', 'try/except', 'from __main__ import isint_try')
timer.add_function('isint_re', 'regex', 'from __main__ import isint_re')
timer.add_function('isint', 'fastnumbers', 'from fastnumbers import isint')
timer.time_functions()


Timing comparison to check if value can be converted to int

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 141.093 ± 5.387 70.776 ± 0.301 21.645 ± 0.532
Small Int String 44.548 ± 0.098 74.475 ± 1.835 12.804 ± 0.319
Int String 43.706 ± 0.625 77.166 ± 0.051 13.167 ± 0.004
Large Int String 53.041 ± 1.504 101.407 ± 0.402 16.835 ± 0.414
Small Float String 136.773 ± 2.733 75.112 ± 0.119 12.675 ± 0.016
Float String 142.669 ± 1.529 83.172 ± 0.504 13.444 ± 0.015
Large Float String 147.643 ± 2.316 82.862 ± 1.113 13.367 ± 0.021
Int 34.570 ± 0.054 32.585 ± 0.061 10.106 ± 0.037
Float 49.457 ± 0.148 30.953 ± 0.116 10.060 ± 0.011

In [9]:
def isfloat_re(x, float_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate isfloat but with regular expressions."""
    t = type(x)
    return t == float if t in (float, int) else bool(float_match(x))

def isfloat_try(x):
    """Function to simulate isfloat but with try/except."""
    try:
        float(x)
    except ValueError:
        return False
    else:
        return type(x) != int

timer = Timer('Timing comparison to check if value can be converted to `float`')
timer.add_function('isfloat_try', 'try/except', 'from __main__ import isfloat_try')
timer.add_function('isfloat_re', 'regex', 'from __main__ import isfloat_re')
timer.add_function('isfloat', 'fastnumbers', 'from fastnumbers import isfloat')
timer.time_functions()


Timing comparison to check if value can be converted to float

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 83.848 ± 7.001 74.631 ± 0.095 21.260 ± 0.371
Small Int String 41.149 ± 0.429 90.871 ± 2.201 14.011 ± 0.084
Int String 44.037 ± 0.390 89.396 ± 0.961 13.440 ± 0.007
Large Int String 65.682 ± 0.083 112.739 ± 0.381 14.927 ± 0.021
Small Float String 41.853 ± 0.026 88.599 ± 2.144 13.295 ± 0.148
Float String 67.390 ± 0.473 101.040 ± 0.035 13.950 ± 0.010
Large Float String 98.492 ± 0.300 111.738 ± 3.609 14.489 ± 0.513
Int 34.600 ± 0.027 37.252 ± 6.607 10.963 ± 0.373
Float 34.284 ± 0.699 29.926 ± 0.016 9.633 ± 0.018

In [10]:
def isreal_re(x, real_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate isreal but with regular expressions."""
    return type(x) in (float, int) or bool(real_match(x))

def isreal_try(x):
    """Function to simulate isreal but with try/except."""
    try:
        float(x)
    except ValueError:
        return False
    else:
        return True

timer = Timer('Timing comparison to check if value can be converted to `float` or `int`')
timer.add_function('isreal_try', 'try/except', 'from __main__ import isreal_try')
timer.add_function('isreal_re', 'regex', 'from __main__ import isreal_re')
timer.add_function('isreal', 'fastnumbers', 'from fastnumbers import isreal')
timer.time_functions()


Timing comparison to check if value can be converted to float or int

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 85.985 ± 8.067 82.365 ± 0.703 21.755 ± 0.398
Small Int String 27.934 ± 0.019 89.240 ± 2.058 13.647 ± 0.446
Int String 29.389 ± 0.630 91.554 ± 0.838 13.491 ± 0.124
Large Int String 53.059 ± 0.657 115.372 ± 0.990 15.133 ± 0.564
Small Float String 28.742 ± 0.081 89.933 ± 5.897 13.198 ± 0.012
Float String 53.800 ± 0.026 105.325 ± 0.781 14.158 ± 0.126
Large Float String 84.577 ± 0.568 114.008 ± 0.612 14.250 ± 0.005
Int 22.443 ± 0.019 27.993 ± 0.026 9.574 ± 0.006
Float 21.392 ± 0.463 26.376 ± 0.129 9.759 ± 0.137

In [11]:
def isintlike_re(x,
                 int_match=re.compile(r'[-+]?\d+$').match,
                 float_match=re.compile(r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?$').match):
    """Function to simulate isintlike but with regular expressions."""
    try:
        if int_match(x):
            return True
        elif float_match(x):
            return float(x).is_integer()
        else:
            return False
    except TypeError:
        return int(x) == x

def isintlike_try(x):
    """Function to simulate isintlike but with try/except."""
    try:
        a = int(x)
    except ValueError:
        try:
            a = float(x)
        except ValueError:
            return False
        else:
            return a.is_integer()
    else:
        return a == float(x)

timer = Timer('Timing comparison to check if value can be coerced losslessly to `int`')
timer.add_function('isintlike_try', 'try/except', 'from __main__ import isintlike_try')
timer.add_function('isintlike_re', 'regex', 'from __main__ import isintlike_re')
timer.add_function('isintlike', 'fastnumbers', 'from fastnumbers import isintlike')
timer.time_functions()


Timing comparison to check if value can be coerced losslessly to int

Input type try/except (ms) regex (ms) fastnumbers (ms)
Non-number String 222.218 ± 9.050 80.082 ± 1.280 20.220 ± 0.022
Small Int String 53.629 ± 0.268 44.853 ± 0.114 12.722 ± 0.045
Int String 57.669 ± 0.310 48.050 ± 0.130 12.674 ± 0.058
Large Int String 98.850 ± 0.392 70.958 ± 0.095 14.075 ± 0.101
Small Float String 165.709 ± 1.333 118.616 ± 1.261 12.805 ± 0.007
Float String 196.835 ± 1.935 171.553 ± 1.162 13.725 ± 0.017
Large Float String 236.102 ± 5.235 209.288 ± 1.628 14.554 ± 0.036
Int 40.924 ± 0.028 91.617 ± 0.498 9.495 ± 0.072
Float 75.837 ± 0.351 132.797 ± 0.305 30.942 ± 0.588

In [ ]: