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.7.

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 Timer 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=7, micro=5, 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 17.932 ± 2.940 13.548 ± 0.517
Int String 18.530 ± 1.043 15.756 ± 0.605
Large Int String 24.696 ± 1.132 27.728 ± 0.454
Int 9.219 ± 0.139 10.433 ± 0.152
Float 24.451 ± 0.245 25.454 ± 0.261

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 15.486 ± 0.473 14.193 ± 0.155
Int String 15.924 ± 0.186 14.782 ± 0.106
Large Int String 39.803 ± 1.467 43.776 ± 1.139
Small Float String 16.145 ± 0.516 14.774 ± 0.249
Float String 41.140 ± 0.714 16.260 ± 0.199
Large Float String 72.156 ± 1.293 75.244 ± 1.796
Int 8.389 ± 0.173 10.670 ± 0.074
Float 7.702 ± 0.166 9.634 ± 0.064

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 146.400 ± 7.402 39.931 ± 0.154 21.698 ± 0.316
Small Int String 26.531 ± 0.682 61.320 ± 0.546 13.975 ± 0.012
Int String 27.011 ± 0.105 66.103 ± 0.073 16.099 ± 0.083
Large Int String 34.512 ± 0.292 103.080 ± 0.694 29.078 ± 0.285
Small Float String 130.215 ± 1.605 43.022 ± 0.040 13.717 ± 0.007
Float String 141.935 ± 3.640 51.606 ± 0.093 14.497 ± 0.049
Large Float String 143.260 ± 0.532 50.034 ± 0.045 14.284 ± 0.019
Int 18.591 ± 0.018 86.049 ± 1.895 11.285 ± 0.129
Float 34.326 ± 0.040 101.555 ± 1.727 25.890 ± 0.135

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 80.172 ± 5.990 40.084 ± 0.025 21.130 ± 0.003
Small Int String 24.566 ± 0.006 72.200 ± 3.940 16.177 ± 0.085
Int String 26.997 ± 0.235 74.031 ± 3.074 15.708 ± 0.005
Large Int String 48.710 ± 0.463 118.449 ± 0.096 43.990 ± 0.214
Small Float String 25.602 ± 0.304 69.010 ± 0.282 15.844 ± 0.029
Float String 50.641 ± 0.807 114.521 ± 0.306 17.226 ± 0.024
Large Float String 80.664 ± 0.026 156.879 ± 3.616 75.021 ± 0.164
Int 16.832 ± 0.005 82.519 ± 0.033 11.189 ± 0.015
Float 15.815 ± 0.010 82.213 ± 1.139 10.316 ± 0.115

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 82.803 ± 2.037 75.379 ± 2.195 22.294 ± 0.140
Small Int String 42.518 ± 0.189 60.290 ± 0.381 14.623 ± 0.008
Int String 45.155 ± 0.123 67.623 ± 1.621 17.427 ± 0.030
Large Int String 85.460 ± 1.146 97.309 ± 0.408 30.452 ± 0.023
Small Float String 42.940 ± 0.013 106.552 ± 1.611 18.466 ± 0.109
Float String 91.277 ± 0.144 161.596 ± 1.752 30.662 ± 0.039
Large Float String 165.773 ± 3.157 199.720 ± 4.092 110.978 ± 0.276
Int 37.884 ± 0.482 94.962 ± 1.168 10.560 ± 0.024
Float 66.981 ± 0.280 87.820 ± 0.338 45.576 ± 0.023

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 225.559 ± 8.299 71.459 ± 0.240 22.172 ± 0.009
Small Int String 25.769 ± 0.111 58.116 ± 0.593 14.587 ± 0.004
Int String 26.941 ± 0.204 65.053 ± 0.844 17.394 ± 0.097
Large Int String 34.931 ± 0.012 170.357 ± 2.402 30.658 ± 0.036
Small Float String 164.257 ± 4.999 114.764 ± 2.557 20.625 ± 0.148
Float String 207.379 ± 2.101 182.404 ± 5.072 30.808 ± 0.022
Large Float String 266.086 ± 7.579 237.553 ± 3.553 112.624 ± 1.795
Int 18.725 ± 0.074 87.270 ± 1.511 10.042 ± 0.013
Float 33.056 ± 0.202 98.963 ± 0.103 25.322 ± 0.008

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 139.979 ± 1.782 63.361 ± 0.278 21.334 ± 0.181
Small Int String 37.410 ± 1.392 64.609 ± 0.761 13.047 ± 0.005
Int String 37.839 ± 0.348 68.377 ± 0.413 13.287 ± 0.004
Large Int String 46.521 ± 0.527 95.243 ± 0.757 14.921 ± 0.297
Small Float String 129.535 ± 1.040 66.287 ± 0.335 13.159 ± 0.031
Float String 141.850 ± 3.183 75.430 ± 1.136 13.407 ± 0.002
Large Float String 142.889 ± 0.751 74.267 ± 0.679 13.497 ± 0.096
Int 29.627 ± 0.118 28.415 ± 0.120 9.861 ± 0.144
Float 44.634 ± 0.444 26.469 ± 0.410 9.737 ± 0.025

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 79.375 ± 3.525 68.012 ± 0.583 21.351 ± 0.014
Small Int String 35.220 ± 0.029 79.661 ± 1.600 13.486 ± 0.103
Int String 37.227 ± 0.316 82.679 ± 2.462 13.614 ± 0.505
Large Int String 60.360 ± 0.199 104.238 ± 0.372 14.856 ± 0.043
Small Float String 36.180 ± 0.723 79.475 ± 0.744 13.782 ± 0.553
Float String 63.161 ± 1.109 97.006 ± 0.725 14.035 ± 0.004
Large Float String 92.725 ± 0.280 105.473 ± 2.179 14.734 ± 0.261
Int 27.912 ± 0.160 28.377 ± 0.365 9.793 ± 0.073
Float 26.382 ± 0.018 25.945 ± 0.534 9.370 ± 0.051

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 79.205 ± 4.791 65.407 ± 0.066 21.527 ± 0.007
Small Int String 25.638 ± 0.507 87.775 ± 10.495 14.242 ± 0.066
Int String 26.071 ± 0.413 78.936 ± 0.614 13.376 ± 0.018
Large Int String 48.917 ± 0.022 101.990 ± 0.193 14.852 ± 0.174
Small Float String 26.126 ± 0.436 78.288 ± 1.177 14.137 ± 0.123
Float String 51.264 ± 1.266 94.813 ± 0.376 14.017 ± 0.006
Large Float String 80.919 ± 0.046 102.607 ± 2.370 15.127 ± 0.160
Int 18.494 ± 0.302 25.281 ± 0.374 9.721 ± 0.168
Float 15.959 ± 0.033 21.943 ± 0.018 9.362 ± 0.007

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 210.808 ± 6.231 76.480 ± 1.204 21.170 ± 0.109
Small Int String 48.875 ± 0.572 41.911 ± 0.066 12.427 ± 0.014
Int String 50.218 ± 0.468 45.611 ± 0.064 12.702 ± 0.014
Large Int String 90.622 ± 0.070 70.319 ± 0.697 14.228 ± 0.094
Small Float String 173.815 ± 9.556 121.739 ± 7.846 13.385 ± 0.489
Float String 217.171 ± 4.553 180.605 ± 10.143 14.302 ± 1.108
Large Float String 253.053 ± 18.106 211.222 ± 9.037 15.430 ± 0.105
Int 35.585 ± 0.536 87.717 ± 1.716 9.211 ± 0.033
Float 66.754 ± 0.259 124.244 ± 0.894 30.690 ± 0.133

In [ ]: