Python 3.x 정수의 경우 비트 시프트보다 2 배 빠릅니다.


150

나는 sorted_containers 의 출처를보고 있었고이 을보고 놀랐습니다 .

self._load, self._twice, self._half = load, load * 2, load >> 1

여기 load정수가 있습니다. 한 곳에서 비트 이동을 사용하고 다른 곳에서 곱셈을 사용하는 이유는 무엇입니까? 비트 쉬프팅이 2의 정수 나누기보다 빠를 수도 있지만, 곱셈을 쉬프트로 대체하지 않는 이유는 무엇입니까? 다음과 같은 경우를 벤치마킹했습니다.

  1. (시간, 나누기)
  2. (시프트, 시프트)
  3. (시간, 교대)
  4. (시프트, 나누기)

# 3이 다른 대안보다 지속적으로 빠릅니다.

# self._load, self._twice, self._half = load, load * 2, load >> 1

import random
import timeit
import pandas as pd

x = random.randint(10 ** 3, 10 ** 6)

def test_naive():
    a, b, c = x, 2 * x, x // 2

def test_shift():
    a, b, c = x, x << 1, x >> 1    

def test_mixed():
    a, b, c = x, x * 2, x >> 1    

def test_mixed_swapped():
    a, b, c = x, x << 1, x // 2

def observe(k):
    print(k)
    return {
        'naive': timeit.timeit(test_naive),
        'shift': timeit.timeit(test_shift),
        'mixed': timeit.timeit(test_mixed),
        'mixed_swapped': timeit.timeit(test_mixed_swapped),
    }

def get_observations():
    return pd.DataFrame([observe(k) for k in range(100)])

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오

질문:

시험이 유효합니까? 그렇다면 왜 (곱하기, 시프트)가 (shift, shift)보다 더 빠릅니까?

우분투 14.04에서 Python 3.5를 실행합니다.

편집하다

위는 질문의 원래 진술입니다. Dan Getz는 그의 답변에서 훌륭한 설명을 제공합니다.

완전성을 위해 x곱셈 최적화가 적용되지 않을 때 더 큰 샘플 그림이 있습니다.

여기에 이미지 설명을 입력하십시오 여기에 이미지 설명을 입력하십시오


3
어디서 정의 x했습니까?
JBernardo

3
리틀 엔디안 / 빅 엔디안을 사용하여 차이점이 있는지 확인하고 싶습니다. 정말 멋진 질문 btw!
LiGhTx117

1
@ LiGhTx117 x메모리가 메모리에 어떻게 저장되는지에 대한 질문이기 때문에 매우 크지 않은 한 작업과 관련이 없을 것으로 기대합니다 .
Dan Getz

1
궁금합니다. 2를 나누는 대신 0.5를 곱하면 어떻습니까? 밉 어셈블리 프로그래밍에 대한 이전 경험에서 나눗셈은 일반적으로 어쨌든 곱셈 연산을 발생시킵니다. (그것은 나누기 대신 비트 시프트의 선호를 설명 할 것입니다)
Sayse

2
@Sayse는 부동 소수점으로 변환합니다. 정수 플로트 분할은 부동 소수점을 통한 왕복보다 빠를 것입니다.
Dan Getz

답변:


155

CPython 3.5에서는 작은 숫자의 곱셈이 왼쪽 시프트가 아닌 방식으로 최적화 되었기 때문입니다. 양의 왼쪽 시프트는 항상 계산의 일부로 결과를 저장하기 위해 더 큰 정수 오브젝트를 작성하는 반면 테스트에서 사용한 정렬의 곱셈의 경우 특수 최적화는이를 피하고 올바른 크기의 정수 오브젝트를 작성합니다. 이것은 파이썬 정수 구현의 소스 코드 에서 볼 수 있습니다 .

파이썬의 정수는 임의의 정밀도이므로 정수 "비트"의 배열로 저장되며 정수 숫자 당 비트 수에 제한이 있습니다. 따라서 일반적으로 정수와 관련된 연산은 단일 연산이 아니라 여러 "숫자"의 경우를 처리해야합니다. 에서는 pyport.h 이 비트 한계 로 정의 달리 64 비트 플랫폼에 30 비트 또는 15 비트. (설명을 간단하게 유지하기 위해 여기에서이 30을 호출하겠습니다. 그러나 32 비트 용으로 컴파일 된 Python을 사용하는 경우 벤치 마크 결과는 x32,768보다 작은 지 여부에 따라 달라집니다 .)

작업의 입력 및 출력이이 30 비트 제한 내에 있으면 작업을 일반적인 방식 대신 최적화 된 방식으로 처리 할 수 ​​있습니다. 정수 곱셈 구현 의 시작은 다음과 같습니다.

static PyObject *
long_mul(PyLongObject *a, PyLongObject *b)
{
    PyLongObject *z;

    CHECK_BINOP(a, b);

    /* fast path for single-digit multiplication */
    if (Py_ABS(Py_SIZE(a)) <= 1 && Py_ABS(Py_SIZE(b)) <= 1) {
        stwodigits v = (stwodigits)(MEDIUM_VALUE(a)) * MEDIUM_VALUE(b);
#ifdef HAVE_LONG_LONG
        return PyLong_FromLongLong((PY_LONG_LONG)v);
#else
        /* if we don't have long long then we're almost certainly
           using 15-bit digits, so v will fit in a long.  In the
           unlikely event that we're using 30-bit digits on a platform
           without long long, a large v will just cause us to fall
           through to the general multiplication code below. */
        if (v >= LONG_MIN && v <= LONG_MAX)
            return PyLong_FromLong((long)v);
#endif
    }

따라서 각각 30 비트 숫자에 맞는 두 개의 정수를 곱하면 정수로 배열을 사용하는 대신 CPython 인터프리터가 직접 곱셈을 수행합니다. ( MEDIUM_VALUE()양의 정수 오브젝트에서 호출되면 단순히 첫 번째 30 비트 숫자를 가져옵니다.) 결과가 단일 30 비트 숫자에 맞는 경우 PyLong_FromLongLong()상대적으로 적은 수의 조작에서이를 감지하고 저장할 단일 숫자 정수 오브젝트를 작성합니다. 그것.

반대로 왼쪽 시프트는이 방법으로 최적화되지 않으며 모든 왼쪽 시프트는 정수가 배열로 시프트되는 것을 처리합니다. 당신의 소스 코드를 보면 특히, long_lshift(): 작지만 긍정적 인 왼쪽 교대의 경우, 단지 길이가 1 이상으로 절단 한 것으로 경우, 2 자리 정수 객체는 항상 생성됩니다 (내 의견에 /*** ***/)

static PyObject *
long_lshift(PyObject *v, PyObject *w)
{
    /*** ... ***/

    wordshift = shiftby / PyLong_SHIFT;   /*** zero for small w ***/
    remshift  = shiftby - wordshift * PyLong_SHIFT;   /*** w for small w ***/

    oldsize = Py_ABS(Py_SIZE(a));   /*** 1 for small v > 0 ***/
    newsize = oldsize + wordshift;
    if (remshift)
        ++newsize;   /*** here newsize becomes at least 2 for w > 0, v > 0 ***/
    z = _PyLong_New(newsize);

    /*** ... ***/
}

정수 나누기

당신은 (그리고 나의) 기대에 맞기 때문에 올바른 시프트와 비교할 때 정수 층 나누기의 더 나쁜 성능에 대해서는 묻지 않았습니다. 그러나 작은 양수를 다른 작은 양수로 나누는 것도 작은 곱셈만큼 최적화되지 않습니다. 모든 함수를 사용하여 // 나머지를 모두 계산합니다 long_divrem(). 이 나머지와 작은 제수에 대한 계산 된 승산 , 및 새롭게 할당 된 정수 객체에 저장되고 ,이 상태에서 즉시 폐기된다.


1
이 부분을 지적 해 주셔서 감사합니다. 이것이 전반적으로 훌륭한 답변이라는 것은 말할 필요도 없습니다.
hilberts_drinking_problem

훌륭한 질문에 대한 잘 연구 된 서면 답변. x최적화 된 범위 를 벗어난 타이밍에 대한 그래프를 표시하는 것이 흥미로울 수 있습니다 .
Barmar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.