เร็วกว่า bit-shift สองเท่าสำหรับ Python 3.x จำนวนเต็ม?


150

ฉันดูที่แหล่งที่มาของsort_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)])

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่

คำถาม:

การทดสอบของฉันใช้ได้หรือไม่ ถ้าเป็นเช่นนั้นทำไม (คูณ, กะ) เร็วกว่า (กะ, กะ)?

ฉันใช้ Python 3.5 บน Ubuntu 14.04

แก้ไข

ด้านบนเป็นข้อความดั้งเดิมของคำถาม Dan Getz ให้คำอธิบายที่ดีเยี่ยมในคำตอบของเขา

เพื่อความสมบูรณ์นี่คือตัวอย่างของตัวอย่างที่มีขนาดใหญ่กว่าxเมื่อไม่ใช้การเพิ่มประสิทธิภาพการคูณ

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่


3
คุณกำหนดไว้xที่ไหน
JBernardo

3
ฉันอยากจะดูว่ามีความแตกต่างใด ๆ โดยใช้ endian น้อย / ใหญ่ endian คำถามเจ๋งจริงๆ btw!
LiGhTx117

1
@ LiGhTx117 ฉันคาดหวังว่าจะไม่เกี่ยวข้องกับการดำเนินการเว้นแต่xจะมีขนาดใหญ่มากเพราะนั่นเป็นเพียงคำถามที่ว่ามันถูกเก็บไว้ในหน่วยความจำใช่ไหม?
Dan Getz

1
ฉันอยากรู้อยากเห็นการคูณด้วย 0.5 แทนที่จะหารด้วย 2 เป็นอย่างไร จากประสบการณ์ที่ผ่านมาด้วยการเขียนโปรแกรมชุดประกอบ mips การแบ่งตามปกติจะส่งผลให้เกิดการดำเนินการคูณ (นั่นจะอธิบายการตั้งค่าของการขยับบิตแทนการแบ่ง)
Sayse

2
@ บอกว่าจะแปลงเป็นทศนิยม หวังว่าการแบ่งชั้นจำนวนเต็มจะเร็วกว่าการเดินทางไป - กลับผ่านจุดลอย
Dan Getz

คำตอบ:


155

สิ่งนี้น่าจะเป็นเพราะการคูณตัวเลขขนาดเล็กได้รับการปรับให้เหมาะสมใน CPython 3.5 ในลักษณะที่การเลื่อนซ้ายโดยตัวเลขขนาดเล็กนั้นไม่ใช่ การเลื่อนไปทางซ้ายที่เป็นบวกจะสร้างวัตถุจำนวนเต็มที่ใหญ่กว่าเสมอเพื่อเก็บผลลัพธ์ซึ่งเป็นส่วนหนึ่งของการคำนวณในขณะที่สำหรับการคูณของการเรียงลำดับที่คุณใช้ในการทดสอบการเพิ่มประสิทธิภาพพิเศษหลีกเลี่ยงสิ่งนี้และสร้างวัตถุจำนวนเต็ม นี้สามารถเห็นได้ในรหัสที่มาของการดำเนินจำนวนเต็ม

เนื่องจากจำนวนเต็มใน Python เป็นความแม่นยำโดยพลการพวกมันจะถูกจัดเก็บเป็นอาร์เรย์ของ "จำนวน" จำนวนเต็มโดยมีการ จำกัด จำนวนบิตต่อจำนวนหลัก ดังนั้นในกรณีทั่วไปการดำเนินการที่เกี่ยวข้องกับจำนวนเต็มไม่ใช่การดำเนินการเดียว แต่ต้องจัดการกับกรณีของ "หลัก" หลายรายการแทน ในpyport.hขีด จำกัด บิตนี้ถูกกำหนดเป็น 30 บิตบนแพลตฟอร์ม 64 บิตหรือ 15 บิตเป็นอย่างอื่น (ฉันจะเรียกสิ่งนี้ 30 จากตรงนี้เพื่อให้คำอธิบายนั้นง่าย แต่โปรดทราบว่าถ้าคุณใช้ Python ที่คอมไพล์ด้วย 32 บิตผลลัพธ์ของการวัดประสิทธิภาพของคุณจะขึ้นอยู่กับว่า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 บิตนี่เป็นการกระทำที่เป็นการคูณโดยตรงโดยล่าม CPython แทนที่จะทำงานกับจำนวนเต็มเป็นอาร์เรย์ ( MEDIUM_VALUE()เรียกว่าบนวัตถุจำนวนเต็มบวกจะได้รับเพียง 30 บิตแรก) หากผลลัพธ์นั้นพอดีในหลัก 30 บิตเดียวPyLong_FromLongLong()จะสังเกตเห็นสิ่งนี้ในการดำเนินการที่ค่อนข้างน้อยและสร้างวัตถุจำนวนเต็มเดียวเพื่อเก็บ มัน.

ในทางตรงกันข้ามกะซ้ายจะไม่ได้รับการปรับปรุงด้วยวิธีนี้และการเลื่อนซ้ายทุกครั้งจะเกี่ยวข้องกับจำนวนเต็มที่ถูกเลื่อนเป็นอาร์เรย์ โดยเฉพาะอย่างยิ่งถ้าคุณดูซอร์สโค้ดlong_lshift()ในกรณีของการเลื่อนซ้ายเล็ก ๆ แต่เป็นบวกวัตถุจำนวนเต็ม 2 หลักจะถูกสร้างขึ้นเสมอหากมีความยาวของมันถูกตัดเหลือ 1 ในภายหลัง: (ความคิดเห็นของฉัน/*** ***/)

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.