Dlaczego kompilator GCC pomija jakiś kod?

9

Nie rozumiem, dlaczego kompilator GCC wycina część mojego kodu, podczas gdy zachowuje absolutnie ten sam w sąsiedztwie?

Kod C:

#define setb_SYNCO do{(PORTA|= (1<<0));} while(0);

ISR(INT0_vect){
    unsigned char i;

    i = 10;
    while(i>0)i--;   // first pause - omitted

    setb_SYNCO;
    setb_GATE;
    i=30;
    clrb_SYNCO;
    while(i>0)i--;  // second pause - preserved
    clrb_GATE;
}

Odpowiednia część LSS (plik asemblera, stworzony przez kompilator):

ISR(INT0_vect){
  a4:   1f 92           push    r1
  a6:   0f 92           push    r0
  a8:   0f b6           in  r0, 0x3f    ; 63
  aa:   0f 92           push    r0
  ac:   11 24           eor r1, r1
  ae:   8f 93           push    r24
    unsigned char i;

    i = 10;
    while(i>0)i--;

    setb_SYNCO;
  b0:   d8 9a           sbi 0x1b, 0 ; 27
    setb_GATE;
  b2:   d9 9a           sbi 0x1b, 1 ; 27
    i=30;
    clrb_SYNCO;
  b4:   d8 98           cbi 0x1b, 0 ; 27
  b6:   8e e1           ldi r24, 0x1E   ; 30
  b8:   81 50           subi    r24, 0x01   ; 1
    while(i>0)i--;
  ba:   f1 f7           brne    .-4         ; 0xb8 <__vector_1+0x14>
    clrb_GATE;
  bc:   d9 98           cbi 0x1b, 1 ; 27
}
  be:   8f 91           pop r24
  c0:   0f 90           pop r0
  c2:   0f be           out 0x3f, r0    ; 63
  c4:   0f 90           pop r0
  c6:   1f 90           pop r1
  c8:   18 95           reti

Mogę założyć, że kompilator domyśli się, że taki kod jest fałszywy i wycina go, ale dlaczego zachowuje ten sam kod na końcu kodu?

Czy są jakieś instrukcje kompilatora, aby zapobiec takiej optymalizacji?

Roman Matveev
źródło
1
Możesz także powiedzieć kompilatorowi, aby nie optymalizował jednej funkcji, być może warto wypróbować tę metodę z ISR. Zobacz to pytanie na temat przepływu stosu.
Vladimir Cravero
2
Hej Roman, dodałem tag „c” do twojego pytania usuwając atmega. Musiałem usunąć jeden tag, ponieważ istnieje limit (pięć), a kiedy zadajemy pytanie związane z kodem, dodanie nazwy języka jako tagu jest świetne, ponieważ cały kod (pytania i odpowiedzi) jest podświetlony.
Vladimir Cravero
5
Ogólnie rzecz biorąc, języki wyższego poziomu (jak C) są wyraźnie zaprojektowane, aby nie były powiązane relacją 1: 1 z ich wynikowym zestawem. Jeśli musisz liczyć instrukcje, aby uzyskać właściwy czas, zawsze musisz polegać na asemblerze (tak jak niektóre odpowiedzi). Istotą języków wysokiego poziomu jest to, że kompilator ma pewną swobodę, dzięki czemu Twój kod jest szybszy, silniejszy i lepszy niż wcześniej. Szczegóły, takie jak przydziały rejestrów i prognozy gałęzi, są o wiele lepiej pozostawione kompilatorowi ... z wyjątkiem takich chwil, w których ty, programista, dokładnie znasz potrzebne instrukcje.
Cort Ammon
5
Lepsze pytanie brzmi: dlaczego GCC nie optymalizuje obu tych pętli?
Ilmari Karonen,
5
Gcc najpierw rozwija pętlę, a dopiero potem zauważa, że ​​odpowiedni kod jest bezużyteczny. Przy rozmiarze pętli 30 rozwijanie byłoby głupie, a gcc tego nie robi. Na wyższym poziomie optymalizacji oba są optymalizowane od razu.
Marc Glisse,

Odpowiedzi:

9

Ponieważ w jednym komentarzu stwierdzasz, że „każdy takt procesora jest godny”, sugeruję użycie wbudowanego zestawu, aby opóźnienia były zapętlone tak, jak chcesz. To rozwiązanie jest lepsze od różnych volatilelub -O0ponieważ wyjaśnia , jakie masz zamiary.

unsigned char i = 10;
__asm__ volatile ( "loop: subi    %0, 0x01\n\t"
                   "      brne    loop"
                   : "+rm" (i)
                   : /* no inputs */
                   : /* no dirty registers to decleare*/);

To powinno wystarczyć. Zmienna rzecz polega na tym, aby powiedzieć kompilatorowi: „Wiem, że to nic nie robi, po prostu zachowaj to i zaufaj mi”. Trzy „instrukcje” asm są dość zrozumiałe, możesz użyć dowolnego rejestru zamiast r24, myślę, że kompilator lubi niższe rejestry, więc możesz chcieć użyć wysokiego. Po pierwszym :powinieneś wypisać zmienne wyjściowe (odczyt i zapis) c, a nie ma żadnych, po drugim :powinieneś wypisać zmienne wejściowe (ronly) c, znowu, nie ma żadnych, a trzecim parametrem jest lista oddzielonych przecinkami zmodyfikowanych rejestrów , w tym przypadku r24. Nie jestem pewien, czy powinieneś dołączyć także rejestr statusu, ponieważ ZEROflaga oczywiście się zmienia, nie uwzględniłem go.

edytuj zredagowaną odpowiedź zgodnie z żądaniem OP. Kilka notatek.

"+rm"Przed (i)środkami, które pozwalasz kompilator decydują się na miejsce, w m Emory lub w r egister. W większości przypadków to dobrze, ponieważ kompilator może lepiej zoptymalizować, jeśli jest bezpłatny. W twoim przypadku uważam, że chcesz zachować tylko ograniczenie r, aby zmusić i do bycia rejestrem.

Vladimir Cravero
źródło
Wygląda na to, że naprawdę tego potrzebuję. Ale czy możesz zmodyfikować swoją odpowiedź, aby zaakceptować dowolną czmienną zamiast literału 10, o którym wspomniałem w pierwotnej odpowiedzi? Próbuję przeczytać podręczniki GCC dotyczące właściwego wykorzystania konstrukcji asm, ale dla mnie jest to teraz trochę zaciemnione. Byłbym bardzo wdzięczny!
Roman Matveev
1
@RomanMatveev edytowany zgodnie z prośbą
Vladimir Cravero
13

Możesz spróbować zrobić pętlę. W tej chwili kompilator słusznie mówi: „Ta pętla nic nie robi - pozbędę się tego”.

Abyś mógł wypróbować konstrukt, którego często używam:

int i;
for (i = 0; i < 10; i++) {
    asm volatile ("nop");
}

Uwaga: nie wszystkie cele kompilatora gcc używają tej samej składni wbudowanej - może być konieczne dostosowanie go do celu.

Majenko
źródło
Twoje rozwiązanie wydaje się znacznie bardziej eleganckie niż moje ... może moje jest lepsze, gdy wymagana jest PRECYZYJNA liczba cykli? Mam na myśli, że nie można zagwarantować, że całość rzeczy zostanie skompilowana w określony sposób, prawda?
Vladimir Cravero
8
Prosty fakt, że używa C, oznacza, że ​​nie możesz zagwarantować cykli. Jeśli potrzebujesz dokładnego liczenia cykli, ASM to jedyna droga. Mam na myśli, że masz inną synchronizację z pętlą C, jeśli masz włączone pętle -funroll, niż jeśli nie, itp.
Majenko
Tak właśnie myślałem. Przy opóźnieniach sprzętowych przy wystarczająco wysokich wartościach i (100 lub więcej) wydaje mi się, że twoje rozwiązanie daje praktycznie takie same wyniki przy jednoczesnym zwiększeniu czytelności.
Vladimir Cravero
6

Tak, możesz to założyć. Jeśli zadeklarujesz zmienną i jako zmienną, powiesz kompilatorowi, aby nie optymalizował na i.

Ambiorix
źródło
1
Moim zdaniem nie jest to do końca prawda.
Vladimir Cravero
1
@VladimirCravero, co masz na myśli mówiąc to? Czy możesz to wyjaśnić?
Roman Matveev
2
Miałem na myśli to, że nie byłbym tak pewien, co robi kompilator. Deklaracja zmiennej lotnej mówi kompilatorowi, że może zmienić się gdzie indziej, więc naprawdę powinien to zrobić.
Vladimir Cravero
1
@Roman Matveev register unsigned char volatile i __asm__("r1");może?
a3f
2
Uznanie iza niestabilne rozwiązuje wszystko. Gwarantuje to norma C 5.1.2.3. Kompilator zgodny nie może optymalizować tych pętli, jeśli ijest niestabilny. Na szczęście GCC jest zgodnym kompilatorem. Niestety istnieje wiele niedoszłych kompilatorów C, które nie są zgodne ze standardem, ale nie ma to znaczenia dla tego konkretnego pytania. Absolutnie nie ma potrzeby wbudowanego asemblera.
Lundin
1

Po pierwszej pętli ijest stała. Inicjalizacja ii pętla robią tylko stałą wartość. Nic w tym standardzie nie określa, że ​​ta pętla musi być skompilowana w obecnej postaci. Standard nie mówi też nic o czasie. Skompilowany kod musi zachowywać się tak, jakby pętla była obecna i tak jest. Nie można wiarygodnie stwierdzić, że ta optymalizacja została przeprowadzona zgodnie ze standardem (czas nie ma znaczenia).

Druga pętla również powinna zostać usunięta. Uważam, że to błąd (lub brak optymalizacji), że tak nie jest. Po pętli ijest stała zero. Kod należy zastąpić ustawieniem ina zero.

Myślę, że GCC utrzymuje isię tylko z tego powodu, że może to mieć wpływ na (nieprzezroczysty) dostęp do portu i.

Posługiwać się

asm volatile ("nop");

oszukać GCC, aby uwierzył, że pętla coś robi.

usr
źródło