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