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

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=5, micro=6, 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 18.300 ± 1.921 12.096 ± 0.195
Int String 18.069 ± 0.116 13.868 ± 0.048
Large Int String 23.110 ± 0.219 24.665 ± 0.132
Int 9.842 ± 0.019 10.065 ± 0.170
Float 25.132 ± 0.173 24.559 ± 0.231

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 16.618 ± 1.050 13.026 ± 0.087
Int String 17.051 ± 0.258 13.592 ± 0.045
Large Int String 39.689 ± 0.352 41.238 ± 0.159
Small Float String 16.779 ± 0.059 13.607 ± 0.188
Float String 41.306 ± 0.212 15.002 ± 0.290
Large Float String 73.779 ± 3.112 72.747 ± 0.992
Int 15.106 ± 0.343 13.376 ± 0.221
Float 10.758 ± 0.261 9.653 ± 0.201

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 139.867 ± 8.382 40.248 ± 0.022 12.515 ± 0.333
Small Int String 27.062 ± 0.039 62.238 ± 2.573 12.661 ± 0.018
Int String 27.884 ± 0.025 67.332 ± 1.208 14.750 ± 0.042
Large Int String 35.074 ± 0.331 98.660 ± 2.440 25.400 ± 0.020
Small Float String 128.910 ± 0.815 45.091 ± 0.611 12.596 ± 0.018
Float String 138.439 ± 0.338 52.313 ± 0.165 12.812 ± 0.016
Large Float String 145.466 ± 6.762 52.518 ± 0.341 13.871 ± 0.197
Int 21.058 ± 0.082 91.689 ± 2.353 10.801 ± 0.024
Float 34.740 ± 0.060 105.248 ± 7.611 25.144 ± 0.020

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 100.024 ± 5.108 46.575 ± 0.980 12.832 ± 0.074
Small Int String 28.879 ± 0.087 74.808 ± 2.293 14.084 ± 0.488
Int String 27.937 ± 0.023 78.356 ± 0.965 14.370 ± 0.006
Large Int String 51.189 ± 0.017 129.265 ± 0.550 42.177 ± 0.321
Small Float String 27.775 ± 0.010 72.907 ± 0.440 14.271 ± 0.005
Float String 52.732 ± 0.033 117.696 ± 0.837 15.779 ± 0.031
Large Float String 90.478 ± 6.332 156.654 ± 0.535 73.442 ± 0.052
Int 26.118 ± 0.407 94.987 ± 0.150 14.159 ± 0.023
Float 21.566 ± 0.025 90.204 ± 0.639 10.219 ± 0.012

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 100.073 ± 7.343 79.565 ± 0.479 13.236 ± 0.082
Small Int String 50.484 ± 1.098 64.384 ± 1.161 13.336 ± 0.013
Int String 52.166 ± 0.439 66.460 ± 0.126 15.304 ± 0.010
Large Int String 86.692 ± 0.028 98.642 ± 0.551 29.187 ± 0.059
Small Float String 50.014 ± 0.995 107.214 ± 0.436 16.712 ± 0.014
Float String 91.603 ± 0.045 159.401 ± 0.235 29.163 ± 0.073
Large Float String 167.878 ± 0.459 198.068 ± 1.309 109.333 ± 0.440
Int 49.084 ± 0.045 94.791 ± 0.069 10.190 ± 0.005
Float 73.756 ± 0.236 95.102 ± 0.482 47.500 ± 0.360

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 244.823 ± 15.385 79.286 ± 0.153 12.971 ± 0.324
Small Int String 26.944 ± 0.019 59.953 ± 0.452 13.281 ± 0.006
Int String 27.817 ± 0.021 68.868 ± 7.404 18.182 ± 1.116
Large Int String 33.263 ± 0.368 178.418 ± 4.251 29.240 ± 0.519
Small Float String 162.987 ± 8.011 120.105 ± 1.716 18.973 ± 0.046
Float String 202.641 ± 2.391 179.179 ± 2.396 29.746 ± 0.087
Large Float String 256.698 ± 1.134 240.627 ± 3.280 108.667 ± 0.025
Int 19.622 ± 0.030 87.825 ± 0.694 10.099 ± 0.056
Float 34.976 ± 0.034 104.639 ± 2.756 24.786 ± 0.359

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 136.770 ± 7.570 68.438 ± 0.765 12.166 ± 0.092
Small Int String 41.092 ± 1.086 75.545 ± 1.053 11.699 ± 0.029
Int String 39.501 ± 0.042 75.559 ± 0.291 12.129 ± 0.027
Large Int String 45.077 ± 0.029 100.621 ± 0.502 14.624 ± 0.024
Small Float String 129.223 ± 0.837 72.828 ± 0.229 11.847 ± 0.011
Float String 135.933 ± 0.153 81.394 ± 0.260 12.383 ± 0.049
Large Float String 141.929 ± 1.038 80.747 ± 0.203 12.288 ± 0.007
Int 31.064 ± 0.007 29.168 ± 0.013 9.079 ± 0.011
Float 46.022 ± 0.152 28.008 ± 0.036 9.255 ± 0.029

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 99.873 ± 5.316 70.125 ± 0.223 12.096 ± 0.039
Small Int String 37.519 ± 0.058 79.637 ± 0.484 11.807 ± 0.021
Int String 38.727 ± 0.270 84.876 ± 0.250 12.310 ± 0.125
Large Int String 63.529 ± 0.434 108.808 ± 1.519 13.577 ± 0.015
Small Float String 38.133 ± 0.024 78.530 ± 0.654 11.962 ± 0.069
Float String 67.089 ± 1.180 100.133 ± 2.517 12.614 ± 0.024
Large Float String 95.234 ± 0.213 105.110 ± 0.363 12.966 ± 0.020
Int 35.489 ± 0.026 29.958 ± 0.081 9.339 ± 0.023
Float 32.315 ± 0.649 27.403 ± 0.015 8.571 ± 0.033

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 99.393 ± 6.224 69.763 ± 1.438 12.103 ± 0.009
Small Int String 27.034 ± 0.203 82.158 ± 1.140 12.602 ± 0.072
Int String 29.275 ± 0.133 85.371 ± 2.650 12.305 ± 0.211
Large Int String 50.929 ± 0.057 109.240 ± 2.687 13.460 ± 0.004
Small Float String 27.619 ± 0.120 80.067 ± 1.444 12.529 ± 0.062
Float String 54.957 ± 0.973 97.142 ± 0.777 12.757 ± 0.023
Large Float String 83.728 ± 0.057 105.238 ± 1.151 13.356 ± 0.124
Int 26.100 ± 0.174 27.249 ± 0.102 9.409 ± 0.186
Float 21.477 ± 0.573 23.395 ± 0.040 8.731 ± 0.055

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 237.356 ± 8.392 77.243 ± 0.120 12.067 ± 0.106
Small Int String 55.912 ± 1.349 44.405 ± 0.884 11.210 ± 0.050
Int String 56.230 ± 0.123 45.226 ± 0.036 11.406 ± 0.024
Large Int String 92.532 ± 0.899 70.153 ± 0.084 12.840 ± 0.067
Small Float String 156.041 ± 1.430 109.634 ± 0.327 11.637 ± 0.025
Float String 191.856 ± 1.885 160.514 ± 0.268 12.368 ± 0.024
Large Float String 227.364 ± 2.524 198.978 ± 0.735 13.073 ± 0.508
Int 45.582 ± 0.356 92.058 ± 1.575 8.785 ± 0.195
Float 72.378 ± 0.400 127.355 ± 0.175 31.312 ± 0.014

In [ ]: