MicroPython hızını en üst düzeye çıkarmak¶
Bu öğretici, MicroPython kodunun performansını artırmanın yollarını açıklar. Diğer dilleri içeren optimizasyonlar başka yerlerde ele alınmıştır; özellikle C ile yazılmış modüllerin kullanımı ve MicroPython satır içi assembler.
Yüksek performanslı kod geliştirme süreci, listelenen sırayla gerçekleştirilmesi gereken aşağıdaki aşamalardan oluşur.
Hız için tasarlayın.
Kodlayın ve hata ayıklayın.
Optimizasyon adımları:
Kodun en yavaş bölümünü belirleyin.
Python kodunun verimliliğini artırın.
Yerel (native) kod yayıcısını kullanın.
Viper kod yayıcısını kullanın.
Donanıma özgü optimizasyonları kullanın.
Hız için tasarlama¶
Performans sorunları en baştan dikkate alınmalıdır. Bu, kodun performans açısından en kritik bölümleri hakkında bir görüş oluşturmayı ve bunların tasarımına özel önem vermeyi içerir. Optimizasyon süreci kod test edildiğinde başlar: tasarım en baştan doğruysa optimizasyon basit olacak ve hatta aslında gereksiz olabilir.
Algoritmalar¶
Herhangi bir rutini performans için tasarlamanın en önemli yönü, en iyi algoritmanın kullanılmasını sağlamaktır. Bu, bir MicroPython kılavuzundan ziyade ders kitaplarına ait bir konudur, ancak verimlilikleriyle bilinen algoritmalar benimsenerek bazen göz alıcı performans kazanımları elde edilebilir.
RAM tahsisi¶
Verimli MicroPython kodu tasarlamak için, yorumlayıcının RAM tahsis etme şeklini anlamak gerekir. Bir nesne oluşturulduğunda veya boyutu büyüdüğünde (örneğin bir öğenin listeye eklenmesi durumunda) gerekli RAM, yığın (heap) olarak bilinen bir bloktan tahsis edilir. Bu önemli miktarda zaman alır; ayrıca zaman zaman birkaç milisaniye sürebilen çöp toplama (garbage collection) olarak bilinen bir süreci tetikler.
Sonuç olarak, bir nesne yalnızca bir kez oluşturulursa ve boyutunun büyümesine izin verilmezse bir fonksiyonun veya metodun performansı artırılabilir. Bu, nesnenin kullanımı süresince var olduğu anlamına gelir: genellikle bir sınıf yapıcısında örneklenir ve çeşitli metotlarda kullanılır.
Bu, aşağıdaki Çöp toplamayı denetleme bölümünde daha ayrıntılı olarak ele alınmıştır.
Arabellekler¶
Yukarıdakine bir örnek, bir cihazla iletişimde kullanılan gibi bir arabelleğin gerekli olduğu yaygın durumdur. Tipik bir sürücü, arabelleği yapıcıda oluşturur ve tekrar tekrar çağrılacak G/Ç metotlarında kullanır.
MicroPython kütüphaneleri genellikle önceden tahsis edilmiş arabellekler için destek sağlar. Örneğin, akış arabirimini destekleyen nesneler (örn. dosya veya UART), okunan veriler için yeni bir arabellek tahsis eden read() metodunu, ayrıca verileri mevcut bir arabelleğe okumak için bir readinto() metodunu sağlar.
Yeniden kullanılabilir arabellek nesneleri oluşturmak için bazı yararlı sınıflar:
Kayan nokta¶
Bazı MicroPython portları kayan nokta sayılarını yığın (heap) üzerinde tahsis eder. Diğer bazı portlarda özel bir kayan nokta yardımcı işlemcisi bulunmayabilir ve aritmetik işlemleri “yazılımda”, tam sayılara göre oldukça daha düşük hızda gerçekleştirir. Performansın önemli olduğu yerlerde, tam sayı işlemlerini kullanın ve kayan nokta kullanımını performansın çok önemli olmadığı kod bölümleriyle sınırlayın. Örneğin, ADC okumalarını hızlı bir şekilde tek seferde bir diziye tam sayı değerleri olarak yakalayın ve ancak ondan sonra sinyal işleme için bunları kayan nokta sayılarına dönüştürün.
Diziler¶
Listelere bir alternatif olarak çeşitli dizi sınıfı türlerinin kullanımını değerlendirin. array modülü, 8 bitlik öğelerin Python’un yerleşik bytes ve bytearray sınıfları tarafından desteklendiği çeşitli öğe türlerini destekler. Bu veri yapılarının tümü öğeleri bitişik bellek konumlarında saklar. Kritik kodda bellek tahsisinden kaçınmak için, bunlar yeniden önceden tahsis edilmeli ve argüman olarak ya da bağlı nesneler olarak iletilmelidir.
Memoryview’ler¶
bytearray örnekleri gibi nesnelerin dilimlerini iletirken, Python dilimin boyutuyla orantılı bir boyutta tahsis içeren bir kopya oluşturur. Bu, bir memoryview nesnesi kullanılarak hafifletilebilir. memoryview kendisi yığın üzerinde tahsis edilir, ancak işaret ettiği dilimin boyutundan bağımsız olarak küçük, sabit boyutlu bir nesnedir. Bir memoryview dilimlemek yeni bir memoryview oluşturur, bu nedenle bu bir kesme hizmet rutininde yapılamaz. Ayrıca, a:b dilim sözdizimi bir slice(a, b) nesnesi örnekleyerek ek tahsise neden olur.
ba = bytearray(10000) # big array
func(ba[30:2000]) # a copy is passed, ~2K new allocation
mv = memoryview(ba) # small object is allocated
func(mv[30:2000]) # a pointer to memory is passed
Bir memoryview yalnızca arabellek protokolünü destekleyen nesnelere uygulanabilir - bu, dizileri içerir ancak listeleri içermez. Küçük bir uyarı, memoryview nesnesi canlıyken orijinal arabellek nesnesini de canlı tutmasıdır. Yani, bir memoryview evrensel bir çare değildir. Örneğin, yukarıdaki örnekte, 10K’lık arabellekle işiniz bittiyse ve yalnızca ondan 30:2000 baytlarına ihtiyacınız varsa, uzun ömürlü bir memoryview oluşturup 10K’yı GC için bloke tutmak yerine bir dilim oluşturmak ve 10K’lık arabelleği serbest bırakmak (çöp toplamaya hazır olmak) daha iyi olabilir.
Yine de, memoryview gelişmiş önceden tahsis edilmiş arabellek yönetimi için vazgeçilmezdir. Yukarıda tartışılan readinto() metodu verileri arabelleğin başına koyar ve tüm arabelleği doldurur. Verileri mevcut bir arabelleğin ortasına koymanız gerekirse ne olur? Arabelleğin gereken bölümüne bir memoryview oluşturun ve onu readinto() metoduna iletin.
String’ler ve Bytes karşılaştırması¶
MicroPython, birden çok özdeş string olduğunda yer kazanmak için string interning kullanır. Çalışma zamanında her yeni string tahsis edildiğinde (örneğin, iki diğer string birleştirildiğinde), MicroPython RAM tasarrufu için yeni string’in internlenebilir olup olmadığını kontrol eder.
Performans açısından kritik string işlemleri gerçekleştiren bir kodunuz varsa, bytes nesnelerini ve sabit değerlerini (yani b"abc") kullanmayı düşünün. Bu, interning kontrolünü atlar ve aynı işlemleri string nesneleriyle gerçekleştirmekten birkaç kat daha hızlı olabilir.
Not
En hızlı performans, örneğin yukarıda açıklanan bir arabellek ile, yeni nesne oluşturmaktan tamamen kaçınılarak her zaman elde edilir.
Kodun en yavaş bölümünü belirleme¶
Bu, profil çıkarma (profiling) olarak bilinen bir süreçtir ve ders kitaplarında ele alınır ve (standart Python için) çeşitli yazılım araçları tarafından desteklenir. MicroPython platformlarında çalışması muhtemel daha küçük gömülü uygulama türleri için, en yavaş fonksiyon veya metot genellikle time içinde belgelenen zamanlama ticks fonksiyon grubunun akıllıca kullanımıyla belirlenebilir. Kod yürütme süresi ms, us veya CPU döngüleri cinsinden ölçülebilir.
Aşağıdaki, bir @timed_function dekoratörü eklenerek herhangi bir fonksiyonun veya metodun zamanlanmasını sağlar:
def timed_function(f, *args, **kwargs):
myname = str(f).split(' ')[1]
def new_func(*args, **kwargs):
t = time.ticks_us()
result = f(*args, **kwargs)
delta = time.ticks_diff(time.ticks_us(), t)
print('Function {} Time = {:6.3f}ms'.format(myname, delta/1000))
return result
return new_func
MicroPython kod iyileştirmeleri¶
const() bildirimi¶
MicroPython bir const() bildirimi sağlar. Bu, C’deki #define ile benzer şekilde çalışır; öyle ki kod bayt koduna derlendiğinde derleyici tanımlayıcı yerine sayısal değeri koyar. Bu, çalışma zamanında bir sözlük aramasından kaçınır. const() argümanı, derleme zamanında bir tam sayıya değerlenen herhangi bir şey olabilir, örn. 0x100 veya 1 << 8.
Nesne referanslarını önbelleğe alma¶
Bir fonksiyon veya metot nesnelere tekrar tekrar eriştiğinde, nesne yerel bir değişkende önbelleğe alınarak performans artırılır:
class foo(object):
def __init__(self):
self.ba = bytearray(100)
def bar(self, obj_display):
ba_ref = self.ba
fb = obj_display.framebuffer
# iterative code using these two objects
Bu, bar() metodunun gövdesinde self.ba ve obj_display.framebuffer öğelerini tekrar tekrar arama ihtiyacını ortadan kaldırır.
Çöp toplamayı denetleme¶
Bellek tahsisi gerektiğinde, MicroPython yığın üzerinde yeterli boyutta bir blok bulmaya çalışır. Bu başarısız olabilir; genellikle yığın artık kod tarafından referans verilmeyen nesnelerle dolu olduğu için. Bir başarısızlık meydana gelirse, çöp toplama olarak bilinen süreç bu gereksiz nesneler tarafından kullanılan belleği geri kazanır ve tahsis daha sonra yeniden denenir - bu süreç birkaç milisaniye sürebilir.
Periyodik olarak gc.collect() çağrısı yaparak bunu önceden gerçekleştirmenin yararları olabilir. Birincisi, gerçekten gerekli olmadan önce bir toplama yapmak daha hızlıdır - sık yapılırsa tipik olarak 1ms civarındadır. İkincisi, rastgele noktalarda, muhtemelen hız açısından kritik bir bölümde daha uzun bir gecikme oluşmasındansa, bu zamanın kullanıldığı kod noktasını siz belirleyebilirsiniz. Son olarak, düzenli olarak toplama yapmak yığındaki parçalanmayı azaltabilir. Şiddetli parçalanma kurtarılamaz tahsis başarısızlıklarına yol açabilir.
Yerel (Native) kod yayıcısı¶
Bu, MicroPython derleyicisinin bayt kodu yerine yerel CPU işlem kodları (opcode) yaymasına neden olur. MicroPython işlevselliğinin büyük kısmını kapsar, bu nedenle çoğu fonksiyon herhangi bir uyarlama gerektirmez (ancak aşağıya bakın). Bir fonksiyon dekoratörü aracılığıyla çağrılır:
@micropython.native
def foo(self, arg):
buf = self.linebuf # Cached object
# code
Yerel kod yayıcısının mevcut uygulamasında bazı sınırlamalar vardır.
raisekullanılırsa bir argüman sağlanmalıdır.Arka plan zamanlayıcısı (bkz.
micropython.schedule) yerel kodun yürütülmesi sırasında çalıştırılmaz.İş parçacıkları (threading) ve GIL bulunan hedeflerde, yerel kodun yürütülmesi sırasında GIL serbest bırakılmaz.
Son iki noktayı hafifletmek için, uzun süre çalışan yerel fonksiyonlar periyodik olarak time.sleep(0) çağırmalıdır; bu, zamanlayıcıyı çalıştırır ve GIL’i serbest bırakıp geri alır.
İyileştirilen performansın (kabaca bayt kodunun iki katı kadar hızlı) bedeli, derlenmiş kod boyutundaki artıştır.
Viper kod yayıcısı¶
Yukarıda tartışılan optimizasyonlar standartlara uyumlu Python kodunu içerir. Viper kod yayıcısı tam olarak uyumlu değildir. Performans peşinde özel Viper yerel veri türlerini destekler. Tam sayı işleme uyumlu değildir çünkü makine sözcüklerini kullanır: 32 bit donanımda aritmetik modulo 2**32 olarak gerçekleştirilir.
Yerel yayıcı gibi Viper de makine talimatları üretir, ancak özellikle tam sayı aritmetiği ve bit işlemleri için performansı önemli ölçüde artıran daha fazla optimizasyon gerçekleştirilir. Bir dekoratör kullanılarak çağrılır:
@micropython.viper
def foo(self, arg: int) -> int:
# code
Yukarıdaki parçanın gösterdiği gibi, Viper optimize edicisine yardımcı olmak için Python tür ipuçlarını kullanmak yararlıdır. Tür ipuçları, argümanların ve dönüş değerinin veri türleri hakkında bilgi sağlar; bunlar burada PEP0484 resmî olarak tanımlanmış standart bir Python dili özelliğidir. Viper, kendi tür kümesini destekler; yani int, uint (işaretsiz tam sayı), ptr, ptr8, ptr16 ve ptr32. ptrX türleri aşağıda tartışılmaktadır. Şu anda uint türü tek bir amaca hizmet eder: bir fonksiyon dönüş değeri için tür ipucu olarak. Böyle bir fonksiyon 0xffffffff döndürürse Python sonucu -1 yerine 2**32 -1 olarak yorumlar.
Yerel yayıcı tarafından getirilen kısıtlamalara ek olarak aşağıdaki kısıtlamalar geçerlidir:
Varsayılan argüman değerlerine izin verilmez.
Kayan nokta kullanılabilir ancak optimize edilmez.
Viper, optimize ediciye yardımcı olmak için işaretçi (pointer) türleri sağlar. Bunlar şunları içerir:
ptrBir nesneye işaretçi.ptr8Bir bayta işaret eder.ptr1616 bitlik bir yarım sözcüğe işaret eder.ptr3232 bitlik bir makine sözcüğüne işaret eder.
İşaretçi kavramı Python programcılarına yabancı gelebilir. Bellekte saklanan verilere doğrudan erişim sağlaması bakımından bir Python memoryview nesnesine benzerlikleri vardır. Öğelere alt simge gösterimi kullanılarak erişilir, ancak dilimler desteklenmez: bir işaretçi yalnızca tek bir öğe döndürebilir. Amacı, bitişik bellek konumlarında saklanan verilere - örneğin arabellek protokolünü destekleyen nesnelerde saklanan veriler ve bir mikrodenetleyicideki belleğe eşlenmiş çevre birimi yazmaçları gibi - hızlı rastgele erişim sağlamaktır. İşaretçiler kullanılarak programlama yapmanın tehlikeli olduğu unutulmamalıdır: sınır denetimi gerçekleştirilmez ve derleyici arabellek taşması hatalarını önlemek için hiçbir şey yapmaz.
Tipik kullanım değişkenleri önbelleğe almaktır:
@micropython.viper
def foo(self, arg: int) -> int:
buf = ptr8(self.linebuf) # self.linebuf is a bytearray or bytes object
for x in range(20, 30):
bar = buf[x] # Access a data item through the pointer
# code omitted
Bu örnekte derleyici, buf öğesinin bir bayt dizisinin adresi olduğunu “bilir”; çalışma zamanında buf[x] adresini hızla hesaplamak için kod yayabilir. Nesneleri Viper yerel türlerine dönüştürmek için tip dönüşümleri (cast) kullanıldığında, dönüşüm işlemi birkaç mikrosaniye sürebileceğinden bunlar zamanlama açısından kritik döngülerde değil, fonksiyonun başında gerçekleştirilmelidir. Tip dönüşümü kuralları şu şekildedir:
Tip dönüşüm operatörleri şu anda şunlardır:
int,bool,uint,ptr,ptr8,ptr16veptr32.Bir tip dönüşümünün sonucu yerel bir Viper değişkeni olacaktır.
Bir tip dönüşümüne verilen argümanlar bir Python nesnesi veya bir yerel Viper değişkeni olabilir.
Argüman bir yerel Viper değişkeniyse, tip dönüşümü yalnızca türü değiştiren (örn.
uinttüründenptr8türüne) bir işlemsizdir (yani çalışma zamanında hiçbir maliyeti yoktur), böylece bu işaretçiyi kullanarak depolama/yükleme yapabilirsiniz.Argüman bir Python nesnesiyse ve tip dönüşümü
intveyauintise, Python nesnesi tam sayı türünde olmalıdır ve o tam sayı nesnesinin değeri döndürülür.Bir bool tip dönüşümüne verilen argüman tam sayı türünde (boolean veya tam sayı) olmalıdır; bir dönüş türü olarak kullanıldığında viper fonksiyonu True veya False nesneleri döndürür.
Argüman bir Python nesnesiyse ve tip dönüşümü
ptr,ptr8,ptr16veyaptr32ise, Python nesnesi ya arabellek protokolüne sahip olmalıdır (bu durumda arabelleğin başlangıcına bir işaretçi döndürülür) ya da tam sayı türünde olmalıdır (bu durumda o tam sayı nesnesinin değeri döndürülür).
Salt okunur bir nesneye işaret eden bir işaretçiye yazmak tanımsız davranışa yol açar.
Not
Aşağıdaki kod örnekleri, stm modülünü sağlayan STM32 tabanlı OpenMV Cam’ler için verilmiştir. Açıklanan teknikler genel olarak geçerlidir.
stm modülü, MCU’nun çevre birimi yazmaçlarının bellek adreslerini açığa çıkarır. Her GPIO bağlantı noktasının, bitleri o bağlantı noktasının pinlerine bire bir eşlenen bir çıkış veri yazmacı (ODR) vardır: yazmaca yazmak, bir machine.Pin metodu çağrısının ek yükü olmadan o pinleri doğrudan sürer ve bir biti XOR’lamak pinini değiştirir. Orijinal OpenMV Cam’de mavi LED GPIOC pin 2’ye bağlıdır, bu nedenle aşağıdaki örnek mavi LED’i n kez değiştirmek için bir ptr16 tip dönüşümü kullanır:
BIT2 = const(1 << 2)
@micropython.viper
def toggle_n(n: int):
odr = ptr16(stm.GPIOC + stm.GPIO_ODR)
for _ in range(n):
odr[0] ^= BIT2
Üç kod yayıcısının ayrıntılı bir teknik açıklaması Kickstarter’da burada Not 1 ve burada Not 2 bulunabilir
Donanıma doğrudan erişme¶
Bu, daha ileri düzey programlama kategorisine girer ve hedef MCU hakkında bir miktar bilgi gerektirir. Bir OpenMV Cam’de bir çıkış pinini değiştirme örneğini düşünün. Standart yaklaşım şunu yazmak olurdu:
mypin.value(mypin.value() ^ 1) # mypin was instantiated as an output pin
Bu, Pin örneğinin value() metoduna iki çağrının ek yükünü içerir. Bu ek yük, yongaların GPIO bağlantı noktası çıkış veri yazmacının (ODR) ilgili bitine bir okuma/yazma gerçekleştirilerek ortadan kaldırılabilir. Bunu kolaylaştırmak için stm modülü, ilgili yazmaçların adreslerini veren bir dizi sabit sağlar (stm.GPIOC GPIOC bağlantı noktasının taban adresidir, stm.GPIO_ODR ise çıkış veri yazmacının ofsetidir). Yukarıdaki gibi, orijinal OpenMV Cam’deki mavi LED GPIOC pin 2’dir, bu nedenle hızlı bir değişimi şu şekilde gerçekleştirilebilir:
import machine
import stm
BIT2 = const(1 << 2)
machine.mem16[stm.GPIOC + stm.GPIO_ODR] ^= BIT2