Ne kadar para çekebilirim? Ne kadar para çekebilirim? Ne kadar ca! @ # QFSD @ $ RFW


19

Arduino Uno kartı sınırlı bir RAM'e sahiptir, bu da sınırlı bir çağrı yığınına sahip olduğu anlamına gelir. Bazen, özyineleme belirli bir algoritmayı uygulamak için tek hızlı seçenektir. Yani, çağrı yığını çok sınırlı olduğu göz önüne alındığında, tahtada çalışan belirli bir program göz önüne alındığında, bir yığın taşması (ve kötü şeyler meydana gelmeden) tam olarak kaç özyinelemeli arama yapabileceğinizi öğrenmenin bir yolu ne olurdu?


2
Özyinelemeli ve özyinelemesiz çözüm arasındaki seçimin çok net olduğu bir algoritma örneği verebilir misiniz?
Cybergibbons

1
@Cybergibbons Son zamanlarda, kısa sürede RDP yazmak ve test etmek zorunda kaldım , bu yüzden tekrarlanan uygulama ile devam ettim. Yinelemenin mümkün olmadığını söylemiyorum, sadece özyinelemeli versiyonla devam etmek daha kolay olabilir.
asheeshr

Soru başlığında bunun anlamı nedir How much ca!@#QFSD@$RFW? Kimsenin bunu neden daha anlamlı bir şey (son 4 yılda) düzenlemediğini merak ediyorum.
Nick Gammon

Cevap 211sürelerdir (birçok faktöre bağlı olarak) :). Cevabımı buraya bakın: arduino.stackexchange.com/a/51098/7727 . @ NickGammon, sanırım "lanetliyor" gibi davranıyor. Bu "recurse" için kelime bir oyun. Bunu da çözmem için bir dakikamı aldı. İlk başta oldukça kafa karıştırıcıydı.
Gabriel Staples

Ah anlıyorum. Ben SE sunucuları bir aksaklık olabilir sanıyordum :)
Nick Gammon

Yanıtlar:


15

Gerçekten tekrarlamak istiyorsanız (ve @jippie'nin kötü bir fikir olduğunu söylediği gibi, bilinçaltı mesajı: yapmayın ) ve ne kadar para çekebileceğinizi bilmek istiyorsanız, o zaman bazı hesaplama ve deneyler yapmanız gerekir; ayrıca, özyinelemeli fonksiyonunuzun çağrıldığı andaki bellek durumuna çok bağlı olduğu için genellikle sadece bir yaklaşımınız olacaktır.

Bunun için önce SRAM'ın AVR tabanlı Arduino içinde nasıl organize edildiğini bilmelisiniz (örneğin Intel tarafından Arduino Galileo için geçerli değildir). Adafruit'ten aşağıdaki diyagram açıkça göstermektedir:

SRAM organizasyonu

O zaman SRAM'ınızın toplam boyutunu bilmeniz gerekir (Atmel MCU'ya bağlıdır, bu nedenle ne tür Arduino kartına sahipsiniz).

Bu şemada, derleme zamanında bilindiği ve daha sonra değişmeyeceği için Statik Veri bloğunun boyutunu bulmak kolaydır .

Öbek Zamanında değişebilir olarak boyut dinamik hafıza tahsisleri (bağlı bilmek daha zor olabilir mallocya newda kroki veya kullandığı kütüphaneler tarafından yapılır). Dinamik bellek kullanımı Arduino'da oldukça nadirdir, ancak bazı standart işlevler bunu yapar (tip Stringkullanır, sanırım).

İçin yığın boyutu, aynı zamanda, bir işlev arama mevcut derinliğine bağlı olarak, çalışma sırasında değişir ve geçirilen ifade dahil olmak üzere yerel değişkenlerin sayısını ve boyutunu ((her bir işlev arama arayanın adresini depolamak için Stack 2 bayt alır) o da saklanan Yığın şu ana kadar denilen tüm fonksiyonlar için).

Diyelim ki, recurse()işleviniz yerel değişkenleri ve argümanları için 12 bayt kullanıyor, sonra bu işleve yapılan her çağrı (harici bir arayandan ve özyinelemeli olanlardan ilk) 12+2bayt kullanacaktır .

Eğer varsayalım:

  • Arduino UNO üzerindesiniz (SRAM = 2K)
  • çiziminiz dinamik bellek ayırma kullanmıyor ( Yığın yok )
  • Statik Verilerinizin boyutunu biliyorsunuz (diyelim ki 132 bayt)
  • senin zaman recurse()işlevi taslaktan denir, cari Stack 128 byte uzunluğundadır

Sonra Yığın2048 - 132 - 128 = 1788 üzerinde kullanılabilir bayt kalır . Bu nedenle 1788 / 14 = 127, ilk çağrıyı da içeren (özyinelemeli olmayan) işlevinize özyinelemeli çağrıların sayısı .

Gördüğünüz gibi, bu çok zor, ama ne istediğinizi bulmak imkansız değil.

Daha önce kullanılabilir yığın boyutunu elde etmenin daha basit bir yolu recurse()aşağıdaki işlevi kullanmaktır (Adafruit öğrenme merkezinde bulunur; kendim test etmedim):

int freeRam () 
{
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

Adafruit öğrenme merkezinde bu makaleyi okumanızı şiddetle tavsiye ediyorum .


Ben peter-r-bloomfield cevabını benim yazarken gönderdiğini görüyorum; Bir çağrıdan sonra yığının içeriğini tam olarak açıkladığı için cevabı daha iyi görünüyor (Ben kayıt durumunu unuttum).
jfpoilpret

Her ikisi de çok kaliteli cevaplar.
Cybergibbons

Statik veri = .bss + .data ve Arduino tarafından "global değişkenler tarafından alınan RAM" olarak rapor edilen veya her neyse, doğru mu?
Gabriel Staples

1
@GabrielStaples evet kesinlikle. Daha ayrıntılı .bssolarak, kodunuzda başlangıç ​​değeri olmayan genel değişkenleri gösterirken, başlangıç ​​değeri olan datagenel değişkenler içindir. Ama sonunda aynı alanı kullanıyorlar: Diyagramdaki Statik Veriler .
jfpoilpret

1
@GabrielStaples bir şeyi unuttu, teknik olarak bunlar sadece küresel değişkenler değil, aynı zamanda staticbir fonksiyon içinde bildirilen değişkenler var .
jfpoilpret

8

Özyineleme, kendinizi önceden belirttiğiniz ve muhtemelen mümkün olduğunca kaçınmak istediğiniz için bir mikrodenetleyici üzerinde kötü bir uygulamadır. Açık Arduino sitede ücretsiz RAM boyutunu kontrol etmek için kullanılabilir bazı örnekler ve kütüphaneler vardır . Örneğin, özyinelemenin ne zaman kırılacağını veya çiziminizi profillemek ve içindeki sınırı kodlamak için biraz daha karmaşık / riskli olmak için kullanabilirsiniz. Bu profil, programınızdaki her değişiklik ve Arduino takım zincirindeki her değişiklik için gerekli olacaktır.


IAR (AVR'yi destekleyen) ve Keil (AVR'yi desteklemeyen) gibi daha üst düzey derleyicilerden bazıları, yığın alanını izlemenize ve yönetmenize yardımcı olacak araçlara sahiptir. Yine de ATmega328 kadar küçük bir şey için tavsiye edilmez.
Cybergibbons

7

Bu fonksiyona bağlıdır.

Bir işlev her çağrıldığında, yığına yeni bir çerçeve itilir. Genellikle aşağıdakileri içeren çeşitli kritik öğeler içerir:

  • Dönüş adresi (kodda işlevin çağrıldığı nokta).
  • thisÜye işlevi çağrılıyorsa yerel örnek işaretçisi ( ).
  • Parametreler işleve iletildi.
  • İşlev sona erdiğinde geri yüklenmesi gereken değerleri kaydedin.
  • Aranan işlev içindeki yerel değişkenler için alan.

Gördüğünüz gibi, belirli bir çağrı için gereken yığın alanı işleve bağlıdır. Örneğin, yalnızca intparametre alan ve yerel değişken kullanmayan özyinelemeli bir işlev yazarsanız , yığın üzerinde birkaç bayttan daha fazlasına ihtiyaç duymaz. Bu, yinelemeli olarak, birkaç parametre alan ve çok sayıda yerel değişken kullanan (yığını daha hızlı tüketecek) bir işlevden çok daha fazla çağırabileceğiniz anlamına gelir.

Açıkça, yığının durumu kodda başka neler olduğuna bağlıdır. Doğrudan standart loop()işlev içinde bir özyineleme başlatırsanız , yığın üzerinde zaten çok fazla olmayacaktır. Ancak, diğer işlevlerde birkaç seviyeyi derinlemesine iç içe başlatırsanız, o kadar fazla yer kalmaz. Bu, yığını yormadan kaç kez tekrarlayabileceğinizi etkileyecektir.

Bazı derleyicilerde kuyruk özyineleme optimizasyonunun mevcut olduğunu belirtmek gerekir (ancak avr-gcc'nin destekleyip desteklemediğinden emin değilim). Yinelemeli çağrı bir işlevdeki en son şeyse, bazen yığın çerçevesini değiştirmekten kaçınmanın mümkün olduğu anlamına gelir. Derleyici mevcut çerçeveyi yeniden kullanabilir, çünkü 'üst' çağrı (tabiri caizse) onu kullanarak bitirilir. Bu, işleviniz başka bir şey çağırmadıkça, teorik olarak istediğiniz kadar yinelemeye devam edebileceğiniz anlamına gelir.


1
avr-gcc kuyruk özyinelemesini desteklemez.
asheeshr

@AsheeshR - Bilmek güzel. Teşekkürler. Muhtemelen olası olmadığını düşündüm.
Peter Bloomfield

Derleyicinin bunu yapmasını ummak yerine kodunuzu yeniden düzenleyerek kuyruk çağrısı eleme / optimizasyonu yapabilirsiniz. Özyinelemeli çağrı özyinelemeli yöntemin sonunda olduğu sürece bir while / for döngüsü kullanmak için yöntemi güvenli bir şekilde yeniden yazabilirsiniz.
abasterfield

1
@TheDoctor'un gönderisi, kodunu test ettiğim gibi "avr-gcc kuyruk yinelemesini desteklemiyor" ile çelişiyor. Derleyici kuyruk özyineleme uyguladı, yani bir milyon özyineye kadar çıktı. Peter doğrudur - derleyicinin çağrı / geri dönüşü (bir fonksiyondaki son çağrı olarak) sadece atlama ile değiştirmesi mümkündür . Aynı sonuç sonucuna sahiptir ve yığın alanı tüketmez.
Nick Gammon

2

Alex Allain , Ch 16: Recursion, s.230, C ++ içine Jumping okurken aynı soru vardı , bu yüzden bazı testler koştu.

TLDR;

Arduino Nano'm (ATmega328 mcu), yığın taşması ve çökmesi olmadan önce 211 özyinelemeli işlev çağrısı yapabilir (aşağıda verilen kod için).

Öncelikle, bu iddiayı ele alalım:

Bazen, özyineleme belirli bir algoritmayı uygulamak için tek hızlı seçenektir.

[Güncelleme: ah, "çabuk" kelimesini kaçırdım. Bu durumda geçerliliğiniz vardır. Bununla birlikte, bence aşağıdakileri söylemeye değer.]

Hayır, bunun gerçek bir ifade olduğunu sanmıyorum. İstisnasız tüm algoritmaların hem özyinelemeli hem de özyinelemeli olmayan bir çözüme sahip olduğundan eminim . Sadece bazen önemli ölçüde daha kolayözyinelemeli algoritma kullanmak. Bunu söyledikten sonra, özyineleme mikrodenetleyicilerde kullanım için çok kaşlarını çatmıştır ve muhtemelen güvenlik açısından kritik kodlarda asla izin verilmez. Bununla birlikte, elbette mikrodenetleyiciler üzerinde yapmak mümkündür. Nasıl "derin" herhangi bir özyinelemeli işleve gidebilirsiniz bilmek için, sadece test edin! Gerçek hayattaki uygulamanızda gerçek hayattaki bir test senaryosunda çalıştırın ve temel durumunuzu sonsuz bir şekilde geri alabilmesi için çıkarın. Bir sayaç yazdırın ve kendiniz için ne kadar "derin" gidebileceğinizi görün, böylece özyinelemeli algoritmanızın RAM'inizin sınırlarını pratik olarak kullanmak için çok fazla zorlayıp zorlamadığını bilirsiniz. Aşağıda, bir Arduino'daki yığın taşmasını zorlamak için bir örnek verilmiştir.

Şimdi, birkaç not:

Kaç özyinelemeli çağrı veya "yığın çerçeve" alabileceğiniz, aşağıdakiler de dahil olmak üzere bir dizi faktör tarafından belirlenir:

  • RAM'inizin boyutu
  • Yığınızda ne kadar şey olduğu veya yığınızda ne kadar yer kapladığı (yani: boş RAM'iniz önemlidir; free_RAM = total_RAM - stack_used - heap_usedveya söyleyebilirsiniz free_RAM = stack_size_allocated - stack_size_used)
  • Her yeni özyinelemeli işlev çağrısı için yığına yerleştirilecek her yeni "yığın çerçevesinin" boyutu. Bu, çağrılan işleve, değişkenlerine ve bellek gereksinimlerine vb.

Benim sonuçlarım:

  • 20171106-2054saat - 16 GB RAM ile Toshiba Satellite; dört çekirdekli, Windows 8.1: çökmeden önce yazdırılan son değer: 43166
    • çökmek için birkaç saniye sürdü - belki 5 ~ 10?
  • 20180306-1913saat 64 GB RAM ile Dell üst düzey dizüstü bilgisayar; 8 çekirdekli, Linux Ubuntu 14.04 LTS: çökmeden önce yazdırılan son değer: 261752
    • ardından cümle Segmentation fault (core dumped)
    • çökmek için sadece ~ 4 ~ 5 saniye sürdü
  • 20180306-1930hrs Arduino Nano: TBD --- ~ 250000'de ve hala sayıyor --- Arduino optimizasyon ayarları, özyinelemeyi optimize etmesine neden olmuş olmalı ... ??? Evet, durum böyle.
    • #pragma GCC optimize ("-O0")Dosyanın en üstüne ekleyin ve tekrar yapın:
  • 20180307-0910saat Arduino Nano: 32 kB flaş, 2 kB SRAM, 16 MHz işlemci: çökmeden önce basılan son değer: 211 Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
    • 115200 seri baud hızında yazdırmaya başladıktan sonra saniyenin sadece bir kısmını aldı - belki 1/10 sn
    • 2 kiB = 2048 bayt / 211 yığın çerçeve = 9.7 bayt / çerçeve (RAM'inizin TÜMÜNÜN yığın tarafından kullanıldığını varsayarsak - aslında durum böyle değildir) - ama yine de bu çok makul görünüyor.

Kod:

PC uygulaması:

/*
stack_overflow
 - a quick program to force a stack overflow in order to see how many stack frames in a small function can be loaded onto the stack before the overflow occurs

By Gabriel Staples
www.ElectricRCAircraftGuy.com
Written: 6 Nov 2017
Updated: 6 Nov 2017

References:
 - Jumping into C++, by Alex Allain, pg. 230 - sample code here in the chapter on recursion

To compile and run:
Compile: g++ -Wall -std=c++11 stack_overflow_1.cpp -o stack_overflow_1
Run in Linux: ./stack_overflow_1
*/

#include <iostream>

void recurse(int count)
{
  std::cout << count << "\n";
  recurse(count + 1);
}

int main()
{
  recurse(1);
}

Arduino "Sketch" programı:

/*
recursion_until_stack_overflow
- do a quick recursion test to see how many times I can make the call before the stack overflows

Gabriel Staples
Written: 6 Mar. 2018 
Updated: 7 Mar. 2018 

References:
- Jumping Into C++, by Alex Allain, Ch. 16: Recursion, p.230
*/

// Force the compiler to NOT optimize! Otherwise this recursive function below just gets optimized into a count++ type
// incrementer instead of doing actual recursion with new frames on the stack each time. This is required since we are
// trying to force stack overflow. 
// - See here for all optimization levels: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
//   - They include: -O1, -O2, -O3, -O0, -Os (Arduino's default I believe), -Ofast, & -Og.

// I mention `#pragma GCC optimize` in my article here: http://www.electricrcaircraftguy.com/2014/01/the-power-of-arduino.html
#pragma GCC optimize ("-O0") 

void recurse(unsigned long count) // each call gets its own "count" variable in a new stack frame 
{
  // delay(1000);
  Serial.println(count);

  // It is not necessary to increment count since each function's variables are separate (so the count in each stack
  // frame will be initialized one greater than the last count)
  recurse (count + 1);

  // GS: notice that there is no base condition; ie: this recursive function, once called, will never finish and return!
}

void setup()
{
  Serial.begin(115200);
  Serial.println(F("\nbegin"));
  // First function call, so it starts at 1
  recurse (1);
}

void loop()
{
}

Referanslar:

  1. C ++ 'a atlama Alex Allain , Bölüm 16: Özyineleme, s.230
  2. http://www.electricrcaircraftguy.com/2014/01/the-power-of-arduino.html - kelimenin tam anlamıyla: Belirli bir dosya için Arduino derleyici optimizasyon düzeylerini nasıl değiştireceğimi hatırlatmak için bu "proje" sırasında kendi web siteme başvurdum ile #pragma GCC optimizeben orada belgelenmiş olduğunu biliyordu beri komuta.

1
Avr-lib'in belgelerine göre, hiçbir şeyin optimizasyon olmadan derlememeniz gerektiğini unutmayın, çünkü bir şey optimizasyon kapalı olduğunda bile çalışacağı garanti edilmez. Böylece, #pragmaorada kullandığınız size karşı size tavsiyede bulunuyorum . Bunun yerine, __attribute__((optimize("O0")))en iyi duruma getirmek istemediğiniz tek bir işleve ekleyebilirsiniz .
Edgar Bonet

Teşekkürler, Edgar. AVR libc'nin bunu nerede belgelediğini biliyor musunuz?
Gabriel Staples

1
<Util / delay.h> üzerinde dokümantasyon devletler: “amacıyla tasarlanan, derleyici optimizasyonlar olarak çalışmalarına bu fonksiyonlar için gereken (orijinalde vurgu) [...] etkinleştirilmelidir”. Diğer avr-libc işlevlerinin bu gereksinime sahip olup olmadığından emin değilim.
Edgar Bonet

1

Bu basit test programını yazdım:

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  recurse(1);
}

void loop() {
  // put your main code here, to run repeatedly: 

}

void recurse(long i) {
  Serial.println(i);
  recurse(i+1);
}

Uno için derledim ve yazarken 1 milyondan fazla kez tekrarladı! Bilmiyorum, ancak derleyici bu programı optimize etmiş olabilir


Belirlenen sayıda aramadan sonra ~ 1000'e dönmeyi deneyin. O zaman bir sorun yaratmalı.
asheeshr

1
Derleyici taslağınızda kurnazca özyineleme uyguladı , çünkü onu söküp sökmeyeceğinizi göreceksiniz. Bunun anlamı, dizinin call xxx/ rettarafından değiştirilmesidir jmp xxx. Bu, derleyicinin yönteminin yığını tüketmemesi dışında aynı anlama gelir. Böylece kodunuzla milyarlarca kez tekrar okuyabilirsiniz (diğer şeyler eşittir).
Nick Gammon

Derleyiciyi özyinelemeyi optimize etmemeye zorlayabilirsiniz. Geri dönüp daha sonra bir örnek göndereceğim.
Gabriel Staples

Bitti! Burada örnek: arduino.stackexchange.com/a/51098/7727 . İşin sırrı, #pragma GCC optimize ("-O0") Arduino programınızın en üstüne ekleyerek optimizasyonu önlemektir . Bunu , başvurmasını istediğiniz her dosyanın üstünde yapmanız gerektiğine inanıyorum - ancak yıllardır bunu aramadım, emin olmak için araştırın.
Gabriel Staples
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.