6.19. الأداء

إنّ قرارات التصميم نفسها التي تجعل numpy سريعًا على الكاميرا -- استدعاءات المكتبة على المصفوفة كاملةً، والمخازن المؤقتة المحزومة ذات النوع المحدد، والمشاهد (views) التي تشارك البيانات مع مصدرها -- تكشف أيضًا عن مجموعة من العادات التي يستحق التعرّف عليها. لقد غطّت صفحة الشكل والخطوات (strides) بالفعل قاعدة تخطيط المحور الأخير؛ وهذه الصفحة تُصنّف عادات التخصيص والنوع (dtype) الأكثر أهمية في حلقة دفق.

6.19.1. اختر نوع بيانات (dtype) معقولًا

نوع البيانات (dtype) الافتراضي لكل مُنشئ هو float. أمّا بالنسبة للبيانات التي تكون بطبيعتها 8-بت أو 16-بت -- عينات ADC، أو بكسلات الصورة، أو قراءات المستشعر -- فمرّر dtype= صراحةً إلى أحد الأنواع الصحيحة:

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

إنّ توفير ذاكرة RAM يبلغ الضعف بالنسبة لـ uint16 وأربعة أضعاف بالنسبة لـ uint8 مقارنةً بالقيمة الافتراضية float ذات الأربعة بايتات. كما تعمل العمليات الحسابية بشكل أسرع لأنّ مسارات الشيفرة الخاصة بالأعداد الصحيحة داخل numpy أكثر إحكامًا من المسارات العامة للأعداد العشرية. وتنطبق قاعدة فيضان الأعداد الصحيحة المشروحة في أنواع البيانات (Dtypes) -- حوّل إلى نوع أوسع قبل إجراء عمليات حسابية قد تتسبب في الفيضان.

6.19.2. فضّل ndarray على المتكرِّر (iterable)

تقبل معظم عمليات الاختزال والدوال الكونية إمّا متكرِّرًا (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 على التنقل عبر المدخل كائنًا واحدًا من كائنات Python في كل مرة، محوِّلًا كلًّا منها إلى عدد قبل أن يتمكن من استخدامه. أمّا مع ndarray فيكون التحويل قد تمّ بالفعل ويجري الاستدعاء مباشرةً عبر المخزن المؤقت المحزوم.

عندما تُستخدَم البيانات نفسها أكثر من مرة، أنشئ ndarray مرة واحدة ومرّرها. وعندما توجد البيانات فقط على هيئة قائمة Python وتُستهلك مرة واحدة، فقد تفوق تكلفة التحويل مكسب السرعة -- إذ يتعيّن على المُنشئ array() نفسه أن يتجوّل في القائمة ويخصّص الذاكرة.

6.19.3. فضّل المشاهد (views) على النسخ

إنّ التقطيع (slicing)، وفهرسة المحور الواحد لمصفوفة أعلى رتبة، وreshape()، وtranspose()، وfrombuffer() تُعيد جميعها مشاهد تشارك البيانات مع المصدر. وهي مجانية في جوهرها.

أمّا copy()، وflatten()، والفهرسة المنطقية (a[mask])، وأيّ تعبير حسابي فتخصّص نسخة. لا تلجأ إليها إلّا عندما تكون هناك حاجة فعلية إلى مخزن مؤقت مستقل.

عند الشك، تطبع ndinfo() موقع المخزن المؤقت الأساسي؛ والمصفوفتان اللتان تُبلّغان عن العنوان نفسه تتشاركان بياناتهما. ويوجد الجدول الكامل للمشهد مقابل النسخة في المشاهد والنسخ.

6.19.4. خصّص مرة واحدة، ثم اكتب

إنّ أكبر مزلق منفرد للأداء على الكاميرا هو تخصيص مصفوفات جديدة داخل حلقة تعمل عدة مرات في الثانية. فكل ndarray جديد يطلب من الكاميرا ذاكرة RAM، والتخصيصات الجديدة المتكررة تُهدرها.

تقبل معظم الدوال الكونية out= بحيث يمكن كتابة النتيجة في مصفوفة موجودة بالفعل:

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. وتقسيم التعبير إلى إسنادات فرعية بسيطة تكتب في مخزن مؤقت مخصَّص مسبقًا يلغي المتغيرات المؤقتة:

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

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

6.19.6. ابنِ النتيجة، ولا تُلحِق بها

ليس لدى ndarray أيّ append -- وذلك عن قصد. فإطالة المصفوفة تعني تخصيص مخزن مؤقت جديد أكبر ونسخ المحتويات القديمة إليه. على متحكّم دقيق، خصّص الحجم النهائي مسبقًا ثم املأه

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

عندما لا تكون N معروفة فعلًا مسبقًا، اكتب في list خاصة بـ Python وحوّلها مرة واحدة في النهاية باستخدام array().

6.19.7. إسناد التقطيع بدلًا من المصفوفات الجديدة

يمكن التعبير عن العديد من أنماط "بناء مصفوفة جديدة من أجزاء مصفوفات أخرى" على هيئة إسنادات تقطيع في مخزن مؤقت مخصَّص مسبقًا بدلًا من تخصيص جديد في كل استدعاء.

إنّ النافذة المتدحرجة فوق دفق من العينات -- أساس مرشّح المتوسط المتحرك -- هي الحالة النموذجية. يحتفظ المخزن المؤقت بآخر N عينات؛ وتُسقِط كل تكرارة الأقدمَ وتُلحِق الأحدث. والصيغة البديهية تعيد بناء المخزن المؤقت في كل تكرارة:

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

هذا تخصيص جديد -- ونسخة لـ N - 1 عنصرًا -- لكل عينة. أمّا صيغة إسناد التقطيع فتُزيح في المكان:

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:] هو السطر المثير للاهتمام: مشهدان متداخلان في المخزن المؤقت نفسه، حيث تُقرأ الشريحة اليمنى من أحد الطرفين وتُكتب إلى الآخر. ويتجوّل numpy في الذاكرة الأساسية بالترتيب الذي يجعل الإزاحة في المكان آمنة. ولا تُخصَّص أيّ مصفوفة جديدة داخل الحلقة على الإطلاق.

6.19.8. احذر من الأقنعة المنطقية في حلقات الدفق

تُنتج الفهرسة المنطقية وwhere() مصفوفة جديدة في كل استدعاء -- إذ يعتمد حجم النتيجة على البيانات، فلا يمكن لأيّ مخزن مؤقت مخصَّص مسبقًا أن يستوعب التخصيص. وبناء الأقنعة المتكرر في حلقة دفق يملأ ذاكرة RAM بمصفوفات يُتخلَّص منها. واستدعاء gc.collect() دوريًا يستعيد المساحة:

import gc

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

ينطبق التحذير نفسه على التعبيرات المنطقية المركّبة مثل (a > lo) & (a < hi) -- إذ يخصّص كل مُعامِل مصفوفة منطقية جديدة. وعندما يُعاد استخدام قناع، ابنِه مرة واحدة واحتفظ به:

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