6.19. ประสิทธิภาพ#

การตัดสินใจออกแบบที่ทำให้ numpy ทำงานเร็วบน camera -- การเรียกใช้ไลบรารีแบบ whole-array, บัฟเฟอร์แบบ packed typed, และ view ที่แบ่งปันข้อมูลกับแหล่งที่มา -- ยังเปิดเผยชุดนิสัยที่ควรทราบ หน้า Shape และ stride ได้อธิบายกฎเกณฑ์ layout ตามแกนสุดท้ายไปแล้ว หน้านี้จะรวบรวมนิสัยการจัดสรรและ dtype ที่สำคัญที่สุดในลูปสตรีมมิง

6.19.1. เลือก dtype ที่เหมาะสม#

dtype เริ่มต้นของทุก constructor คือ float สำหรับข้อมูลที่เป็น 8 บิตหรือ 16 บิตโดยธรรมชาติ -- ตัวอย่าง ADC, พิกเซลของภาพ, ค่าอ่านจาก sensor -- ให้ระบุ dtype= อย่างชัดเจนเป็นหนึ่งในประเภทจำนวนเต็ม:

adc = np.array(adc_samples, dtype=np.uint16)

การประหยัด RAM คือ 2 เท่าสำหรับ uint16 และ 4 เท่าสำหรับ uint8 เมื่อเทียบกับค่าเริ่มต้น float 4 ไบต์ การคำนวณยังทำงานเร็วขึ้นด้วยเพราะ code path สำหรับจำนวนเต็มภายใน numpy กระชับกว่า code path สำหรับ float ทั่วไป กฎ integer overflow ที่อธิบายไว้ใน Dtypes ยังคงใช้งานได้ -- แปลงเป็นชนิดที่กว้างกว่าก่อนการคำนวณที่อาจ overflow

6.19.2. ให้ใช้ ndarray แทน iterable#

ฟังก์ชัน reduction และ universal function ส่วนใหญ่รับได้ทั้ง iterable และ ndarray

np.sum([1, 2, 3, 4, 5])               # works, but slow
np.sum(np.array([1, 2, 3, 4, 5]))     # ~3x faster

รูปแบบ iterable จะบังคับให้ numpy วนซ้ำผ่าน input ทีละ Python object โดยแปลงแต่ละรายการเป็นตัวเลขก่อนที่จะใช้งาน เมื่อใช้กับ ndarray การแปลงทำเสร็จแล้วและการเรียกทำงานตรงผ่าน packed buffer เลย

เมื่อข้อมูลเดิมถูกใช้มากกว่าหนึ่งครั้ง ให้สร้าง ndarray ครั้งเดียวแล้วส่งต่อ เมื่อข้อมูลมีอยู่เพียงใน Python list และถูกใช้เพียงครั้งเดียว ต้นทุนการแปลงอาจมากกว่าความเร็วที่ได้ -- constructor array() เองต้องวนผ่าน list และจัดสรรหน่วยความจำ

6.19.3. ให้ใช้ view แทน copy#

การ slice, การ index แกนเดียวของ array rank สูงกว่า, reshape(), transpose(), และ frombuffer() ล้วนคืนค่า view ที่แบ่งปันข้อมูลกับแหล่งที่มา ต้นทุนของสิ่งเหล่านี้แทบเป็นศูนย์

copy(), flatten(), boolean indexing (a[mask]), และนิพจน์คณิตศาสตร์ทุกประเภทจะจัดสรร copy ให้ใช้เมื่อต้องการ buffer อิสระอย่างแท้จริงเท่านั้น

เมื่อไม่แน่ใจ ndinfo() จะพิมพ์ตำแหน่งของ buffer พื้นฐาน โดย array สองตัวที่รายงานที่อยู่เดียวกันจะแบ่งปันข้อมูลร่วมกัน ตาราง view vs. copy ฉบับสมบูรณ์อยู่ใน View และ copy

6.19.4. จัดสรรครั้งเดียว แล้วเขียนทับ#

ปัญหาประสิทธิภาพที่ใหญ่ที่สุดบน camera คือการจัดสรร array ใหม่ภายในลูปที่ทำงานหลายครั้งต่อวินาที ndarray ใหม่แต่ละตัวขอ RAM จาก cam และการจัดสรรใหม่บ่อยๆ จะทำให้หน่วยความจำสูญเปล่า

universal function ส่วนใหญ่รับ out= เพื่อให้ผลลัพธ์สามารถเขียนลงใน array ที่มีอยู่แล้ว:

x = np.linspace(0, 2 * np.pi, num=512)
y = np.zeros(512)        # allocate once

while True:
    np.sin(x, out=y)
    # use y ...

image.Image.to_ndarray() รับ buffer= ด้วยเหตุผลเดียวกัน; spectrogram() และตัวแปลงรูปแบบ from_int32_buffer() รับทั้ง out= และ scratchpad= ให้จัดสรรทุกอย่างครั้งเดียวและนำกลับมาใช้ใหม่

6.19.5. ใช้ตัวดำเนินการ in-place#

b = b + 1 จัดสรรค่าชั่วคราวขนาดเท่ากับ b, คัดลอก, และกำหนดค่าใหม่ b += 1 แก้ไข b โดยตรง:

# makes a temporary
b = b + 1

# no temporary
b += 1

แนวคิดเดียวกันนี้ใช้กับนิพจน์ผสม a + b * c จัดสรรค่าชั่วคราวสำหรับ b * c การแยกนิพจน์ออกเป็น sub-assignment ง่ายๆ ที่เขียนลงใน buffer ที่จัดสรรล่วงหน้าจะช่วยลดค่าชั่วคราว:

# one temporary for (a + b), another for the ``* 2``
out = (a + b) * 2

# zero temporaries
out[:]  = a
out    += b
out    *= 2

6.19.6. สร้างผลลัพธ์โดยตรง อย่า append ต่อท้าย#

ndarray ไม่มี append -- โดยตั้งใจ การขยาย array จะต้องจัดสรร buffer ใหม่ที่ใหญ่กว่าและคัดลอกเนื้อหาเดิมลงไป บน microcontroller ให้จัดสรรขนาดสุดท้ายล่วงหน้าแล้ว เติม ลงไป:

out = np.zeros(N, dtype=np.float)
for i in range(N):
    out[i] = some_calculation(i)

เมื่อ N ไม่ทราบล่วงหน้าจริงๆ ให้เขียนลงใน Python list แล้วแปลงครั้งเดียวตอนท้ายด้วย array()

6.19.7. การกำหนดค่าผ่าน slice แทนการสร้าง array ใหม่#

รูปแบบ "สร้าง array ใหม่จากชิ้นส่วนของ array อื่น" หลายๆ รูปแบบสามารถแสดงเป็นการกำหนดค่าผ่าน slice ลงใน buffer ที่จัดสรรล่วงหน้า แทนที่จะจัดสรรใหม่ทุกครั้งที่เรียก

หน้าต่างเลื่อนบนสตรีมของตัวอย่าง -- พื้นฐานของ moving-average filter -- คือกรณีตัวอย่างหลัก buffer เก็บตัวอย่าง N ตัวล่าสุด; แต่ละรอบจะทิ้งตัวเก่าที่สุดและเพิ่มตัวใหม่ล่าสุด รูปแบบที่ชัดเจนสร้าง buffer ใหม่ทุกรอบ:

while True:
    sample = read_sample()
    buf = np.concatenate((buf[1:],              # new buffer every loop
                          np.array([sample])))
    avg = np.mean(buf)

นั่นคือการจัดสรรใหม่ -- และการคัดลอก N - 1 elements -- ต่อหนึ่งตัวอย่าง รูปแบบการกำหนดค่าผ่าน slice เลื่อนในที่เดิม:

N   = 16
buf = np.zeros(N, dtype=np.float)               # allocate once

while True:
    sample   = read_sample()
    buf[:-1] = buf[1:]                          # shift left by one
    buf[-1]  = sample                           # append at the end
    avg      = np.mean(buf)

buf[:-1] = buf[1:] คือบรรทัดที่น่าสนใจ: view สองตัวที่ซ้อนทับกันใน buffer เดียวกัน slice ด้านขวามืออ่านจากปลายหนึ่งและเขียนไปอีกปลาย numpy เดินผ่านหน่วยความจำพื้นฐานในลำดับที่ทำให้การเลื่อน in-place ปลอดภัย ไม่มีการจัดสรร array ใหม่ภายในลูปเลย

6.19.8. ระวัง boolean mask ในลูปสตรีมมิง#

Boolean indexing และ where() สร้าง array ใหม่ทุกครั้งที่เรียก -- ขนาดของผลลัพธ์ขึ้นอยู่กับข้อมูล ดังนั้น buffer ที่จัดสรรล่วงหน้าไม่สามารถรองรับการจัดสรรนี้ได้ การสร้าง mask ซ้ำๆ ในลูปสตรีมมิงจะเติม RAM ด้วย array ที่ใช้แล้วทิ้ง การเรียก gc.collect() เป็นระยะจะคืนพื้นที่:

import gc

for i in range(1000):
    mask = a < threshold
    _    = a[mask]
    if i % 100 == 0:
        gc.collect()

ข้อควรระวังเดียวกันนี้ใช้กับนิพจน์ boolean ผสม เช่น (a > lo) & (a < hi) -- แต่ละตัวดำเนินการจัดสรร bool array ใหม่ เมื่อ mask ถูกนำมาใช้ซ้ำ ให้สร้างครั้งเดียวและเก็บไว้:

mask = a < threshold
foo[mask] = 0
bar[mask] = 1