Czy nieskończona pętla wewnątrz pętli () działałaby szybciej?

19

Pisząc typowy szkic, zwykle polegasz na loop()tym , że jesteś wywoływany wielokrotnie tak długo, jak działa Arduino. Wchodzenie i wychodzenie z loop()funkcji musi jednak powodować niewielki narzut.

Aby tego uniknąć, możesz prawdopodobnie stworzyć własną nieskończoną pętlę, taką jak ta:

void loop()
{
    while (true)
    {
        // do stuff...
    }
}

Czy to realny sposób na poprawę wydajności? Czy spowoduje to inne problemy, jeśli loop()nigdy nie powróci?

Peter Bloomfield
źródło

Odpowiedzi:

18

Część kodu w rdzeniu ATmega, która wykonuje setup () i loop (), jest następująca:

#include <Arduino.h>

int main(void)
{
        init();

#if defined(USBCON)
        USBDevice.attach();
#endif

        setup();

        for (;;) {
                loop();
                if (serialEventRun) serialEventRun();
        }

        return 0;
}

Całkiem proste, ale istnieje narzut związany z serialEventRun (); tam.

Porównajmy dwa proste szkice:

void setup()
{

}

volatile uint8_t x;

void loop()
{

    x = 1;

}

i

void setup()
{

}

volatile uint8_t x;

void loop()
{
    while(true)
    {
        x = 1;
    }
}

X i lotne mają jedynie na celu zapewnienie, że nie jest zoptymalizowane.

W wyprodukowanym ASM otrzymujesz różne wyniki: Porównanie dwóch

Możesz zobaczyć, jak while (prawda) po prostu wykonuje rjmp (skok względny) cofając kilka instrukcji, podczas gdy loop () wykonuje odejmowanie, porównanie i wywołanie. To są 4 instrukcje vs 1 instrukcja.

Aby wygenerować ASM jak wyżej, musisz użyć narzędzia o nazwie avr-objdump. Jest to dołączone do avr-gcc. Lokalizacja różni się w zależności od systemu operacyjnego, więc najłatwiej jest wyszukiwać według nazwy.

avr-objdump może działać na plikach .hex, ale brakuje w nich oryginalnego źródła i komentarzy. Jeśli właśnie zbudowałeś kod, będziesz mieć plik .elf, który zawiera te dane. Ponownie, lokalizacja tych plików różni się w zależności od systemu operacyjnego - najłatwiejszym sposobem ich zlokalizowania jest włączenie pełnej kompilacji w preferencjach i sprawdzenie, gdzie są przechowywane pliki wyjściowe.

Uruchom polecenie w następujący sposób:

avr-objdump -S output.elf> asm.txt

I sprawdź dane wyjściowe w edytorze tekstu.

Cybergibbons
źródło
OK, ale czy nie ma powodu wywoływania funkcji serialEventRun ()? Po co to jest?
jfpoilpret
1
Jest to część funkcji używanej przez HardwareSerial, nie wiadomo, dlaczego nie jest usuwana, gdy nie jest potrzebny szeregowy.
Cybergibbons
2
Przydałoby się krótkie wyjaśnienie, w jaki sposób wygenerowano dane wyjściowe ASM, aby ludzie mogli się sprawdzić.
jippie
@Cybergibbons nigdy nie jest usuwany, ponieważ jest częścią standardu main.cużywanego przez Arduino IDE. Nie oznacza to jednak, że biblioteka HardwareSerial jest dołączona do szkicu; faktycznie nie jest włączone, jeśli nie go używać (dlatego istnieje if (serialEventRun)w main()funkcji Jeśli nie używać HardwareSerial biblioteka wtedy. serialEventRunbędzie zerowa, stąd nie ma połączenia.
jfpoilpret
1
Tak, jest to część main.c, jak cytowano, ale oczekiwałbym, że zostanie zoptymalizowana, jeśli nie będzie wymagana, dlatego myślę, że zawsze uwzględniane są aspekty Serial. Często piszę kod, który nigdy nie powróci z loop () i nie zauważam problemów z Serial.
Cybergibbons
6

Odpowiedź Cybergibbonsa całkiem ładnie opisuje generowanie kodu asemblera i różnice między dwiema technikami. Ma to stanowić uzupełniającą odpowiedź, biorąc pod uwagę kwestię różnic praktycznych , tj. Ile różnicy zrobi każde z nich pod względem czasu realizacji .


Odmiany kodu

Zrobiłem analizę obejmującą następujące odmiany:

  • Podstawowy void loop()(który jest wprowadzany podczas kompilacji)
  • Bez linii void loop()(za pomocą __attribute__ ((noinline)))
  • Pętla z while(1)(która zostaje zoptymalizowana)
  • Pętla z niezoptymalizowaną while(1)(przez dodanie __asm__ __volatile__("");. Jest to nopinstrukcja, która zapobiega optymalizacji pętli bez powodowania dodatkowych narzutów volatilezmiennej)
  • Unline void loop()z optymalizacjąwhile(1)
  • Un-inline void loop()z niezoptymalizowanymwhile(1)

Szkice można znaleźć tutaj .

Eksperyment

Uruchomiłem każdy z tych szkiców przez 30 sekund, gromadząc w ten sposób 300 punktów danych . W delaykażdej pętli było wywołanie 100 milisekund (bez których zdarzają się złe rzeczy ).

Wyniki

Następnie obliczyłem średni czas wykonania każdej pętli, odejmowałem 100 milisekund od każdej, a następnie narysowałem wyniki.

http://raw2.github.com/AsheeshR/Arduino-Loop-Analysis/master/Figures/timeplot.png

Wniosek

  • Niezoptymalizowana while(1)pętla wewnątrz void loopjest szybsza niż zoptymalizowany kompilator void loop.
  • Różnica czasu między niezoptymalizowanym kodem a domyślnym zoptymalizowanym kodem Arduino jest praktycznie nieistotna . Lepiej będzie kompilować ręcznie, używając avr-gccwłasnych flag optymalizacji i zamiast korzystać z Arduino IDE, aby ci w tym pomóc (jeśli potrzebujesz optymalizacji mikrosekundowych).

UWAGA: Rzeczywiste wartości czasu nie mają tutaj znaczenia, różnica między nimi jest. Do ~ 90 mikrosekund czasu wykonania obejmuje wywołanie Serial.println, microsa delay.

UWAGA 2: Dokonano tego przy użyciu Arduino IDE i domyślnych flag kompilatora, które dostarcza.

UWAGA 3: Analiza (wykres i obliczenia) została wykonana przy użyciu R.

asheeshr
źródło
1
Dobra robota. Wykres ma milisekundy, a nie mikrosekundy, ale nie stanowi wielkiego problemu.
Cybergibbons
@Cybergibbons To mało prawdopodobne, ponieważ wszystkie pomiary są w mikrosekundach i nie zmieniłem nigdzie skali :)
asheeshr