MicroPython على المتحكمات الدقيقة

صُمّمت MicroPython لتكون قادرة على العمل على المتحكمات الدقيقة. وهذه تنطوي على قيود عتادية قد لا تكون مألوفة للمبرمجين المعتادين على الحواسيب التقليدية. وعلى وجه الخصوص فإن كمية ذاكرة RAM وسعة التخزين غير المتطايرة "القرص" (ذاكرة الفلاش) محدودة. يقدّم هذا الدليل التعليمي طرقًا للاستفادة القصوى من الموارد المحدودة. ونظرًا لأن MicroPython تعمل على متحكمات قائمة على مجموعة متنوعة من البُنى، فإن الأساليب المعروضة عامة: ففي بعض الحالات سيكون من الضروري الحصول على معلومات مفصلة من الوثائق الخاصة بالمنصة.

ذاكرة الفلاش

في كاميرات OpenMV Cam تتمثل الطريقة البسيطة لمعالجة السعة المحدودة في تركيب بطاقة micro SD. وفي بعض الحالات يكون هذا غير عملي، إما لأن الجهاز لا يحتوي على فتحة لبطاقة SD أو لأسباب تتعلق بالتكلفة أو استهلاك الطاقة؛ ومن ثم يجب استخدام ذاكرة الفلاش المدمجة على الرقاقة. ويُخزَّن البرنامج الثابت المتضمّن للنظام الفرعي MicroPython في ذاكرة الفلاش المدمجة على اللوحة. وتكون السعة المتبقية متاحة للاستخدام. ولأسباب تتعلق بالبنية الفيزيائية لذاكرة الفلاش قد يكون جزء من هذه السعة غير قابل للوصول كنظام ملفات. وفي مثل هذه الحالات يمكن استغلال هذه المساحة عن طريق دمج وحدات المستخدم ضمن بنية برنامج ثابت يُحمَّل بعد ذلك إلى الجهاز عبر ذاكرة الفلاش.

توجد طريقتان لتحقيق ذلك: الوحدات المجمّدة والبايتكود المجمّد. تخزّن الوحدات المجمّدة شيفرة Python المصدرية مع البرنامج الثابت. أما البايتكود المجمّد فيستخدم المُصرِّف المتقاطع لتحويل الشيفرة المصدرية إلى بايتكود يُخزَّن بعد ذلك مع البرنامج الثابت. وفي كلتا الحالتين يمكن الوصول إلى الوحدة باستخدام عبارة import:

import mymodule

إن إجراء إنتاج الوحدات المجمّدة والبايتكود يعتمد على المنصة؛ ويمكن العثور على تعليمات بناء البرنامج الثابت في ملفات README في الجزء ذي الصلة من شجرة المصدر.

بصفة عامة تكون الخطوات كالتالي:

  • استنساخ مستودع MicroPython.

  • الحصول على سلسلة الأدوات (الخاصة بالمنصة) لبناء البرنامج الثابت.

  • بناء المُصرِّف المتقاطع.

  • وضع الوحدات المراد تجميدها في دليل محدد (يعتمد على ما إذا كانت الوحدة ستُجمَّد كمصدر أم كبايتكود).

  • بناء البرنامج الثابت. قد يلزم أمر محدد لبناء الشيفرة المجمّدة من أي من النوعين - راجع وثائق المنصة.

  • تحميل البرنامج الثابت إلى الجهاز عبر ذاكرة الفلاش.

RAM

عند تقليل استخدام RAM ثمة مرحلتان يجب مراعاتهما: التحويل البرمجي والتنفيذ. فضلًا عن استهلاك الذاكرة، تبرز مشكلة تُعرف بتجزئة الكومة (heap fragmentation). بوجه عام، يُنصح بتقليل الإنشاء والإتلاف المتكرر للكائنات. ويرد تفصيل ذلك في القسم المخصص للـ heap.

مرحلة التصريف

عند استيراد وحدة، تقوم MicroPython بتصريف الشيفرة إلى بايتكود يُنفَّذ بعد ذلك بواسطة الآلة الافتراضية لـ MicroPython (VM). ويُخزَّن البايتكود في ذاكرة RAM. ويتطلب المُصرِّف نفسه ذاكرة RAM، لكن هذه تصبح متاحة للاستخدام عند اكتمال التصريف.

إذا كان عدد من الوحدات قد استُورد بالفعل فقد تنشأ حالة لا توجد فيها ذاكرة RAM كافية لتشغيل المُصرِّف. وفي هذه الحالة ستُنتج عبارة import استثناء ذاكرة.

إذا أنشأت وحدة كائنات عامة عند الاستيراد فإنها ستستهلك ذاكرة RAM وقت الاستيراد، وهي ذاكرة تصبح بعد ذلك غير متاحة للمُصرِّف لاستخدامها في عمليات الاستيراد اللاحقة. وبصفة عامة من الأفضل تجنّب الشيفرة التي تُنفَّذ عند الاستيراد؛ والنهج الأفضل هو وجود شيفرة تهيئة يُشغّلها التطبيق بعد استيراد جميع الوحدات. وهذا يزيد من ذاكرة RAM المتاحة للمُصرِّف إلى أقصى حد.

إذا ظلت ذاكرة RAM غير كافية لتصريف جميع الوحدات فإن أحد الحلول هو التصريف المسبق للوحدات. تمتلك MicroPython مُصرِّفًا متقاطعًا قادرًا على تصريف وحدات Python إلى بايتكود (راجع ملف README في دليل mpy-cross). يحمل ملف البايتكود الناتج الامتداد .mpy؛ ويمكن نسخه إلى نظام الملفات واستيراده بالطريقة المعتادة. وبدلاً من ذلك يمكن تنفيذ بعض الوحدات أو جميعها كبايتكود مجمّد: وعلى معظم المنصات يوفّر هذا قدرًا أكبر من ذاكرة RAM لأن البايتكود يُشغَّل مباشرة من ذاكرة الفلاش بدلاً من تخزينه في ذاكرة RAM.

مرحلة التنفيذ

هناك عدد من تقنيات البرمجة لتقليل استخدام ذاكرة RAM.

الثوابت

توفّر MicroPython كلمة مفتاحية const يمكن استخدامها كما يلي:

from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS

في كلتا الحالتين اللتين يُسنَد فيهما الثابت إلى متغير سيتجنب المُصرِّف ترميز عملية بحث عن اسم الثابت من خلال استبداله بقيمته الحرفية. وهذا يوفّر البايتكود ومن ثم ذاكرة RAM. غير أن القيمة ROWS ستشغل كلمتي آلة على الأقل، واحدة لكل من المفتاح والقيمة في قاموس المتغيرات العامة. ووجودها في القاموس ضروري لأن وحدة أخرى قد تستوردها أو تستخدمها. ويمكن توفير هذه الذاكرة بإضافة شرطة سفلية قبل الاسم كما في _COLS: فهذا الرمز غير مرئي خارج الوحدة ولذلك لن يشغل ذاكرة RAM.

يمكن أن تكون الوسيطة المُمرَّرة إلى const() أي شيء يُقيَّم، وقت التصريف، إلى ثابت مثل 0x100 أو 1 << 8 أو (True, "string", b"bytes") (راجع القسم أدناه للتفاصيل). بل يمكن أن تتضمن رموز const أخرى عُرّفت بالفعل، مثل 1 << BIT.

بنى البيانات الثابتة

عندما يكون هناك حجم كبير من البيانات الثابتة وتدعم المنصة التنفيذ من ذاكرة الفلاش، يمكن توفير ذاكرة RAM كما يلي. ينبغي وضع البيانات في وحدات Python وتجميدها كبايتكود. ويجب تعريف البيانات ككائنات bytes. فالمُصرِّف "يعلم" أن كائنات bytes غير قابلة للتغيير ويضمن بقاء الكائنات في ذاكرة الفلاش بدلاً من نسخها إلى ذاكرة RAM. ويمكن للوحدة struct أن تساعد في التحويل بين أنواع bytes وأنواع Python المدمجة الأخرى.

عند النظر في تبعات البايتكود المجمّد، لاحظ أنه في Python تكون السلاسل النصية والأعداد العشرية والبايتات والأعداد الصحيحة والأعداد المركّبة والصفوف (tuples) غير قابلة للتغيير. وبناءً على ذلك ستُجمَّد هذه في ذاكرة الفلاش (وبالنسبة للصفوف، فقط إذا كانت جميع عناصرها غير قابلة للتغيير). وهكذا، في السطر

mystring = "The quick brown fox"

ستوجد السلسلة النصية الفعلية "The quick brown fox" في ذاكرة الفلاش. وفي وقت التشغيل يُسنَد مرجع إلى السلسلة النصية إلى المتغير mystring. ويشغل المرجع كلمة آلة واحدة. ومن حيث المبدأ يمكن استخدام عدد صحيح طويل لتخزين بيانات ثابتة:

bar = 0xDEADBEEF0000DEADBEEF

كما في مثال السلسلة النصية، يُسنَد في وقت التشغيل مرجع إلى العدد الصحيح الكبير اعتباطيًا إلى المتغير bar. ويشغل ذلك المرجع كلمة آلة واحدة.

إن صفوف الكائنات الثابتة هي نفسها ثابتة. ويُحسِّن المُصرِّف مثل هذه الصفوف الثابتة بحيث لا تحتاج إلى الإنشاء في وقت التشغيل في كل مرة تُستخدَم فيها. على سبيل المثال:

foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))

سيوجد هذا الصف بأكمله ككائن واحد (ربما في ذاكرة الفلاش إذا كانت الشيفرة مجمّدة) ويُشار إليه في كل مرة يُحتاج فيها إليه.

إنشاء الكائنات غير الضروري

هناك عدد من المواقف التي قد تُنشأ فيها الكائنات وتُتلَف دون قصد. وهذا قد يقلل من قابلية استخدام ذاكرة RAM بسبب التجزئة. وتناقش الأقسام التالية أمثلة على ذلك.

سَلسَلة السلاسل النصية

تأمّل مقاطع الشيفرة التالية التي تهدف إلى إنتاج سلاسل نصية ثابتة:

var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""

كل منها ينتج النتيجة نفسها، غير أن الأول يُنشئ دون داعٍ كائنَي سلسلة نصية في وقت التشغيل، ويخصص مزيدًا من ذاكرة RAM للسَلسَلة قبل إنتاج الثالث. أما الآخران فيُجريان السَلسَلة في وقت التصريف وهو أكثر كفاءة، مما يقلل التجزئة.

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

أفضل طريقة لإنشاء سلاسل نصية ديناميكية هي عن طريق التابع format() للسلسلة النصية:

var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)

المخازن المؤقتة

عند الوصول إلى أجهزة مثل نسخ من واجهات UART وI2C وSPI، فإن استخدام مخازن مؤقتة مخصصة مسبقًا يتجنب إنشاء كائنات غير ضرورية. تأمّل هاتين الحلقتين:

while True:
    var = spi.read(100)
    # process data

buf = bytearray(100)
while True:
    spi.readinto(buf)
    # process data in buf

تُنشئ الأولى مخزنًا مؤقتًا في كل مرور بينما تعيد الثانية استخدام مخزن مؤقت مخصص مسبقًا؛ وهذا أسرع وأكثر كفاءة من حيث تجزئة الذاكرة على حد سواء.

البايتات أصغر من الأعداد الصحيحة

على معظم المنصات يستهلك العدد الصحيح أربعة بايتات. تأمّل الاستدعاءات الثلاثة للدالة foo():

def foo(bar):
    for x in bar:
        print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')

في الاستدعاء الأول تُنشأ list من الأعداد الصحيحة في ذاكرة RAM في كل مرة تُنفَّذ فيها الشيفرة. ويُنشئ الاستدعاء الثاني كائن tuple ثابت (tuple يحتوي على كائنات ثابتة فقط) كجزء من مرحلة التصريف، لذا يُنشأ مرة واحدة فقط وهو أكثر كفاءة من list. ويُنشئ الاستدعاء الثالث بكفاءة كائن bytes يستهلك الحد الأدنى من ذاكرة RAM. وإذا جُمِّدت الوحدة كبايتكود، فإن كلًّا من كائن tuple وكائن bytes سيوجد في ذاكرة الفلاش.

السلاسل النصية مقابل البايتات

أدخلت Python3 دعم Unicode. وقد أدخل هذا تمييزًا بين السلسلة النصية ومصفوفة البايتات. وتضمن MicroPython ألا تشغل سلاسل Unicode النصية مساحة إضافية طالما أن جميع الأحرف في السلسلة من نوع ASCII (أي لها قيمة < 128). وإذا كانت القيم في النطاق الكامل المؤلف من 8 بتات مطلوبة فيمكن استخدام كائنات bytes وbytearray لضمان عدم الحاجة إلى أي مساحة إضافية. لاحظ أن معظم توابع السلسلة النصية (مثل str.strip()) تنطبق أيضًا على نسخ bytes لذا فإن عملية التخلص من Unicode يمكن أن تكون سهلة.

s = 'the quick brown fox'   # A string instance
b = b'the quick brown fox'  # A bytes instance

عندما يكون من الضروري التحويل بين السلاسل النصية والبايتات يمكن استخدام التابعين str.encode() وbytes.decode(). لاحظ أن كلًّا من السلاسل النصية والبايتات غير قابلة للتغيير. وأي عملية تأخذ كائنًا كهذا كمدخل وتُنتج آخر تستلزم تخصيص ذاكرة RAM واحدًا على الأقل لإنتاج النتيجة. في السطر الثاني أدناه يُخصَّص كائن bytes جديد. وكان هذا سيحدث أيضًا لو كان foo سلسلة نصية.

foo = b'   empty whitespace'
foo = foo.lstrip()

تنفيذ المُصرِّف في وقت التشغيل

تستدعي دالتا Python eval وexec المُصرِّف في وقت التشغيل، وهو ما يتطلب قدرًا كبيرًا من ذاكرة RAM. لاحظ أن مكتبة pickle من micropython-lib تستخدم exec. وقد يكون استخدام مكتبة json لتسلسل الكائنات أكثر كفاءة من حيث ذاكرة RAM.

تخزين السلاسل النصية في ذاكرة الفلاش

السلاسل النصية في Python غير قابلة للتغيير ومن ثم لديها إمكانية أن تُخزَّن في ذاكرة للقراءة فقط. ويمكن للمُصرِّف وضع السلاسل النصية المعرَّفة في شيفرة Python في ذاكرة الفلاش. وكما هو الحال مع الوحدات المجمّدة من الضروري امتلاك نسخة من شجرة المصدر على الحاسوب وسلسلة الأدوات لبناء البرنامج الثابت. وسيعمل الإجراء حتى لو لم يتم تصحيح أخطاء الوحدات بالكامل، طالما يمكن استيرادها وتشغيلها.

بعد استيراد الوحدات، نفّذ:

micropython.qstr_info(1)

ثم انسخ والصق جميع أسطر Q(xxx) في محرر نصوص. تحقق من الأسطر غير الصالحة بشكل واضح واحذفها. افتح الملف qstrdefsport.h الذي ستجده في ports/stm32 (أو الدليل المكافئ للبنية المستخدمة). انسخ والصق الأسطر المصححة في نهاية الملف. احفظ الملف، وأعد البناء وحمّل البرنامج الثابت عبر ذاكرة الفلاش. ويمكن التحقق من النتيجة باستيراد الوحدات وإصدار الأمر مرة أخرى:

micropython.qstr_info(1)

ينبغي أن تكون أسطر Q(xxx) قد اختفت.

الكومة

عندما يُنشئ برنامج قيد التشغيل كائنًا تُخصَّص ذاكرة RAM اللازمة من مجمّع ثابت الحجم يُعرف بالكومة (heap). وعندما يخرج الكائن عن النطاق (بعبارة أخرى يصبح غير قابل للوصول من الشيفرة) يُعرف الكائن الزائد بـ "النفايات" (garbage). وتقوم عملية تُعرف بـ "جمع النفايات" (GC) باستعادة تلك الذاكرة، وإعادتها إلى الكومة الحرة. وتعمل هذه العملية تلقائيًا، غير أنه يمكن استدعاؤها مباشرة بإصدار gc.collect().

إن النقاش حول هذا الموضوع معقّد إلى حد ما. وللحصول على "حل سريع" أصدر ما يلي بشكل دوري:

gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())

لمزيد من المعلومات، راجع أدناه ووثائق الوحدة المدمجة gc.

للحصول على التفاصيل من منظور البنية الداخلية لـ MicroPython/المطوّر، راجع أيضًا إدارة الذاكرة.

التجزئة

لنفترض أن برنامجًا يُنشئ كائنًا foo، ثم كائنًا bar. وبعد ذلك يخرج foo عن النطاق بينما يبقى bar. ستُستعاد ذاكرة RAM التي استخدمها foo بواسطة جمع النفايات. غير أنه إذا كان bar قد خُصِّص لعنوان أعلى، فإن ذاكرة RAM المُستعادة من foo لن تكون مفيدة إلا لكائنات لا تزيد عن حجم foo. وفي برنامج معقد أو طويل التشغيل يمكن أن تصبح الكومة مجزّأة: فعلى الرغم من وجود قدر كبير من ذاكرة RAM المتاحة، لا توجد مساحة متجاورة كافية لتخصيص كائن معيّن، ويفشل البرنامج بخطأ ذاكرة.

تهدف التقنيات الموضحة أعلاه إلى التقليل من هذا. وعندما تكون هناك حاجة إلى مخازن مؤقتة دائمة كبيرة أو كائنات أخرى فمن الأفضل إنشاؤها مبكرًا في عملية تنفيذ البرنامج قبل أن تحدث التجزئة. ويمكن إجراء مزيد من التحسينات عن طريق مراقبة حالة الكومة والتحكم في جمع النفايات؛ وهذه موضحة أدناه.

إعداد التقارير

يتوفر عدد من دوال المكتبة للإبلاغ عن تخصيص الذاكرة والتحكم في جمع النفايات. وتوجد هذه في الوحدتين gc وmicropython. ويمكن لصق المثال التالي في REPL (Ctrl-E للدخول إلى وضع اللصق، وCtrl-D لتشغيله).

import gc
import micropython
gc.collect()
micropython.mem_info()
print('-----------------------------')
print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
def func():
    a = bytearray(10000)
gc.collect()
print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
func()
print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
gc.collect()
print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
print('-----------------------------')
micropython.mem_info(1)

التوابع المستخدمة أعلاه:

  • gc.collect() فرض جمع النفايات. راجع الحاشية.

  • micropython.mem_info() طباعة ملخص لاستخدام ذاكرة RAM.

  • gc.mem_free() إرجاع حجم الكومة الحرة بالبايتات.

  • gc.mem_alloc() إرجاع عدد البايتات المخصصة حاليًا.

  • micropython.mem_info(1) طباعة جدول باستخدام الكومة (مفصّل أدناه).

تعتمد الأرقام المُنتَجة على المنصة، لكن يمكن ملاحظة أن تعريف الدالة يستخدم قدرًا صغيرًا من ذاكرة RAM في صورة بايتكود يصدره المُصرِّف (وقد استُعيدت ذاكرة RAM التي استخدمها المُصرِّف). ويستخدم تشغيل الدالة أكثر من 10 كيبي بايت، لكن عند العودة يصبح a نفايات لأنه خارج النطاق ولا يمكن الإشارة إليه. ويستعيد gc.collect() النهائي تلك الذاكرة.

ستختلف المخرجات النهائية التي يُنتجها micropython.mem_info(1) في التفاصيل لكن يمكن تفسيرها كما يلي:

الرمز

المعنى

.

كتلة حرة

h

كتلة رأس

=

كتلة ذيل

m

كتلة رأس مُعلَّمة

T

صف (tuple)

L

قائمة

D

قاموس

F

عدد عشري

B

بايتكود

M

وحدة

S

سلسلة نصية أو بايتات

A

مصفوفة بايتات

يمثّل كل حرف كتلة ذاكرة واحدة، حيث تساوي الكتلة 16 بايتًا. لذا يمثّل كل سطر من تفريغ الكومة 0x400 بايت أو 1 كيبي بايت من ذاكرة RAM.

التحكم في جمع النفايات

يمكن طلب جمع النفايات في أي وقت بإصدار gc.collect(). ومن المفيد القيام بذلك على فترات، أولاً لاستباق التجزئة وثانيًا للأداء. وقد يستغرق جمع النفايات عدة ميلي ثانية لكنه أسرع عندما يكون هناك قليل من العمل المطلوب (نحو 1 ميلي ثانية على OpenMV Cam). ويمكن للاستدعاء الصريح أن يقلل ذلك التأخير مع ضمان حدوثه عند نقاط في البرنامج يكون فيها مقبولاً.

يُستثار جمع النفايات التلقائي في الظروف التالية. عندما تفشل محاولة تخصيص، يُجرى جمع النفايات وتُعاد محاولة التخصيص. ولا يُطرح استثناء إلا إذا فشل هذا. وثانيًا سيُطلَق جمع نفايات تلقائي إذا انخفضت كمية ذاكرة RAM الحرة دون عتبة معينة. ويمكن تكييف هذه العتبة مع تقدم التنفيذ:

gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())

سيستثير هذا جمع النفايات عندما تصبح أكثر من 25% من الكومة الحرة حاليًا مشغولة.

بصفة عامة ينبغي للوحدات أن تُنشئ كائنات البيانات في وقت التشغيل باستخدام المُنشئات أو دوال التهيئة الأخرى. والسبب هو أنه إذا حدث هذا عند التهيئة فقد يُحرَم المُصرِّف من ذاكرة RAM عند استيراد الوحدات اللاحقة. وإذا أنشأت الوحدات بيانات عند الاستيراد فإن gc.collect() الذي يُصدَر بعد الاستيراد سيخفف من المشكلة.

عمليات السلاسل النصية

تتعامل MicroPython مع السلاسل النصية بطريقة فعّالة وفهم هذا يمكن أن يساعد في تصميم تطبيقات تعمل على المتحكمات الدقيقة. فعند تصريف وحدة، تُخزَّن السلاسل النصية التي تتكرر مرات عديدة مرة واحدة فقط، وهي عملية تُعرف بإدراج السلاسل النصية (string interning). وفي MicroPython تُعرف السلسلة النصية المُدرَجة بـ qstr. وفي وحدة مستوردة بشكل عادي ستوجد تلك النسخة الوحيدة في ذاكرة RAM، لكن كما هو موضح أعلاه، في الوحدات المجمّدة كبايتكود ستوجد في ذاكرة الفلاش.

تُجرى مقارنات السلاسل النصية أيضًا بكفاءة باستخدام التجزئة (hashing) بدلاً من المقارنة حرفًا بحرف. ومن ثم قد تكون عقوبة استخدام السلاسل النصية بدلاً من الأعداد الصحيحة صغيرة من حيث الأداء واستخدام ذاكرة RAM على حد سواء - وهي حقيقة قد تأتي كمفاجأة لمبرمجي لغة C.

خاتمة

تمرّر MicroPython الكائنات وتُرجعها و(افتراضيًا) تنسخها بالمرجع. ويشغل المرجع كلمة آلة واحدة لذا فإن هذه العمليات فعّالة من حيث استخدام ذاكرة RAM والسرعة.

عندما تكون هناك حاجة إلى متغيرات لا يكون حجمها بايتًا ولا كلمة آلة فهناك مكتبات قياسية يمكنها المساعدة في تخزينها بكفاءة وفي إجراء التحويلات. راجع الوحدات array وstruct وuctypes.

حاشية: القيمة المُرجَعة من gc.collect()

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