[]가 list ()보다 빠른 이유는 무엇입니까?


706

최근의 처리 속도 비교 []list()그 발견 놀랐다 []실행 빠르게 세 번 것보다 더 이상 list(). 나는과 동일한 테스트를 실행 {}하고 dict(): 결과는 실질적으로 동일했다 []{}동안 모두 약 0.128sec / 만회했다 list()dict()약 0.428sec / 만회 각했다.

왜 이런거야? 수행 []하고 {}(아마 ()''도) 자신의 명시 적 이름의 대응이 (동안 즉시 일부 빈 재고 리터럴의 복사본을 다시 전달 list(), dict(), tuple(), str())가 완전히 실제로 요소가 있는지 여부, 개체를 만드는 방법에 대해 이동?

이 두 가지 방법이 어떻게 다른지 잘 모르겠지만 알고 싶습니다. 문서 나 SO에서 답을 찾을 수 없었으며 빈 괄호를 검색하는 것이 예상보다 문제가 많은 것으로 나타났습니다.

나는 호출하여 내 타이밍 결과를 가지고 timeit.timeit("[]")timeit.timeit("list()"), 및 timeit.timeit("{}")timeit.timeit("dict()")각각 목록 및 사전을 비교. Python 2.7.9를 실행 중입니다.

내가 최근에 발견 된 " 왜 인 경우는 true보다 느린 경우 하나? "의 성능을 비교 if True하는 if 1과에 터치 보인다 비슷한 문자 - 대 - 글로벌 시나리오; 아마도 고려해 볼 가치가 있습니다.


2
주 : ()그리고 ''그들은 단지 비어 있지있는 한, 특수, 그들은, 그리고 같은, 그것은 그들을 싱글을 할 수있는 쉬운 승리는 불변입니다있는 거; 그들은 심지어 새로운 객체를 만들지 않고 empty tuple/에 대한 싱글 톤을로드합니다 str. 기술적으로 구현 세부 사항,하지만 나는 그들이 왜 힘든 시간의 상상이 않을 것이다 빈 캐시 tuple/를 str성능상의 이유로합니다. 대한 직관 그래서 []{}주식 문자를 다시 통과 잘못하지만 적용됩니까 ()''.
ShadowRanger

답변:


757

때문에 []하고 {}있습니다 리터럴 구문 . 파이썬은리스트 나 딕셔너리 객체를 만들기 위해 바이트 코드를 만들 수 있습니다 :

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()그리고 dict()별도의 개체입니다. 이름을 확인하고 인수를 푸시하기 위해 스택을 포함해야하며 나중에 검색하기 위해 프레임을 저장해야하며 호출해야합니다. 모든 시간이 더 걸립니다.

빈 경우에는 최소한 현재 프레임을 유지 해야하는 LOAD_NAME(글로벌 네임 스페이스와 __builtin__모듈 을 검색해야 함 ) 뒤에 a CALL_FUNCTION가 있어야합니다.

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

다음을 사용하여 이름 조회 시간을 별도로 지정할 수 있습니다 timeit.

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

시간 불일치에는 사전 해시 충돌이있을 수 있습니다. 해당 객체를 호출하는 시간에서 해당 시간을 빼고 리터럴을 사용하는 시간과 결과를 비교하십시오.

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

따라서 개체를 호출하는 데는 1.00 - 0.31 - 0.30 == 0.39천만 호출마다 추가 초가 걸립니다 .

전역 이름의 이름을 로컬로 지정하여 전역 조회 비용을 피할 수 있습니다 ( timeit설정을 사용 하면 이름에 바인딩하는 모든 것이 로컬 임).

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

그러나 그 CALL_FUNCTION비용 을 결코 극복 할 수는 없습니다 .


150

list()전역 조회 및 함수 호출이 필요하지만 []단일 명령어로 컴파일됩니다. 보다:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None

75

문자열을 목록 객체로 변환 list하는 함수 이기 때문에 []방망이에서 목록을 만드는 데 사용됩니다. 이것을 시도하십시오 (더 의미가있을 수 있습니다).

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

동안

y = ["wham bam"]
>>> y
["wham bam"]

당신이 그것에 넣은 것을 포함하는 실제 목록을 제공합니다.


7
이것은 질문을 직접 다루지 않습니다. 문제는 이유에 대해이었다 []빠르게보다 list()하지, 왜 ['wham bam']더 빨리보다 list('wham bam').
Jeremy Visser 2016

2
@JeremyVisser []/ list()는 정확히 ['wham']/ 와 동일하므로 수학 에서와 list('wham')동일한 변수 차이를 갖기 때문에 나에게는 거의 의미가 없습니다 . 이론적으로는 제거 할 수 있으며 사실은 여전히 ​​동일합니다. 즉 함수 이름을 호출하여 무언가를 변환하려고 시도하면서 변수를 바로 변환합니다. 함수 호출은 다릅니다. 예를 들어 회사의 네트워크 맵도 솔루션 / 문제의 논리이기 때문에 문제의 논리적 개요 일뿐입니다. 그러나 당신이 원하는 투표하십시오. 1000/10100/1wham bamlist()[]
Torxed

@JeremyVisser는 반대로 콘텐츠에 대해 다른 작업을 수행함을 보여줍니다.
Baldrickk

20

이 질문에 대한 대답은 훌륭하고이 질문을 완전히 다루고 있습니다. 관심있는 사람들을 위해 바이트 코드에서 한 단계 더 나아가겠습니다. CPython의 최신 저장소를 사용하고 있습니다. 이전 버전은 이와 관련하여 비슷하게 작동하지만 약간의 변경이있을 수 있습니다.

다음은 BUILD_LISTfor []CALL_FUNCTIONfor 각각에 대한 실행에 대한 분석 list()입니다.


BUILD_LIST명령 :

당신은 공포를 볼 수 있습니다 :

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

몹시 혼란 스러웠습니다. 이것이 얼마나 간단합니다 :

  • 다음을 사용하여 새 목록을 만듭니다 PyList_New(주로 새 목록 객체에 대한 메모리 할당).oparg스택의 인수 수를 알려주 . 바로 포인트.
  • 에 문제가 없는지 확인하십시오 if (list==NULL).
  • PyList_SET_ITEM(매크로) 를 사용하여 스택에있는 인수 (이 경우에는 실행되지 않음 )를 추가하십시오.

빠르다는 것은 놀라운 일이 아닙니다! 새로운 목록을 만들기 위해 맞춤 제작되었습니다.

CALL_FUNCTION명령 :

코드 처리를 엿볼 때 가장 먼저 보게되는 것은 다음과 같습니다 CALL_FUNCTION.

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

무해 해 보이죠? 글쎄, 안타깝게도 call_function, 함수를 즉시 호출하는 간단한 사람은 아닙니다. 대신 스택에서 객체를 가져와 스택의 모든 인수를 가져온 다음 객체 유형에 따라 전환합니다. 그건:

우리는 list타입을 호출합니다 . 전달 된 인수 call_functionPyList_Type입니다. CPython은 이제 일반 함수를 호출하여 _PyObject_FastCallKeywords더 많은 함수 호출 이라고하는 호출 가능한 오브젝트를 처리해야 합니다.

이 함수는 특정 함수 유형 (이유를 이해할 수 없음)을 다시 확인한 다음 필요한 경우 kwargs에 대한 dict를 작성한 후 계속 호출 _PyObject_FastCallDict합니다.

_PyObject_FastCallDict마침내 우리를 어딘가에 데려옵니다! 수행 한 후 더 많은 검사를잡고 tp_call로부터 슬롯typetype우리입니다, 그것을 잡고, 전달했습니다를 type.tp_call. 그런 다음 전달 된 인수에서 튜플을 만들고 _PyStack_AsTuple마지막 으로 호출을 할 수 있습니다 !

tp_call일치 type.__call__하는 목록 객체를 가져 와서 최종적으로 목록 객체를 만듭니다. 이 목록 호출 __new__에 해당 PyType_GenericNew그것을과 할당 메모리 PyType_GenericAlloc: 이것은 실제로 함께 잡는 부분입니다 PyList_New마지막으로 . 이전의 모든 것은 일반적인 방식으로 객체를 처리하는 데 필요합니다.

결국, type_call전화list.__init__ 사용 가능한 인수로 목록을 하고 초기화 한 다음, 우리가 왔던 방식으로 되돌아갑니다. :-)

마지막으로을 기억 LOAD_NAME하십시오. 여기에 기여하는 또 다른 사람입니다.


입력을 다룰 때 파이썬은 일반적으로 C작업을 수행하기에 적합한 기능을 실제로 찾기 위해 농구대를 뛰어 넘어야한다는 것을 쉽게 알 수 있습니다. 그것은 그것의 동적, 사람이 마스크 수 있기 때문에 즉시 호출의 curtesy이없는 list( 그리고 소년은 많은 사람들이 할 할 ) 다른 경로를주의해야합니다.

곳은 list()많이 상실 다음 탐험 파이썬은 무엇을해야하는지 도대체 알아 할 필요가있다.

반면에 리터럴 구문은 정확히 한 가지를 의미합니다. 변경 될 수 없으며 항상 미리 결정된 방식으로 작동합니다.

각주 : 모든 기능 이름은 릴리스마다 변경 될 수 있습니다. 요점은 여전히 ​​스탠드되며 향후 버전에서는 대부분 가능성이 높습니다.


13

[]보다 빠른 이유는 무엇 list()입니까?

가장 큰 이유는 파이썬 list()이 사용자 정의 함수처럼 취급 한다는 것입니다.list 자신의 서브 클래 싱 된 목록을 사용하거나 deque와 같이 다른 것을 .

로 내장 목록의 새 인스턴스를 즉시 만듭니다 [].

나의 설명은 당신에게 이것에 대한 직감을 제공하려고합니다.

설명

[] 일반적으로 리터럴 구문이라고합니다.

문법에서는이를 "목록 표시"라고합니다. 문서에서 :

목록 표시는 대괄호로 묶인 빈 일련의 표현식입니다.

list_display ::=  "[" [starred_list | comprehension] "]"

목록 표시는 새 목록 객체를 생성하며, 내용은 표현식 목록 또는 이해로 지정됩니다. 쉼표로 구분 된 표현식 목록이 제공되면 해당 요소가 왼쪽에서 오른쪽으로 평가되어 순서대로 목록 오브젝트에 배치됩니다. 이해가 제공 될 때,리스트는 이해의 결과로 구성되는 요소들로 구성됩니다.

요컨대 이것은 유형의 내장 객체를 의미합니다. list 가 작성 .

이것을 피할 수는 없습니다. 즉, 파이썬은 최대한 빨리 할 수 ​​있습니다.

반면 list()에 내장을 만드는 것을 가로 챌 수 있습니다list , 내장 목록 생성자를 사용하여 내장 .

예를 들어, 목록이 시끄럽게 생성되기를 원한다고 가정 해보십시오.

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

그런 다음 list모듈 수준 전역 범위 에서 이름을 가로 챌 수 있으며 list,를 만들 때 실제로 하위 유형 목록을 만듭니다.

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

마찬가지로 전역 네임 스페이스에서 제거 할 수 있습니다.

del list

내장 네임 스페이스에 넣습니다.

import builtins
builtins.list = List

그리고 지금:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

그리고리스트 디스플레이는 무조건리스트를 만듭니다.

>>> list_1 = []
>>> type(list_1)
<class 'list'>

아마도이 작업은 일시적으로 만 수행되므로 변경 사항을 취소하십시오. 먼저 List내장에서 새 객체를 제거하십시오 .

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

아뇨, 우리는 원본을 잃어 버렸습니다.

걱정할 필요는 없지만 여전히 list목록 리터럴의 유형입니다.

>>> builtins.list = type([])
>>> list()
[]

그래서...

[]보다 빠른 이유는 무엇 list()입니까?

우리가 보았 듯이 덮어 쓸 수는 list있지만 리터럴 유형의 생성을 가로 챌 수는 없습니다. 우리가 사용할 때는 list어떤 것이 있는지 확인하기 위해 조회를해야합니다.

그런 다음 우리가 찾은 호출 가능한 것을 호출해야합니다. 문법에서 :

호출은 일련의 빈 인수로 호출 가능한 객체 (예 : 함수)를 호출합니다.

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

우리는 목록뿐만 아니라 모든 이름에 대해 동일한 작업을 수행한다는 것을 알 수 있습니다.

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

들어 []파이썬 바이트 코드 수준에서 함수 호출이 없다 :

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

바이트 코드 수준에서 조회하거나 호출하지 않고 목록을 작성하는 것입니다.

결론

우리는 그 입증 list범위 지정 규칙을 사용하여 사용자 코드를 가로 챌 수 있습니다, 그 list()다음 호출에 대한 외모와 그것을 호출합니다.

반면 []목록 표시 또는 리터럴은 이름 조회 및 함수 호출을 피합니다.


2
+1은 납치 할 수 list있으며 파이썬 컴파일러가 실제로 빈 목록을 반환하는지 확신 할 수 없음 을 지적 합니다.
Beefster
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.