ทำไมอาร์เรย์ของ Python จึงช้า


153

ฉันคาดว่าarray.arrayจะเร็วกว่ารายการเนื่องจากอาร์เรย์ดูเหมือนจะไม่มีกล่อง

อย่างไรก็ตามฉันได้รับผลลัพธ์ต่อไปนี้:

In [1]: import array

In [2]: L = list(range(100000000))

In [3]: A = array.array('l', range(100000000))

In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop

In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop

In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop

In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop

อะไรคือสาเหตุของความแตกต่างดังกล่าว


4
เครื่องมือจำนวนมากสามารถใช้ประโยชน์จากอาเรย์ของคุณได้อย่างมีประสิทธิภาพ:% timeit np.sum (A): 100 ลูป, ดีที่สุดคือ 3: 8.87 ms ต่อลูป
BM

6
ฉันไม่เคยเจอสถานการณ์ที่ฉันจำเป็นต้องใช้arrayแพ็คเกจ หากคุณต้องการที่จะทำจำนวนเงินที่สำคัญของคณิตศาสตร์ Numpy ทำงานที่ความเร็วแสง (เช่น C) และมักจะดีกว่าการใช้งานที่ไร้เดียงสาของสิ่งที่ต้องการsum())
Nick T

40
ผู้ลงคะแนนปิด: ทำไมต้องอิงตามความคิดเห็นนี้ OP ดูเหมือนจะถามคำถามเฉพาะทางเทคนิคเกี่ยวกับปรากฏการณ์ที่สามารถวัดได้และทำซ้ำได้
เควิน

5
@NickT อ่านเรื่องเล็ก ๆ น้อยเพิ่มประสิทธิภาพ ปรากฎว่าarrayค่อนข้างเร็วในการแปลงสตริงจำนวนเต็ม (แทน ASCII ไบต์) เป็นstrวัตถุ กุยโด้เองก็มาด้วยวิธีนี้หลังจากแก้ปัญหาอื่น ๆ มากมายและรู้สึกประหลาดใจกับประสิทธิภาพ อย่างไรก็ตามนี่เป็นที่เดียวที่ฉันจำได้ว่ามันมีประโยชน์ numpyดีกว่ามากสำหรับการจัดการกับอาร์เรย์ แต่มันเป็นการพึ่งพาบุคคลที่สาม
Bakuriu

คำตอบ:


221

ที่เก็บข้อมูลเป็น "unboxed" แต่ทุกครั้งที่คุณเข้าถึงองค์ประกอบ Python ต้อง "box" มัน (ฝังไว้ในวัตถุ Python ปกติ) เพื่อที่จะทำอะไรกับมัน ตัวอย่างเช่นคุณsum(A)วนซ้ำแถวและกล่องจำนวนเต็มทีละหนึ่งในintวัตถุหลามปกติ นั่นทำให้เสียเวลา ในของคุณsum(L)การชกมวยทั้งหมดเสร็จสิ้นในเวลาที่มีการสร้างรายการ

ดังนั้นในที่สุดอาร์เรย์โดยทั่วไปจะช้ากว่า แต่ต้องการหน่วยความจำน้อยกว่าอย่างมาก


นี่คือรหัสที่เกี่ยวข้องจาก Python 3 รุ่นล่าสุด แต่แนวคิดพื้นฐานเดียวกันนี้ใช้กับการปรับใช้ CPython ทั้งหมดตั้งแต่ Python เปิดตัวครั้งแรก

นี่คือรหัสในการเข้าถึงรายการ:

PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
    /* error checking omitted */
    return ((PyListObject *)op) -> ob_item[i];
}

มีน้อยมาก: somelist[i]เพียงแค่คืนค่าiวัตถุที่ 'ในรายการ (และวัตถุ Python ทั้งหมดใน CPython เป็นตัวชี้ไปยังโครงสร้างที่ส่วนเริ่มต้นสอดคล้องกับรูปแบบของ a struct PyObject)

และนี่คือการ__getitem__ดำเนินการสำหรับarrayด้วยรหัสประเภทl:

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

หน่วยความจำดิบจะถือว่าเป็นเวกเตอร์ของC longจำนวนเต็มแพลตฟอร์มพื้นเมือง; i'TH C longถูกอ่านขึ้น จากนั้นPyLong_FromLong()ถูกเรียกให้ห่อ ("กล่อง") ดั้งเดิมC longในlongวัตถุPython (ซึ่งใน Python 3 ซึ่งกำจัดความแตกต่างของ Python 2 ระหว่างintและlongแสดงจริงเป็นประเภทint)

มวยนี้จะต้องจัดสรรหน่วยความจำใหม่สำหรับintวัตถุPython และสเปรย์C longบิตพื้นเมืองของมัน ในบริบทของตัวอย่างดั้งเดิมอายุการใช้งานของวัตถุนี้สั้นมาก (นานพอที่sum()จะเพิ่มเนื้อหาลงในผลรวมสะสม) และจากนั้นจำเป็นต้องใช้เวลามากขึ้นในการยกเลิกการจัดสรรintวัตถุใหม่

นี่คือที่ซึ่งความแตกต่างความเร็วมาจากเสมอมาและจะมาจากเสมอในการใช้งาน CPython


87

หากต้องการเพิ่มคำตอบที่ยอดเยี่ยมของ Tim Peters อาร์เรย์จะใช้โปรโตคอลบัฟเฟอร์ในขณะที่รายการไม่มี ซึ่งหมายความว่าหากคุณกำลังเขียนส่วนขยาย C (หรือเทียบเท่าทางศีลธรรมเช่นการเขียนโมดูลCython ) จากนั้นคุณสามารถเข้าถึงและทำงานกับองค์ประกอบของอาร์เรย์ได้เร็วกว่าสิ่งที่ Python สามารถทำได้ สิ่งนี้จะช่วยให้คุณปรับปรุงความเร็วได้ดีพอสมควร อย่างไรก็ตามมีข้อเสียหลายประการ:

  1. ตอนนี้คุณอยู่ในธุรกิจการเขียน C แทนที่จะเป็น Python Cython เป็นวิธีหนึ่งในการแก้ไขปัญหานี้ แต่ก็ไม่ได้ขจัดความแตกต่างพื้นฐานมากมายระหว่างภาษา คุณจำเป็นต้องคุ้นเคยกับซีแมนทิกส์ C และเข้าใจสิ่งที่กำลังทำอยู่
  2. C API ของ PyPy ทำงานได้ในระดับหนึ่ง แต่ไม่เร็วมาก หากคุณกำหนดเป้าหมายไปที่ PyPy คุณควรจะเขียนโค้ดง่ายๆด้วยรายการปกติแล้วปล่อยให้ JITter ปรับให้เหมาะสมสำหรับคุณ
  3. ส่วนขยาย C นั้นยากที่จะแจกจ่ายได้มากกว่ารหัส Python ที่บริสุทธิ์เพราะจะต้องทำการรวบรวม การรวบรวมมีแนวโน้มที่จะขึ้นอยู่กับสถาปัตยกรรมและระบบปฏิบัติการดังนั้นคุณจะต้องแน่ใจว่าคุณกำลังรวบรวมสำหรับแพลตฟอร์มเป้าหมายของคุณ

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


10

ทิมปีเตอร์สตอบว่าทำไมนี้จะช้า แต่เรามาดูวิธีการปรับปรุงมัน

ติดกับตัวอย่างของคุณsum(range(...))(ตัวคูณ 10 มีขนาดเล็กกว่าตัวอย่างของคุณเพื่อให้พอดีกับหน่วยความจำที่นี่):

import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)

%timeit sum(L)
10 loops, best of 3: 101 ms per loop

%timeit sum(A)
1 loop, best of 3: 237 ms per loop

%timeit sum(N)
1 loop, best of 3: 743 ms per loop

วิธีนี้ยังมีความจำเป็นต้องใช้กล่อง / กล่องซึ่งมีค่าใช้จ่ายเพิ่มเติม ในการทำให้มันรวดเร็วคุณต้องอยู่ในโค้ด numpy:

%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop

ดังนั้นจากรายการโซลูชันถึงเวอร์ชัน numpy นี่เป็นปัจจัย 16 ในการใช้งานจริง

ลองตรวจสอบดูว่าการสร้างโครงสร้างข้อมูลเหล่านั้นใช้เวลานานแค่ไหน

%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop

%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop

%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop

%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop

ผู้ชนะที่ชัดเจน: Numpy

นอกจากนี้โปรดทราบว่าการสร้างโครงสร้างข้อมูลนั้นต้องใช้เวลามากพอ ๆ กับการรวมถ้าไม่มาก การจัดสรรหน่วยความจำช้า

การใช้งานหน่วยความจำของเหล่านั้น:

sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096

ดังนั้นมันใช้ 8 ไบต์ต่อตัวเลข สำหรับช่วงที่เราใช้ 32 บิต ints นั้นเพียงพอดังนั้นเราจึงสามารถรักษาความปลอดภัยของหน่วยความจำได้

N=numpy.arange(10**7, dtype=numpy.int32)

sys.getsizeof(N)
40000096

%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop

แต่ปรากฎว่าการเพิ่ม 64 บิต int นั้นเร็วกว่า 32 บิต ints ในเครื่องของฉันดังนั้นนี่จะคุ้มค่าถ้าคุณถูก จำกัด โดยหน่วยความจำ / แบนด์วิดท์


-1

โปรดทราบว่าไม่100000000เท่ากับและผลลัพธ์ของฉันเป็นเหมือน folowwing:10^810^7

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.