Tôi có thể tái phát bao nhiêu? Tôi có thể tái phát bao nhiêu? Bao nhiêu ca! @ # QFSD @ $ RFW


19

Bảng mạch Arduino Uno có RAM hạn chế, có nghĩa là nó có sẵn một ngăn xếp cuộc gọi hạn chế. Đôi khi, đệ quy là lựa chọn nhanh duy nhất để thực hiện một thuật toán nhất định. Vì vậy, do ngăn xếp cuộc gọi bị hạn chế nghiêm trọng, đâu là cách để tìm ra chương trình nhất định đang chạy trên bảng, chính xác có bao nhiêu cuộc gọi đệ quy bạn có thể chi trả trước khi có tình trạng tràn ngăn xếp (và điều tồi tệ xảy ra)?


2
Bạn có thể đưa ra một ví dụ về một thuật toán trong đó sự lựa chọn giữa một giải pháp đệ quy và không đệ quy rất rõ ràng không?
Cyberg Ribbon

1
@Cyberg Ribbon Gần đây nhất, tôi đã phải viết và kiểm tra RDP trong một khoảng thời gian ngắn, vì vậy tôi đã tiếp tục với việc thực hiện đệ quy. Tôi không nói rằng lặp đi lặp lại là không thể, chỉ là nó có thể dễ dàng hơn để đi trước với phiên bản đệ quy.
asheeshr

Điều này có nghĩa gì trong tiêu đề câu hỏi : How much ca!@#QFSD@$RFW? Tôi tò mò về lý do tại sao không ai chỉnh sửa nó thành một thứ gì đó có ý nghĩa hơn (trong 4 năm qua).
Nick Gammon

Câu trả lời là 211lần (tùy thuộc vào nhiều yếu tố) :). Xem câu trả lời của tôi ở đây: arduino.stackexchange.com/a/51098/7727 . @NickGammon, anh đang giả vờ "chửi" tôi nghĩ. Đây là một cách chơi chữ cho "recurse". Mất tôi một phút để tìm ra điều đó quá. Ban đầu khá khó hiểu.
Gabriel Staples

Ồ tôi hiểu rồi. Tôi nghĩ rằng các máy chủ SE có thể đã gặp trục trặc :)
Nick Gammon

Câu trả lời:


15

Nếu bạn thực sự muốn tái diễn (và như @jippie nói rằng đó là một ý tưởng tồi; thông điệp cao siêu: đừng làm điều đó ) và muốn biết bạn có thể tái diễn bao nhiêu, thì bạn sẽ phải thực hiện một số tính toán và thử nghiệm; ngoài ra, bạn thường chỉ có một xấp xỉ của nó vì nó phụ thuộc rất nhiều vào trạng thái bộ nhớ tại thời điểm hàm đệ quy của bạn sẽ được gọi.

Đối với điều này, trước tiên bạn nên biết SRAM được tổ chức bên trong Arduino dựa trên AVR (nó sẽ không áp dụng cho ví dụ Arduino Galileo của Intel). Sơ đồ sau đây từ Adaf nhung cho thấy rõ:

Tổ chức SRAM

Sau đó, bạn cần phải biết tổng kích thước của SRAM của bạn (phụ thuộc vào Atmel MCU, do đó bạn có loại bo mạch Arduino nào).

Trên sơ đồ này, có thể dễ dàng tìm ra kích thước của khối Dữ liệu tĩnh vì nó được biết đến vào thời gian biên dịch và sẽ không thay đổi sau này.

Các Heap kích thước có thể khó khăn hơn để biết vì nó có thể thay đổi trong thời gian chạy, tùy theo cấp phát bộ nhớ động ( mallochoặc new) được thực hiện bởi phác thảo của bạn hoặc các thư viện nó sử dụng. Sử dụng bộ nhớ động là khá hiếm trên Arduino, nhưng một số chức năng tiêu chuẩn làm điều đó (loại Stringsử dụng nó, tôi nghĩ vậy).

Đối với kích thước Stack , nó cũng sẽ thay đổi trong thời gian chạy, dựa trên độ sâu hiện tại của các lệnh gọi hàm (mỗi lệnh gọi hàm lấy 2 byte trên Stack để lưu địa chỉ của người gọi) và số lượng và kích thước của các biến cục bộ bao gồm các đối số được truyền ( cũng được lưu trữ trên Stack ) cho tất cả các hàm được gọi cho đến bây giờ.

Vì vậy, giả sử recurse()hàm của bạn sử dụng 12 byte cho các biến và đối số cục bộ của nó, sau đó mỗi lệnh gọi hàm này (lần đầu tiên từ một người gọi bên ngoài và các hàm đệ quy) sẽ sử dụng 12+2byte.

Nếu chúng ta cho rằng:

  • bạn đang dùng Arduino UNO (SRAM = 2K)
  • bản phác thảo của bạn không sử dụng cấp phát bộ nhớ động (không có Heap )
  • bạn biết kích thước của Dữ liệu tĩnh của mình (giả sử là 132 byte)
  • khi recurse()hàm của bạn được gọi từ bản phác thảo của bạn, Stack hiện tại dài 128 byte

Sau đó, bạn còn lại với các 2048 - 132 - 128 = 1788byte có sẵn trên Stack . Do đó, số lượng các cuộc gọi đệ quy đến chức năng của bạn 1788 / 14 = 127, bao gồm cả cuộc gọi ban đầu (không phải là cuộc gọi đệ quy).

Như bạn có thể thấy, điều này rất khó, nhưng không phải là không thể tìm thấy những gì bạn muốn.

Một cách đơn giản hơn để có được kích thước ngăn xếp có sẵn trước đó recurse()được gọi là sử dụng chức năng sau (được tìm thấy trên trung tâm học tập Adafbean; tôi chưa tự mình kiểm tra nó):

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

Tôi đặc biệt khuyến khích bạn đọc bài viết này tại trung tâm học tập Adafbean.


Tôi thấy peter-r-bloomfield đã đăng câu trả lời của anh ấy khi tôi đang viết của tôi; Câu trả lời của anh ấy có vẻ tốt hơn vì nó mô tả đầy đủ nội dung của ngăn xếp sau một cuộc gọi (tôi đã quên trạng thái đăng ký).
jfpoilpret

Cả hai câu trả lời chất lượng rất tốt.
Cyberg Ribbon

Dữ liệu tĩnh = .bss + .data và được Arduino báo cáo là "RAM được lấy bởi các biến toàn cục" hay bất cứ điều gì, đúng không?
Gabriel Staples

1
@GabrielStaples có chính xác. Chi tiết hơn .bssđại diện cho các biến toàn cục không có giá trị ban đầu trong mã của bạn, trong khi đó datalà cho các biến toàn cục có giá trị ban đầu. Nhưng cuối cùng họ sử dụng cùng một không gian: Dữ liệu tĩnh trong sơ đồ.
jfpoilpret

1
@GabrielStaples đã quên một điều, về mặt kỹ thuật đây không chỉ là các biến toàn cục ở đó, bạn còn có các biến được khai báo statictrong một hàm.
jfpoilpret

8

Đệ quy là thực hành xấu trên một vi điều khiển như bạn đã tuyên bố và bạn có thể muốn tránh nó bất cứ khi nào có thể. Trên trang Arduino có một số ví dụ và thư viện có sẵn để kiểm tra kích thước RAM miễn phí . Ví dụ, bạn có thể sử dụng điều này để tìm ra khi nào nên phá vỡ đệ quy hoặc một chút phức tạp / mạo hiểm hơn để lập hồ sơ phác thảo của bạn và mã cứng giới hạn trong đó. Hồ sơ này sẽ được yêu cầu cho mọi thay đổi trong chương trình của bạn và cho mọi thay đổi trong chuỗi công cụ Arduino.


Một số trình biên dịch cao cấp hơn, chẳng hạn như IAR (người hỗ trợ AVR) và Keil (người không hỗ trợ AVR) có các công cụ để giúp bạn giám sát và quản lý không gian ngăn xếp. Mặc dù vậy, nó thực sự không nên dùng cho một cái gì đó nhỏ như ATmega328.
Cyberg Ribbon

7

Nó phụ thuộc vào chức năng.

Mỗi khi một chức năng được gọi, một khung mới sẽ được đẩy lên ngăn xếp. Nó thường sẽ chứa các mặt hàng quan trọng khác nhau, có khả năng bao gồm:

  • Trả về địa chỉ (điểm trong mã mà hàm được gọi).
  • Con trỏ cá thể cục bộ ( this) nếu gọi hàm thành viên.
  • Các tham số truyền vào hàm.
  • Đăng ký giá trị cần được khôi phục khi chức năng kết thúc.
  • Không gian cho các biến cục bộ bên trong hàm được gọi.

Như bạn có thể thấy, không gian ngăn xếp cần thiết cho một cuộc gọi nhất định phụ thuộc vào chức năng. Ví dụ: nếu bạn viết một hàm đệ quy chỉ lấy một inttham số và không sử dụng biến cục bộ, thì nó sẽ không cần nhiều hơn một vài byte trên ngăn xếp. Điều đó có nghĩa là bạn có thể gọi đệ quy nó nhiều hơn một hàm có nhiều tham số và sử dụng nhiều biến cục bộ (sẽ ăn hết stack nhanh hơn nhiều).

Rõ ràng trạng thái của ngăn xếp phụ thuộc vào những gì khác đang diễn ra trong mã. Nếu bạn bắt đầu đệ quy trực tiếp trong loop()hàm tiêu chuẩn , thì có lẽ sẽ không có nhiều trên ngăn xếp. Tuy nhiên, nếu bạn bắt đầu nó lồng nhiều cấp độ sâu vào các chức năng khác, thì sẽ không có nhiều chỗ. Điều đó sẽ ảnh hưởng đến số lần bạn có thể tái diễn mà không làm cạn kiệt ngăn xếp.

Điều đáng chú ý là tối ưu hóa đệ quy đuôi tồn tại trên một số trình biên dịch (mặc dù tôi không chắc liệu avr-gcc có hỗ trợ hay không). Nếu cuộc gọi đệ quy là điều cuối cùng trong một hàm, điều đó có nghĩa là đôi khi có thể tránh thay đổi khung ngăn xếp. Trình biên dịch chỉ có thể sử dụng lại khung hiện có, vì cuộc gọi 'cha mẹ' (có thể nói) đã kết thúc bằng cách sử dụng nó. Điều đó có nghĩa là về mặt lý thuyết bạn có thể tiếp tục đệ quy bao nhiêu tùy thích, miễn là chức năng của bạn không gọi bất cứ thứ gì khác.


1
avr-gcc không hỗ trợ đệ quy đuôi.
asheeshr

@AsheeshR - Tốt để biết. Cảm ơn. Tôi đoán nó có lẽ là không thể.
Peter Bloomfield

Bạn có thể thực hiện loại bỏ / tối ưu hóa cuộc gọi bằng cách tái cấu trúc mã của mình thay vì hy vọng trình biên dịch sẽ thực hiện. Miễn là cuộc gọi đệ quy ở cuối phương thức đệ quy, bạn có thể viết lại phương thức một cách an toàn để sử dụng vòng lặp while / for.
abasterfield

1
Bài đăng của @TheDoctor mâu thuẫn với "avr-gcc không hỗ trợ đệ quy đuôi", cũng như bài kiểm tra mã của tôi. Trình biên dịch đã thực sự thực hiện đệ quy đuôi, đó là cách anh ta có được tới một triệu lần thu hồi. Peter là chính xác - trình biên dịch có thể thay thế cuộc gọi / trả lại (như cuộc gọi cuối cùng trong hàm) bằng cách nhảy đơn giản . Nó có cùng kết quả cuối cùng và không tiêu tốn không gian ngăn xếp.
Nick Gammon

2

Tôi đã có câu hỏi chính xác tương tự khi tôi đọc Jumping vào C ++ của Alex Allain , Ch 16: Recursion, p.230, vì vậy tôi đã chạy một số bài kiểm tra.

TLDR;

Arduino Nano của tôi (ATmega328 mcu) có thể thực hiện 211 cuộc gọi hàm đệ quy (đối với mã được đưa ra dưới đây) trước khi nó bị tràn ngăn xếp và gặp sự cố.

Trước hết, hãy để tôi giải quyết khiếu nại này:

Đôi khi, đệ quy là lựa chọn nhanh duy nhất để thực hiện một thuật toán nhất định.

[Cập nhật: ah, tôi đọc lướt từ "nhanh". Trong trường hợp đó bạn có một số giá trị. Tuy nhiên, tôi nghĩ rằng nó đáng để nói như sau.]

Không, tôi không nghĩ đó là một tuyên bố đúng. Tôi khá chắc chắn tất cả các thuật toán đều có giải pháp đệ quy và không đệ quy, không có ngoại lệ. Chỉ là đôi khi nó dễ dàng hơn đáng kểđể sử dụng một thuật toán đệ quy. Đã nói rằng, đệ quy rất khó chịu khi sử dụng trên các vi điều khiển và có lẽ sẽ không bao giờ được phép trong mã quan trọng an toàn. Tuy nhiên, tất nhiên có thể làm điều đó trên vi điều khiển. Để biết mức độ "sâu" của bạn có thể đi vào bất kỳ chức năng đệ quy nào, chỉ cần kiểm tra nó! Chạy nó trong ứng dụng thực tế của bạn trong trường hợp thử nghiệm thực tế và loại bỏ tình trạng cơ sở của bạn để nó sẽ tái diễn vô hạn. In ra một bộ đếm và tự mình xem bạn có thể đi sâu đến mức nào để bạn biết liệu thuật toán đệ quy của mình có đẩy các giới hạn của RAM quá gần để sử dụng thực tế hay không. Dưới đây là một ví dụ dưới đây để buộc tràn ngăn xếp trên Arduino.

Bây giờ, một vài lưu ý:

Có bao nhiêu cuộc gọi đệ quy hoặc "khung ngăn xếp" bạn có thể nhận được được xác định bởi một số yếu tố, bao gồm:

  • Kích thước của RAM của bạn
  • Có bao nhiêu thứ đã có trên ngăn xếp của bạn hoặc chiếm hết trong đống của bạn (ví dụ: vấn đề RAM miễn phí của bạn; free_RAM = total_RAM - stack_used - heap_usedhoặc bạn có thể nói free_RAM = stack_size_allocated - stack_size_used)
  • Kích thước của mỗi "khung ngăn xếp" mới sẽ được đặt vào ngăn xếp cho mỗi lệnh gọi hàm đệ quy mới. Điều này sẽ phụ thuộc vào hàm được gọi và các biến và yêu cầu bộ nhớ của nó, v.v.

Kết quả của tôi:

  • 20171106-2054hrs - Vệ tinh Toshiba w / RAM 16 GB; lõi tứ, Windows 8.1: giá trị cuối cùng được in trước khi gặp sự cố: 43166
    • mất vài giây để sụp đổ - có thể 5 ~ 10?
  • Máy tính xách tay cao cấp Dell 20180306-1913hrs với RAM 64 GB; 8 nhân, Linux Ubuntu 14.04 LTS: giá trị cuối cùng được in trước khi gặp sự cố: 261752
    • theo sau là cụm từ Segmentation fault (core dumped)
    • chỉ mất ~ 4 ~ 5 giây hoặc lâu hơn để sụp đổ
  • 20180306-1930hrs Arduino Nano: TBD --- ở mức ~ 250000 và vẫn đang tính --- cài đặt tối ưu hóa Arduino phải khiến nó tối ưu hóa đệ quy ... ??? Vâng, đó là trường hợp.
    • Thêm #pragma GCC optimize ("-O0")vào đầu tập tin và làm lại:
  • 20180307-0910hrs Arduino Nano: flash 32 kB, SRAM 2 kB, tiến trình 16 MHz: giá trị cuối cùng được in trước khi gặp sự cố: 211 Here are the final print results: 209 210 211 ⸮ 9⸮ 3⸮
    • chỉ mất một phần của giây khi nó bắt đầu in ở tốc độ baud nối tiếp 115200 - có thể là 1/10 giây
    • 2 kiB = 2048 byte / 211 khung xếp chồng = 9,7 byte / khung (giả sử TẤT CẢ RAM của bạn đang được sử dụng bởi ngăn xếp - thực tế không phải vậy) - tuy nhiên điều này có vẻ rất hợp lý.

Mật mã:

Ứng dụng PC:

/*
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);
}

Chương trình "Phác thảo" Arduino:

/*
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()
{
}

Tài liệu tham khảo:

  1. Nhảy vào C ++ của Alex Allain , Ch 16: Recursion, tr.230
  2. http://www.electricrcaircraftguy.com/2014/01/the-power-of-arduino.html - theo nghĩa đen: Tôi tham khảo trang web của riêng tôi trong thời gian này "dự án" để nhắc nhở bản thân mình làm thế nào để thay đổi Arduino mức biên dịch tối ưu hóa cho một tập tin được với #pragma GCC optimizelệnh từ khi tôi biết tôi đã có tài liệu ở đó.

1
Lưu ý rằng, theo các tài liệu của avr-lib, bạn không bao giờ nên biên dịch mà không tối ưu hóa bất cứ điều gì liên quan đến avr-libc, vì một số thứ không được đảm bảo để thậm chí hoạt động với tối ưu hóa bị tắt. Vì vậy, tôi khuyên bạn chống lại #pragmabạn đang sử dụng ở đó. Thay vào đó, bạn có thể thêm __attribute__((optimize("O0")))vào chức năng duy nhất mà bạn muốn tối ưu hóa.
Edgar Bonet

Cảm ơn, Edgar. Bạn có biết nơi libc của AVR có tài liệu này không?
Gabriel Staples

1
Các tài liệu về <util / delay.h> trạng thái: “Để cho các chức năng này để làm việc như dự định, tối ưu hóa trình biên dịch phải được kích hoạt [...]” (nhấn mạnh trong bản gốc). Tôi không chắc chắn liệu có bất kỳ chức năng avr-libc nào khác có yêu cầu này không.
Edgar Bonet

1

Tôi đã viết chương trình thử nghiệm đơn giản này:

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);
}

Tôi đã biên dịch nó cho Uno, và khi tôi viết nó đã được đệ quy hơn 1 triệu lần! Tôi không biết, nhưng trình biên dịch có thể đã tối ưu hóa chương trình này


Cố gắng quay lại sau một số lượng cuộc gọi đã đặt ~ 1000. Nó sẽ tạo ra một vấn đề sau đó.
asheeshr

1
Trình biên dịch đã thực hiện khéo léo đệ quy đuôi trên bản phác thảo của bạn, như bạn sẽ thấy nếu bạn tháo rời nó. Điều này có nghĩa là nó thay thế chuỗi call xxx/ retbởi jmp xxx. Điều này cũng tương tự, ngoại trừ phương pháp của trình biên dịch không tiêu thụ ngăn xếp. Do đó, bạn có thể lặp lại hàng tỷ lần với mã của mình (những thứ khác bằng nhau).
Nick Gammon

Bạn có thể buộc trình biên dịch không tối ưu hóa đệ quy. Tôi sẽ quay lại và đăng một ví dụ sau.
Gabriel Staples

Làm xong! Ví dụ ở đây: arduino.stackexchange.com/a/51098/7727 . Bí quyết là ngăn chặn tối ưu hóa bằng cách thêm #pragma GCC optimize ("-O0") vào đầu chương trình Arduino của bạn. Tôi tin rằng bạn phải làm điều này ở đầu mỗi tệp mà bạn muốn áp dụng cho nó - nhưng tôi đã không tìm thấy điều đó trong nhiều năm nên hãy tự mình nghiên cứu nó.
Gabriel Staples
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.