Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an equivalent of `sum()` builtin which uses augmented assignment?

Is there any standard library/numpy equivalent of the following function:

def augmented_assignment_sum(iterable, start=0):
    for n in iterable:
        start += n
    return start

?

While sum(ITERABLE) is very elegant, it uses + operator instead of +=, which in case of np.ndarray objects may affect performance.

I have tested that my function may be as fast as sum() (while its equivalent using + is much slower). As it is a pure Python function, I guess its performance is still handicapped, thus I am looking for some alternative:

In [49]: ARRAYS = [np.random.random((1000000)) for _ in range(100)]

In [50]: def not_augmented_assignment_sum(iterable, start=0): 
    ...:     for n in iterable: 
    ...:         start = start + n 
    ...:     return start 
    ...:                                                                                                                                                                                                                                                                       

In [51]: %timeit not_augmented_assignment_sum(ARRAYS)                                                                                                                                                                                                                          
63.6 ms ± 8.88 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [52]: %timeit sum(ARRAYS)                                                                                                                                                                                                                                                   
31.2 ms ± 2.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [53]: %timeit augmented_assignment_sum(ARRAYS)                                                                                                                                                                                                                              
31.2 ms ± 4.73 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [54]: %timeit not_augmented_assignment_sum(ARRAYS)                                                                                                                                                                                                                          
62.5 ms ± 12.1 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [55]: %timeit sum(ARRAYS)                                                                                                                                                                                                                                                   
37 ms ± 9.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [56]: %timeit augmented_assignment_sum(ARRAYS)                                                                                                                                                                                                                              
27.7 ms ± 2.53 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

I have tried to use functools.reduce combined with operator.iadd, but its performace is similar:

In [79]: %timeit reduce(iadd, ARRAYS, 0)                                                                                                                                                                                                                                       
33.4 ms ± 11.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [80]: %timeit reduce(iadd, ARRAYS, 0)                                                                                                                                                                                                                                       
29.4 ms ± 2.31 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

I am also interested in memory efficiency, thus prefer augmented assignments as they not require creation of intermediate objects.

like image 680
abukaj Avatar asked Nov 14 '19 17:11

abukaj


People also ask

What addition method is used for augmented assignment?

Augmented Assignment Is a Statement The most common form of an augmented assignment that many are familiar with is += .

What is an augmented assignment statement?

An augmented assignment is generally used to replace a statement where an operator takes a variable as one of its arguments and then assigns the result back to the same variable. A simple example is x += 1 which is expanded to x = x + (1) . Similar constructions are often available for various binary operators.

What are augmented assignment operators in Python?

Augmented assignment operators have a special role to play in Python programming. It basically combines the functioning of the arithmetic or bitwise operator with the assignment operator.


1 Answers

The answer to the headline question --- I hope @Martijn Pieters will forgive my choice of metaphor --- straight from the horse's mouth is: No, there is no such builtin.

If we allow for a few lines of code to implement such an equivalent we get a rather complicated picture with what is fastest very much depending on operand size:

enter image description here

This graph shows timings of different methods relative to sum over operand size, number of terms is always 100. augmented_assignment_sum starts paying off towards relatively large operand sizes. Using scipy.linalg.blas.*axpy looks pretty competitive over most of the range tested, its main drawback being being way less easy to use than sum.

Code:

from simple_benchmark import BenchmarkBuilder, MultiArgument
import numpy as np
from scipy.linalg import blas

B = BenchmarkBuilder()

@B.add_function()
def augmented_assignment_sum(iterable, start=0):
    for n in iterable:
        start += n
    return start

@B.add_function()
def not_augmented_assignment_sum(iterable, start=0):
    for n in iterable:
        start = start + n
    return start

@B.add_function()
def plain_sum(iterable, start=0):
    return sum(iterable,start)

@B.add_function()
def blas_sum(iterable, start=None):
    iterable = iter(iterable)
    if start is None:
        try:
            start = next(iterable).copy()
        except StopIteration:
            return 0
    try:
        f = {np.dtype('float32'):blas.saxpy,
             np.dtype('float64'):blas.daxpy,
             np.dtype('complex64'):blas.caxpy,
             np.dtype('complex128'):blas.zaxpy}[start.dtype]
    except KeyError:
        f = blas.daxpy
        start = start.astype(float)
    for n in iterable:
        f(n,start)
    return start

@B.add_arguments('size of terms')
def argument_provider():
    for exp in range(1,21):
        sz = int(2**exp)
        yield sz,[np.random.randn(sz) for _ in range(100)]

r = B.run()
r.plot(relative_to=plain_sum)

import pylab
pylab.savefig('inplacesum.png')
like image 57
Paul Panzer Avatar answered Nov 14 '22 23:11

Paul Panzer