Numpy array copy slower than Python list copy

Question:

I’ve seen several posts here about accessing individual items in numpy arrays and python lists via a for loop.

My program is a little different. What I’m doing is copying a small array (or list) of about 10 elements and then using it. I’m doing this many times, so I want it to be fast. The application if you’re interested is that I’m searching a tree, and each small array/list is a ‘state’ in the tree.

But I’m finding that the numpy.copy() function is slower than the Python list() function.

To demonstrate what I’m saying, here’s a small program with timings:

import time

import numpy as np
def numPyArrays(iterations:int):

    initialArray = np.array([1,0,0,1,0,1,1,1,0,0])

    for i in range(iterations):
        nextArray = initialArray.copy()
    
    print(f"Numpy Arrays:n{nextArray}")
    return    

def pythonLists(iterations:int):

    initialList = [1,0,0,1,0,1,1,1,0,0]

    for i in range(iterations):
        nextList = list(initialList)
    
    print(f"Python Lists:n{nextList}")
    return

def main():

    numIterations = 10000000

    startTime = time.time()
    numPyArrays(numIterations)
    print(f"Time taken: {round(time.time() - startTime, 2)} seconds.n")

    startTime = time.time()
    pythonLists(numIterations)
    print(f"Time taken: {round(time.time() - startTime, 2)} seconds.n")

main()

Timings:

Numpy Arrays:
[1 0 0 1 0 1 1 1 0 0]
Time taken: 4.68 seconds.

Python Lists:
[1, 0, 0, 1, 0, 1, 1, 1, 0, 0]
Time taken: 1.5 seconds.

I would have thought the numpy.copy function would have been as fast as a list copy.

EDIT: For those wanting to know what the underlying problem is, it’s an Advent of Code problem. Day 19, 2022. https://adventofcode.com/2022/day/19

Asked By: davo36

||

Answers:

The operation your doing seems pointless, you just assign the same array or list to a new variable over and over and over. What happens if you actually make that many copies of the data in a useful manner?

How about this:

import time

import numpy as np


def numPyArrays(iterations: int):
    initialArray = np.array([1, 0, 0, 1, 0, 1, 1, 1, 0, 0])

    result = np.zeros((iterations, len(initialArray)), dtype=int)
    result[:, :] = initialArray

    print(result[0], result[1], result[-1])
    return


def pythonLists(iterations: int):
    initialList = [1, 0, 0, 1, 0, 1, 1, 1, 0, 0]

    result = [list(initialList) for i in range(iterations)]

    print(result[0], result[1], result[-1])
    return


def main():
    numIterations = 10000000

    startTime = time.time()
    numPyArrays(numIterations)
    print(f"Time taken: {round(time.time() - startTime, 2)} seconds.n")

    startTime = time.time()
    pythonLists(numIterations)
    print(f"Time taken: {round(time.time() - startTime, 2)} seconds.n")


main()

Result:

[1 0 0 1 0 1 1 1 0 0] [1 0 0 1 0 1 1 1 0 0] [1 0 0 1 0 1 1 1 0 0]
Time taken: 0.13 seconds.

[1, 0, 0, 1, 0, 1, 1, 1, 0, 0] [1, 0, 0, 1, 0, 1, 1, 1, 0, 0] [1, 0, 0, 1, 0, 1, 1, 1, 0, 0]
Time taken: 4.26 seconds.

All your example shows is that initialising an entirely new numpy array has some overhead compared to defining a new list. But in any realistic scenario, you’d only do that a few times anyway, while the copy operation itself is way faster in numpy, as this example shows.

Instead of copying the same array over and over to a completely new array, my code copies the array to a new row of an array that many times. And then it does the same with lists. Here, numpy is more than an order of magnitude faster.

(Note that my example just prints the first 2 and then the last copy of each result, but of course that’s arbitary – you can write some code to check that all rows do in fact have the same values assigned)

Answered By: Grismar

Using ipython timeit

In [82]: %%timeit x= [1,0,0,1,0,1,1,1,0,0]
    ...: list(x)
185 ns ± 1.97 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

another way of making a copy of a list:

In [83]: %%timeit x= [1,0,0,1,0,1,1,1,0,0]
    ...: z=x[:]
146 ns ± 1.98 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

A list consists of a databuffer that holds references to the list objects (plus a small growth space). These copies make a new list, with simple copy of that databuffer. Same references.

The array copy, as you note is slower, but I consider any timeit measured in ns to be trivially fast.

In [84]: %%timeit x=np.array([1,0,0,1,0,1,1,1,0,0])
    ...: x.copy()
756 ns ± 2.74 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

But let’s look at a larger array/list:

In [88]: %%timeit x=np.arange(10000)
    ...: x.copy()
5.37 µs ± 8.77 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

In [89]: %%timeit x= list(range(10000))
    ...: x[:]
40.4 µs ± 100 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

The array copy scales better. That means that a large part of the difference you saw has to do higher "startup" cost in the array copy. The copy per element is faster.

In general, operations on small lists often are faster than similar ones with arrays. It’s when the length gets large(r) that the arrays are advantageous. But don’t just go by speed; what you are doing with the list or array is just as important.

edit

Let’s look at what tweaking a few values does to the times

In [97]: %%timeit x= list(range(10))
    ...: y=x[:]; y[3] = 4; y[1]=0
243 ns ± 2.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)

In [98]: %%timeit x=np.arange(10)
    ...: y=x.copy(); y[[3,1]] =[4,0]
9.89 µs ± 15.7 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

Even though the list tweaking has to be done one by one, it’s still faster. In fact accessing individual items adds substantially to the time.

This addition doesn’t change the times for the large example. There the copying, for both, dominates the times.

Answered By: hpaulj
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.