[Python] Why do these nearly identical functions perform very differently?

Discussão em 'Python' iniciado por Stack, Abril 12, 2025.

  1. Stack

    Stack Membro Participativo

    I have written four functions that modify a square 2D array in place, it reflects half of the square array delimited by two sides that meet and the corresponding 45 degree diagonal, to the other half separated by the same diagonal.

    I have written a function for each of the four possible cases, to reflect product(('upper', 'lower'), ('left', 'right')) to product(('lower', 'upper'), ('right', 'left')).

    They use Numba to compile Just-In-Time and they are parallelized using numba.prange and are therefore much faster than the methods provided by NumPy:

    In [2]: sqr = np.random.randint(0, 256, (1000, 1000), dtype=np.uint8)

    In [3]: %timeit x, y = np.tril_indices(1000); sqr[x, y] = sqr[y, x]
    9.16 ms ± 30.9 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)


    As you can see, the above code takes a very long time to execute.

    import numpy as np
    import numba as nb


    @nb.njit(cache=True, parallel=True, nogil=True)
    def triangle_flip_LL2UR(arr: np.ndarray) -> None:
    height, width = arr.shape[:2]
    if height != width:
    raise ValueError("argument arr must be a square")

    for i in nb.prange(height):
    arr[i, i:] = arr[i:, i]


    @nb.njit(cache=True, parallel=True, nogil=True)
    def triangle_flip_UR2LL(arr: np.ndarray) -> None:
    height, width = arr.shape[:2]
    if height != width:
    raise ValueError("argument arr must be a square")

    for i in nb.prange(height):
    arr[i:, i] = arr[i, i:]


    @nb.njit(cache=True, parallel=True, nogil=True)
    def triangle_flip_LR2UL(arr: np.ndarray) -> None:
    height, width = arr.shape[:2]
    if height != width:
    raise ValueError("argument arr must be a square")

    last = height - 1
    for i in nb.prange(height):
    arr[i, last - i :: -1] = arr[i:, last - i]


    @nb.njit(cache=True, parallel=True, nogil=True)
    def triangle_flip_UL2LR(arr: np.ndarray) -> None:
    height, width = arr.shape[:2]
    if height != width:
    raise ValueError("argument arr must be a square")

    last = height - 1
    for i in nb.prange(height):
    arr[i:, last - i] = arr[i, last - i :: -1]


    In [4]: triangle_flip_LL2UR(sqr)

    In [5]: triangle_flip_UR2LL(sqr)

    In [6]: triangle_flip_LR2UL(sqr)

    In [7]: triangle_flip_UL2LR(sqr)

    In [8]: %timeit triangle_flip_LL2UR(sqr)
    194 μs ± 634 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

    In [9]: %timeit triangle_flip_UR2LL(sqr)
    488 μs ± 3.26 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)

    In [10]: %timeit triangle_flip_LR2UL(sqr)
    196 μs ± 501 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

    In [11]: %timeit triangle_flip_UL2LR(sqr)
    486 μs ± 855 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)


    Why do they have execution times with a significant difference? Two of them take around 200 microseconds to execute, the other two around 500 microseconds, despite the fact that they are almost identical.

    Continue reading...

Compartilhe esta Página