programing

Python 3에서 "100000000000000000000001"이 왜 그렇게 빠른가?

prostudy 2022. 3. 13. 11:04
반응형

Python 3에서 "100000000000000000000001"이 왜 그렇게 빠른가?

이 나의 다.range()Python 3에서 실제로 개체 유형인 함수는 생성기와 유사하게 콘텐츠를 즉석에서 생성한다.

이 경우, 나는 다음 라인이 1,000조원이 범위 내에 있는지 여부를 판단하기 위해서는 1,000조개의 값이 생성되어야 하기 때문에 과도한 시간이 소요될 것으로 예상했을 것이다.

1_000_000_000_000_000 in range(1_000_000_000_000_001)

더욱이, 내가 아무리 0을 더해도, 계산은 거의 같은 시간(기본적으로 즉각적)을 필요로 하는 것 같다.

나 또한 이런 것들을 시도해 보았지만, 계산은 여전히 거의 즉각적이다.

# count by tens
1_000_000_000_000_000_000_000 in range(0,1_000_000_000_000_000_000_001,10)

나만의 레인지 기능을 구현하려고 하면 결과가 별로 좋지 않아!

def my_crappy_range(N):
    i = 0
    while i < N:
        yield i
        i += 1
    return

무엇 때문에 그러십니까?range()후드 아래에서 일을 하는 걸 그렇게 빨리 하는 걸 반대한다고?


마티진 피에테르의 대답은 그 완전성 때문에 선택되었지만, 또한 그것이 무엇을 위한 것인지에 대한 좋은 논의를 위한 첫 번째 대답을 보게 된다.rangePython 3의 전체 시퀀스와 에 대한 잠재적 불일치에 대한 일부 정보/경고__contains__Python 구현 전반에 걸친 기능 최적화. 에바너트의 다른 답변은 좀 더 상세하게 설명되며 Python 3의 최적화 이면에 있는 이력에 관심이 있는 사람들을 위한 링크를 제공한다(그리고 최적화 부족).xrangePython 2).쿡쿡 찌르고 윔으로 대답하면 관심 있는 사람에게 관련 C 소스 코드와 설명이 제공된다.

파이톤 3range()개체는 즉시 숫자를 생산하지 않는다; 그것은 요구숫자를 생산하는 스마트 시퀀스 개체다.여기에 포함된 모든 것은 시작, 중지 및 단계 값이며, 그 다음 정수를 개체 위에 반복할 때 각 반복이 계산된다.

물체는 또한 후크를 구현하고, 당신의 번호가 그 범위의 일부인지 계산한다.계산은 일정한 시간 연산이다범위 내에서 가능한 모든 정수를 스캔할 필요는 없다.

개체 설명서:

range규칙적으로 타이핑하다.list또는tuple범위 객체는 (그것이 단지 저장하기 때문에) 나타내는 범위의 크기와 상관없이 항상 같은 (작은) 메모리 양을 차지한다는 것이다.start stop그리고step값, 필요에 따라 개별 항목 및 하위 범위 계산).

그러니까 최소한 당신의.range()객체에서 수행할 수 있는 작업:

class my_range:
    def __init__(self, start, stop=None, step=1, /):
        if stop is None:
            start, stop = 0, start
        self.start, self.stop, self.step = start, stop, step
        if step < 0:
            lo, hi, step = stop, start, -step
        else:
            lo, hi = start, stop
        self.length = 0 if lo > hi else ((hi - lo - 1) // step) + 1

    def __iter__(self):
        current = self.start
        if self.step < 0:
            while current > self.stop:
                yield current
                current += self.step
        else:
            while current < self.stop:
                yield current
                current += self.step

    def __len__(self):
        return self.length

    def __getitem__(self, i):
        if i < 0:
            i += self.length
        if 0 <= i < self.length:
            return self.start + i * self.step
        raise IndexError('my_range object index out of range')

    def __contains__(self, num):
        if self.step < 0:
            if not (self.stop < num <= self.start):
                return False
        else:
            if not (self.start <= num < self.stop):
                return False
        return (num - self.start) % self.step == 0

이것은 여전히 진짜인 몇 가지를 놓치고 있다.range(): 지만:.index()또는.count()방법, 해싱, 평등 테스트 또는 슬라이싱) 그러나 아이디어를 제공해야 한다.

나는 또한 단순화했다.__contains__정수 테스트에만 집중하기 위한 구현; 실제를 제공하는 경우range()비반복적 값(하위 클래스 포함)을 대상으로 한다.int() 포함된 모든 값 목록에 대해 격납용기 테스트를 사용하는 것처럼, 일치하는 것이 있는지 확인하기 위해 저속 스캔을 시작한다.이는 정수로 동등성 시험을 지원할 때 발생하지만 정수 산술도 지원할 것으로 예상되지 않는 다른 숫자 유형을 계속 지원하기 위해 수행되었다.격납 테스트를 구현한 원래 Python 문제를 참조하십시오.


* Python 정수는 한이 없기 때문에 거의 일정한 시간이고, 따라서 수학 연산 또한 N이 증가함에 따라 시간에 따라 증가하므로 O(log N) 연산이다.최적화된 C 코드로 모두 실행되며 파이썬은 정수 값을 30비트 청크로 저장하기 때문에 여기에 포함된 정수의 크기로 인해 성능에 미치는 영향을 보기 전에 메모리가 부족할 수 있다.

여기서 근본적인 오해는 다음과 같은 생각에서 비롯된다.range발전기다.그그 . 사실 어떤 사실, 그것은 어떤 종류의 반복자도 아니다.

당신은 이것을 꽤 쉽게 알 수 있다.

>>> a = range(5)
>>> print(list(a))
[0, 1, 2, 3, 4]
>>> print(list(a))
[0, 1, 2, 3, 4]

발전기일 경우, 한 번 반복하면 다음과 같이 소진된다.

>>> b = my_crappy_range(5)
>>> print(list(b))
[0, 1, 2, 3, 4]
>>> print(list(b))
[]

무엇range사실, 리스트와 같은 시퀀스야당신은 심지어 이것을 테스트 할 수도 있다.

>>> import collections.abc
>>> isinstance(a, collections.abc.Sequence)
True

이것은 그것이 연속이 되는 모든 규칙을 따라야 한다는 것을 의미한다.

>>> a[3]         # indexable
3
>>> len(a)       # sized
5
>>> 3 in a       # membership
True
>>> reversed(a)  # reversible
<range_iterator at 0x101cd2360>
>>> a.index(3)   # implements 'index'
3
>>> a.count(3)   # implements 'count'
1

의 .range그리고 alist저것은 a이다range게으르거나 역동적인 연속이다; 그것은 그것의 모든 가치를 기억하지 못하고 단지 그것의 가치를 기억한다.start stop그리고step, 그리고 온 디맨드 가치 창출__getitem__.

(부기사항으로, 만약 당신이 한다면.print(iter(a))에 주목하게 될 것이다.range같은 것을 사용하다listiterator로 타이핑하다.list어떻게 되는가?listiterator에 대해 특별한 것을 사용하지 않다list는 C로 시행하고 있다.__getitem__, 그래서 그것은 잘 작동한다.range역시)


이제, 그 때, 그 라고 하는 Sequence.__contains__항상 일정한 시간이어야 한다. 사실 다음과 같은 명확한 시퀀스의 예에 대해서는list절대 아닙니다.그러나 있을 없는 일이라고 하는 것은 아무것도 없다.그리고 구현이 더 용이하다.range.__contains__그냥 수학적으로 확인하다 ((val - start) % step, 그러나 부정적인 단계를 다루기 위한 약간의 추가적인 복잡성으로) 모든 값을 실제로 생성하고 테스트하는 것보다 더 나은 방법으로 하지 않는 이유는 무엇인가?

그러나 언어에는 이런 일이 일어날 이라고 보장해 주는 것은 아무것도 없는 것 같다.아슈히니 차우드하리가 지적하듯이 정수로 변환하여 수학적 테스트를 하는 대신 비통합적 값을 부여하면 모든 값을 반복하여 하나하나 비교하는 것으로 되돌아간다.그리고 CPython 3.2+와 PyPy 3.x 버전이 우연히 이러한 최적화를 포함하고 있으며, 그것은 명백하게 좋은 아이디어고 하기 쉽다고 해서, IronPython이나 NewKickAssPython 3.x가 그것을 빼놓을 이유가 없다.(그리고 사실 CPython 3.0-3.1은 포함하지 않았다.)


만약range 사사추세츠 기게어 .my_crappy_range테스트하는 건 말이 안 돼__contains__이런 식으로, 아니면 적어도 이치에 맞는 방법은 분명하지 않을 것이다.처음 3개의 값을 이미 반복했다면1가만히in발전기?테스트해야 함1그것을 반복하게 하고 모든 값을 소비하게 하다.1(또는 첫 번째 값까지)>= 1)?

소스를 사용해, 루크!

CPython은range(...).__contains__(method wrapper)는 결국 값이 범위 내에 있을 수 있는지 확인하는 간단한 계산을 위임한다.여기서 속도를 내는 이유는 우리가 범위 객체를 직접 반복하기 보다는 한계에 대한 수학적 추론을 사용하고 있기 때문이다.사용된 논리를 설명하려면:

  1. 번호가 다음 사이에 있는지 확인하십시오.start그리고stop그리고
  2. 보폭 값이 우리 번호를 "넘어가지" 않는지 확인하십시오.

예를 들어,994에 있다range(4, 1000, 2)이유:

  1. 4 <= 994 < 1000그리고
  2. (994 - 4) % 2 == 0.

전체 C 코드는 아래에 포함되어 있는데, 메모리 관리와 참조 카운팅 세부사항 때문에 좀 더 장황하지만, 기본 아이디어는 다음과 같다.

static int
range_contains_long(rangeobject *r, PyObject *ob)
{
    int cmp1, cmp2, cmp3;
    PyObject *tmp1 = NULL;
    PyObject *tmp2 = NULL;
    PyObject *zero = NULL;
    int result = -1;

    zero = PyLong_FromLong(0);
    if (zero == NULL) /* MemoryError in int(0) */
        goto end;

    /* Check if the value can possibly be in the range. */

    cmp1 = PyObject_RichCompareBool(r->step, zero, Py_GT);
    if (cmp1 == -1)
        goto end;
    if (cmp1 == 1) { /* positive steps: start <= ob < stop */
        cmp2 = PyObject_RichCompareBool(r->start, ob, Py_LE);
        cmp3 = PyObject_RichCompareBool(ob, r->stop, Py_LT);
    }
    else { /* negative steps: stop < ob <= start */
        cmp2 = PyObject_RichCompareBool(ob, r->start, Py_LE);
        cmp3 = PyObject_RichCompareBool(r->stop, ob, Py_LT);
    }

    if (cmp2 == -1 || cmp3 == -1) /* TypeError */
        goto end;
    if (cmp2 == 0 || cmp3 == 0) { /* ob outside of range */
        result = 0;
        goto end;
    }

    /* Check that the stride does not invalidate ob's membership. */
    tmp1 = PyNumber_Subtract(ob, r->start);
    if (tmp1 == NULL)
        goto end;
    tmp2 = PyNumber_Remainder(tmp1, r->step);
    if (tmp2 == NULL)
        goto end;
    /* result = ((int(ob) - start) % step) == 0 */
    result = PyObject_RichCompareBool(tmp2, zero, Py_EQ);
  end:
    Py_XDECREF(tmp1);
    Py_XDECREF(tmp2);
    Py_XDECREF(zero);
    return result;
}

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

아이디어의 "고기"는 다음 에서 언급된다.

/* result = ((int(ob) - start) % step) == 0 */ 

마지막 메모로 - 다음 항목을 보십시오.range_contains코드 조각의 하단에서 기능한다.정확한 유형 확인이 실패할 경우, 우리는 설명한 영리한 알고리즘을 사용하지 않고 대신 범위를 다시 단순 반복 검색으로 되돌린다._PySequence_IterSearch에서 이 할 수 .5있다 통역기에서 이 동작을 확인할 수 있다(여기서 v3.5.0을 사용하고 있다).

>>> x, r = 1000000000000000, range(1000000000000001)
>>> class MyInt(int):
...     pass
... 
>>> x_ = MyInt(x)
>>> x in r  # calculates immediately :) 
True
>>> x_ in r  # iterates for ages.. :( 
^\Quit (core dumped)

Martijn의 대답에 덧붙여 말하자면, 이것은 소스의 관련 부분(C에서, 범위 객체가 네이티브 코드로 쓰여 있기 때문에):

static int
range_contains(rangeobject *r, PyObject *ob)
{
    if (PyLong_CheckExact(ob) || PyBool_Check(ob))
        return range_contains_long(r, ob);

    return (int)_PySequence_IterSearch((PyObject*)r, ob,
                                       PY_ITERSEARCH_CONTAINS);
}

그러니깐PyLong물체(그것은)int 3에서는 Python 3)을 한다.range_contains_long결과를 결정하는 기능.그리고 그 함수는 본질적으로ob지정된 범위 안에 있다(C에서 좀 더 복잡해 보이지만).

만약 그게 아니라면.int값을 찾을 때까지(또는 찾을 수 없을 때까지) 반복으로 되돌아간다.

이 모든 논리는 다음과 같이 사이비 피톤으로 번역될 수 있다.

def range_contains (rangeObj, obj):
    if isinstance(obj, int):
        return range_contains_long(rangeObj, obj)

    # default logic by iterating
    return any(obj == x for x in rangeObj)

def range_contains_long (r, num):
    if r.step > 0:
        # positive step: r.start <= num < r.stop
        cmp2 = r.start <= num
        cmp3 = num < r.stop
    else:
        # negative step: r.start >= num > r.stop
        cmp2 = num <= r.start
        cmp3 = r.stop < num

    # outside of the range boundaries
    if not cmp2 or not cmp3:
        return False

    # num must be on a valid step inside the boundaries
    return (num - r.start) % r.step == 0

최적화가 왜 에 추가되었는지 궁금하다면range.__contains__, 그리고 왜 그것이 추가되지 않았는지.xrange.__contains__2.:

첫째, 애쉬위니 차우드하리가 발견한 바와 같이, 1766304호를 명시적으로 개방하여 최적화하였다.[x]range.__contains__. 3.2에 대한 패치가 승인되어 체크인되었지만, "이 때문에 2.7로 백포팅되지 않았다.xrange오랫동안 이렇게 행동해 왔기 때문에 이렇게 늦게 패치를 저지르면 우리가 무엇을 얻을 수 있는지 모르겠다."(2.7은 거의그 시점에 나왔다.)오랫동안 이렇게 행동해 왔기 때문에 이렇게 늦게 패치를 저지르면 우리가 무엇을 얻을 수 있는지 모르겠다."(2.7은 거의그시점에 나왔다.)

한편:

원raxrange눈에 띄지 않는 물건이었어3.1 문서에 따르면:

범위 객체는 동작이 거의 없다. 즉 인덱싱, 반복 및 데이터만 지원한다.len기능을 하다

이것은 그다지 사실이 아니었다.xrange개체는 실제로 인덱싱과 함께 자동으로 제공되는 몇 가지 다른 것들을 지원했다.len,* 포함__contains__(선형 검색을 통해).하지만 아무도 그 당시에는 그것들을 완전한 순서에 맞게 만들 가치가 있다고 생각하지 않았다.

그 후, 추상 베이스 클래스 PEP의 이행의 일환으로, 어떤 빌트인 타입이 어떤 ABC를 구현하는 것으로 표시되어야 하는가를 파악하는 것이 중요했다.xrange/range시행을 주장하다collections.Sequence비록 그것이 여전히 같은 "아주 작은 행동"만을 다루었음에도 불구하고.9213호까지는 아무도 그 문제를 눈치채지 못했다.해당 문제에 대한 패치가 추가되었을 뿐만 아니라index그리고count3.2까지range, 또한 최적화된 것을 재조정한다.__contains__(그것은 같은 수학을 공유한다.index, 그리고 직접적으로 사용된다.count **변경은 3.2에도 적용되었으며, "새로운 방법을 추가하는 버그 픽스"이기 때문에 2.x로 보고되지 않았다.(이 시점에서 2.7은 이미 rc 상태를 초과했다.)

따라서 이 최적화를 2.7로 다시 보고할 수 있는 기회가 두 번 있었지만 둘 다 거절당했다.


* 사실, 인덱싱만으로도 무료로 반복이 가능하지만, 2.3에서는 xrange객체에 사용자 정의 반복기가 있음.

** 첫 번째 버전은 실제로 이를 재조명하고 세부사항을 잘못 이해함(예: 다음과 같은 경우)MyIntSubclass(2) in range(5) == False그러나 Daniel Stutzbach의 업데이트 버전 패치는 일반적이고 느린 패치를 포함하여 이전 코드 대부분을 복구했다._PySequence_IterSearch저 3.2 이전range.__contains__최적화가 적용되지 않을 때 암묵적으로 사용하고 있었다.

다른 답은 이미 잘 설명했지만, 범위 객체의 특성을 보여주는 또 다른 실험을 제안하고자 한다.

>>> r = range(5)
>>> for i in r:
        print(i, 2 in r, list(r))
        
0 True [0, 1, 2, 3, 4]
1 True [0, 1, 2, 3, 4]
2 True [0, 1, 2, 3, 4]
3 True [0, 1, 2, 3, 4]
4 True [0, 1, 2, 3, 4]

a는range객체는 그 범위를 기억하는 물체로, 일회성 발전기만이 아니라 여러 번(반복하는 동안에도) 사용할 수 있다.

그것은 모두 평가에 대한 게으른 접근과 약간의 추가적인 최적화에 관한 것이다.range될 더 계산할가 없다 범위의 값은 실제 사용이 될 때까지 또는 추가 최적화로 인해 더 이상 계산할 필요가 없다.

그런데, 당신의 정수는 그렇게 크지 않다, 고려해봐.sys.maxsize

sys.maxsize in range(sys.maxsize) 꽤 빠르다

최적화 때문에 주어진 정수를 최소 및 최대 범위의 정수와 비교하기가 쉽다.

그러나:

Decimal(sys.maxsize) in range(sys.maxsize) 꽤 느리다.

(이 에, 에 가가 없이 다다)에는.range, 그래서 만약 python이 예기치 않은 소수점을 받는다면 python은 모든 숫자를 비교할 것이다.)

구현 세부사항을 알고 있어야 하지만 나중에 변경될 수 있기 때문에 신뢰할 수 없다.

TL;DR

반환된에 range()사실은 a이다.range③의 ③기하다할 수 이 개체는 발생기, 목록 또는 튜플처럼 순차적으로 값을 반복할 수 있도록 반복기 인터페이스를 구현한다.

그러나 그것은 또한 실제로 물체가 오른쪽에 나타날 때 호출되는 인터페이스도 구현한다.in교환원의__contains__()방법은 a를 반환한다.bool이 의의의 의 여부.in목적지에 있다.이후range물체는 그 한계와 속도를 알고 있다. 이것은 O(1)에서 구현하기 매우 쉽다.

  1. 최적화 때문에 주어진 정수를 최소 및 최대 범위만으로 비교하는 것은 매우 쉽다.
  2. Python3에서 레인지() 함수가 이렇게 빠른 이유는 여기서 레인지 객체의 직접 반복이 아닌 경계에 대한 수학적 추론을 사용하기 때문이다.
  3. 여기서 논리를 설명하려면:
  • 번호가 시작과 중지 사이에 있는지 확인하십시오.
  • 스텝 정밀도가 우리 번호를 넘지 않는지 확인해봐.
  1. 예를 들어, 997은 다음과 같은 이유로 범위(4, 1000, 3)에 있다.

    4 <= 997 < 1000, and (997 - 4) % 3 == 0.

해보다x-1 in (i for i in range(x))대체로x제너레이터의 이해를 사용하여 호출되지 않는 값range.__contains__최적화

TLDR;range그 물체가 거기에 있는지 매우 쉽게 계산할 수 있는 산술 시리즈다.만약 그것이 정말 빨리 목록화된다면 그것은 그것의 지수까지 얻을 수 있을 것이다.

참조URL: https://stackoverflow.com/questions/30081275/why-is-1000000000000000-in-range1000000000000001-so-fast-in-python-3

반응형