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