programing

Python의 어레이가 느린 이유는 무엇입니까?

megabox 2023. 7. 3. 22:39
반응형

Python의 어레이가 느린 이유는 무엇입니까?

나는 예상했습니다.array.array목록보다 빠릅니다. 배열이 해제된 것처럼 보이기 때문입니다.

그러나 다음과 같은 결과가 나타납니다.

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

무엇이 그러한 차이의 원인이 될 수 있습니까?

스토리지는 "박스 해제"되어 있지만 요소에 액세스할 때마다 Python은 요소를 사용하여 모든 작업을 수행하기 위해 요소를 "박스"(일반 Python 개체에 포함)해야 합니다.예를 들어, 당신의sum(A)일반 Python에서 어레이를 반복하고 각 정수를 한 번에 하나씩 상자에 넣습니다.int그것은 시간이 걸립니다.의 신의에서.sum(L)모든 복싱은 목록이 만들어질 때 이루어졌습니다.

결국 어레이는 일반적으로 속도가 느리지만 메모리가 상당히 적게 필요합니다.


여기 최신 버전의 Python 3의 관련 코드가 있지만, Python이 처음 출시된 이후 모든 CPython 구현에 동일한 기본 아이디어가 적용됩니다.

목록 항목에 액세스하는 코드는 다음과 같습니다.

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

거의아무없습니다도것없다.somelist[i]▁the▁returns를 반환합니다.i가 a Python의 struct PyObject).

그리고 여기 있습니다.__getitem__을위실행에 대한 array 코드 이프코로드타▁type로l:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

네이티브 원시는플네랫이의벡처다리니의 됩니다.C long 수정; 더ith'''C long 위로 읽히고 읽집니다나서그고리혀나서▁is;▁then그고▁read▁and▁up.PyLong_FromLong()네이티브를 랩("상자")하기 위해 호출됩니다.C long 파썬로로longPython에서 Python 2의 에서 Python 3의 구분)int그리고.long실제로 유형으로 표시됩니다.int).

은 파이썬에 새로운 .int물체, 그리고 원주민을 분사합니다.C long의 조각들이 그 안에 있습니다.원래 예제의 맥락에서 이 개체의 수명은 매우 짧습니다.sum() 총계, 하는 데 더 많은 이 필요합니다.int물건.

이것이 바로 CPython 구현에서 발생하는 속도 차이이며, 항상 발생하며, 앞으로도 계속 발생할 것입니다.

Tim Peters의 훌륭한 답변에 덧붙여, 배열은 버퍼 프로토콜을 구현하는 반면, 목록은 구현하지 않습니다., C 확장자(또는 Cython 모듈 작성과 같은 도덕적 등가물)를 작성하는 경우 Python이 수행할 수 있는 것보다 훨씬 빠르게 배열의 요소에 액세스하고 작업할 수 있습니다.이를 통해 속도가 상당히 개선될 수 있으며, 아마도 크기를 훨씬 초과할 것입니다.그러나 다음과 같은 단점이 있습니다.

  1. 당신은 지금 파이썬 대신 C를 쓰는 사업을 하고 있습니다.Cython은 이를 개선하는 한 가지 방법이지만 언어 간의 많은 근본적인 차이를 제거하지는 못합니다. C 의미론에 익숙하고 C가 무엇을 하는지 이해해야 합니다.
  2. PyPy의 C API는 어느 정도 작동하지만, 그리 빠르지는 않습니다.만약 당신이 PyPy를 목표로 한다면, 당신은 아마도 일반 목록으로 간단한 코드를 작성하고, 그리고 나서 J.IT 부서에서 최적화합니다.
  3. C 확장자는 컴파일이 필요하기 때문에 순수한 Python 코드보다 배포하기가 어렵습니다.컴파일은 아키텍처 및 운영 체제에 따라 달라지는 경향이 있으므로 대상 플랫폼에 맞게 컴파일해야 합니다.

C 확장으로 바로 이동하는 것은 사용 사례에 따라 큰 망치를 사용하여 파리를 치는 것일 수 있습니다.먼저 NumPy를 조사하고 NumPy가 당신이 하려는 어떤 수학도 할 수 있을 정도로 강력한지 확인해야 합니다.또한 올바르게 사용하면 네이티브 파이썬보다 훨씬 빠를 것입니다.

Tim Peters는 왜 이것이 느리는지에 대해 답했습니다. 하지만 어떻게 그것을 개선할 수 있는지 봅시다.

당신의 예를 고수하는 것.sum(range(...))(여기서 메모리에 넣을 예제보다 작은 인자 10):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

이러한 방식으로 numpy는 또한 박스/박스 해제가 필요하며, 이는 추가적인 오버헤드를 가집니다.속도를 높이기 위해서는 numpy code 안에 있어야 합니다.

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

따라서 목록 솔루션에서 numpy 버전에 이르기까지 이것은 런타임의 16번째 요소입니다.

이러한 데이터 구조를 생성하는 데 걸리는 시간도 확인해 보겠습니다.

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

확실한 승자:움피

또한 데이터 구조를 생성하는 데에는 더 많은 시간이 소요되지는 않더라도 합계만큼의 시간이 소요됩니다.메모리 할당 속도가 느립니다.

메모리 사용량:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

따라서 오버헤드는 다양하고 숫자당 8바이트가 소요됩니다.우리가 사용하는 범위는 32bit int로 충분하므로 메모리를 보호할 수 있습니다.

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

그러나 64비트 int를 추가하는 것이 내 컴퓨터의 32비트 int보다 빠르다는 것이 밝혀졌으므로 메모리/대역폭에 의해 제한되는 경우에만 가치가 있습니다.

나는 그것을 알아챘다.typecode L보다 빠름l그리고 그것은 또한 작동합니다.I그리고.Q.

파이썬 3.8.5

여기 테스트 코드가 있습니다.d_d를 확인해 보세요.

#!/usr/bin/python3
import inspect
from tqdm import tqdm
from array import array


def get_var_name(var):
    """
    Gets the name of var. Does it from the out most frame inner-wards.
    :param var: variable to get name from.
    :return: string
    """
    for fi in reversed(inspect.stack()):
        names = [var_name for var_name, var_val in fi.frame.f_locals.items() if var_val is var]
        if len(names) > 0:
            return names[0]

def performtest(func, n, *args, **kwargs):

    times = array('f')
    times_append = times.append
    for i in tqdm(range(n)):
        st = time.time()
        func(*args, **kwargs)
        times_append(time.time() - st)
    print(
        f"Func {func.__name__} with {[get_var_name(i) for i in args]} run {n} rounds consuming |"
        f" Mean: {sum(times)/len(times)}s | Max: {max(times)}s | Min: {min(times)}s"
    )

def list_int(start, end, step=1):
    return [i for i in range(start, end, step)]

def list_float(start, end, step=1):
    return [i + 1e-1 for i in range(start, end, step)]

def array_int(start, end, step=1):
    return array("I", range(start, end, step)) # speed I > i, H > h, Q > q, I~=H~=Q

def array_float(start, end, step=1):
    return array("f", [i + 1e-1 for i in range(start, end, step)]) # speed f > d


if __name__ == "__main__":

    performtest(list_int, 1000, 0, 10000)
    performtest(array_int, 1000, 0, 10000)

    performtest(list_float, 1000, 0, 10000)
    performtest(array_float, 1000, 0, 10000)

결과.

시험결과

참고로100000000와 동등한.10^8하지 않는10^7제 결과는 다음과 같습니다.

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153

언급URL : https://stackoverflow.com/questions/36778568/why-are-pythons-arrays-slow

반응형