web-dev-qa-db-ja.com

Python 3.x整数の場合、ビットシフトよりも2倍高速ですか?

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)])

enter image description hereenter image description here

質問:

テストは有効ですか?もしそうなら、なぜ(乗算、シフト)は(シフト、シフト)より速いのですか?

Ubuntu 14.04でPython 3.5を実行します。

編集

上記は質問の元の声明です。ダン・ゲッツは、彼の答えで優れた説明を提供します。

完全を期すために、乗算の最適化が適用されない場合の大きなxのサンプル図を次に示します。

enter image description hereenter image description here

これは、小さい数字の左シフトがそうでないように、CPython 3.5で小さい数字の乗算が最適化されているためと思われます。正の左シフトは常に計算の一部として結果を格納するために大きな整数オブジェクトを作成しますが、テストで使用した並べ替えの乗算では、特別な最適化によりこれが回避され、正しいサイズの整数オブジェクトが作成されます。これは Pythonの整数実装のソースコード で見ることができます。

Pythonの整数は任意の精度であるため、整数「桁」の配列として格納され、整数桁あたりのビット数に制限があります。そのため、一般的なケースでは、整数を含む演算は単一の演算ではなく、複数の「数字」の場合を処理する必要があります。 pyport.hでは、このビット制限 定義 64ビットプラットフォームでは30ビット、それ以外の場合は15ビット。 (説明を簡単にするため、ここからこの30を呼び出します。ただし、32ビット用にコンパイルされたPythonを使用している場合、ベンチマークの結果はxが32,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ビットの数字に収まる2つの整数を乗算する場合、整数を配列として扱うのではなく、CPythonインタープリターによる直接の乗算として行われます。 ( MEDIUM_VALUE() は、正の整数オブジェクトで呼び出され、最初の30ビットの数字を取得するだけです。)結果が1つの30ビットの数字に収まる場合、 PyLong_FromLongLong() は、比較的少数の操作で、1桁の整数オブジェクトを作成して保存します。

対照的に、左シフトはこのように最適化されておらず、すべての左シフトは配列としてシフトされる整数を扱います。特に、 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() を使用して商剰余の両方を計算します。この剰余は、 乗算 、および 新たに割り当てられた整数オブジェクトに格納されます で小さな除数に対して計算され、この状況ではすぐに破棄されます。

147
Dan Getz