Skocz do zawartości

GCC -O3 optymalizacja - przyspieszenie przez spowolnienie


virtualny

Pomocna odpowiedź

Przyjrzałem się optymalizacji kompilatora GCC. Wydaje mi się że... prawie każdy bierze na wiarę optymalizację kodu bez "zaglądania pod maskę" - co dzisiaj ja postanowiłem uczynić.
Dla trywialnego przykładu:

for(int i = 0; i<5000000; i++);

Nie chodzi o to, co ma się dziać "wewnątrz" pętli for, chodzi tylko o to - jak może czy jak powinna ona wyglądać po kompilacji i konfrontacji z tym jak rzeczywiście wygląda.
W związku z "zachowaniem" kompilatora muszę zacząć od końca i pokazać wzorcowy, zoptymalizowany przykład.

Tak więc gdybym chciał taką prościutką pętlę, for (zliczającą od 0 do 5 milionów) optymalnie zaprogramować w assemblerze napisałbym coś takiego:

//FOR LOOP for(int i = 0; i<5000000; i++);

MOV    R0, #0         // i = 0
LDR      R1, =5000000

LP0:
ADD    R0, R0, #1     // i++
CMP    R0, R1         // i < 5000000?
BLE     LP0

Powyższa pętla jest dość dobrze zoptymalizowana, posiada także mimowolną wadę:

1. Pętla jest krótka, zmienna "i" jest w rejestrze R0, daje to natychmiastowy do niej dostęp, bez zbędnego tracenia cykli i zajętości procesora.

2. Należy także zauważyć, że na skutek konstrukcji pętli używany jest drugi rejestr (nie jest to w tym wypadku wielkim problemem), lecz także 5 milionów razy wykonywany jest rozkaz porównania dwóch rejestrów (R0 i R1).
Tak więc w tej pętli wykonuje się 5 MILIONÓW RAZY porównanie, które nie zawsze (to zależy od tego, co byłoby robione wewnątrz pętli) nusi być wykonywane i można by spróbować zrezygnować z tego porównania.

Aby to zrobić należy przekonstruować pętlę for do takiej postaci:

for(int i = #5000000; i != 0; i--);

Wówczas taką pętlę nakazującą odliczać procesorowi 5 milionów razy napisał bym tak:

//FOR LOOP for(int i = 5000000; i != 0; i--);

LDR      R0, = #5000000   ; i = 5000000

LP0:
SUBS    R0, R0, #1        ; i = i-1
BNE     LP0               ; i == 0?

Wyszła piękna pętla, zoptymalizowana, również odliczająca 5000000 razy, tyle że wstecz.
Dzięki temu pozbyliśmy się 5 milionów operacji wykonywanych w pętli... i jeszcze nie użyliśmy drugiego rejestru.

ALE...

Jeżeli ktokolwiek myśli że tej pętli nie da się zoptymalizować - to nie docenia "pomysłowości" kompilatora GCC 🙂

Więc teraz zaczynam kompilować program dla ARM CM3 w postaci:

(bez flagi optymalizacji "O")

arm-none-eabi-gcc.exe  -mcpu=cortex-m3 -g3  -DDEBUG -c main.c -o main.o 

Program wygląda tak:

void main(){
for(int i=0; i<5000000; i++);
}

Natomiast dla czytelności i przyszłych porównań zapiszę go w postaci:

void main(){
int i;
for(i=0; i<5000000; i++);
}

skompilowanie i zapisanie do elfa, oraz deassemblacja została wykonana za pomocą poleceń:

arm-none-eabi-ld.exe  main.o -o program.elf
arm-none-eabi-objdump.exe  program.elf  -d --source > deas.txt

Oto jak wygląda deassemblacja:

program.elf:     file format elf32-littlearm


Disassembly of section .text:

00008000 <main>:

void main(){
    8000:    b480          push    {r7}                        // zachowanie wartości rejestru R7, który zostanie użyty jako wskaźnik do zmiennej na stosie
    8002:    b083          sub    sp, #12                      // dodaj miejsce na stosie
    8004:    af00          add    r7, sp, #0                   //  R7 - wskaźnik do zmiennej
for(int i=0; i<5000000; i++);
    8006:    2300          movs    r3, #0                      // i = 0
    8008:    607b          str    r3, [r7, #4]                 // zapisz daną na stosie
    800a:    e002          b.n    8012 <main+0x12>             // rozpocznij pętlę od porównania zmiennej "i" z wartością końcową (5000000)

//==================================================
    800c:    687b          ldr    r3, [r7, #4]                 // odczyta danej "i" ze stosu
    800e:    3301          adds    r3, #1                      // inkrementacja
    8010:    607b          str    r3, [r7, #4]                 // zapis zmiennej na stos
//---
    8012:    687b          ldr    r3, [r7, #4]                 //  !!! ponowny odczyt zmiennej ze stosu !!! PO CO???????
    8014:    4a04          ldr    r2, [pc, #16]                // !!!!! PO CO 5000000 razy ładować to do rejestru, którego wartość się nie zmienia??? ; (8028 <main+0x28>)
    8016:    4293          cmp    r3, r2                       // porównanie -  czy "i" = 5000000?
    8018:    ddf8          ble.n    800c <main+0xc>            // Jeżeli "i" < 5000000 - kontynuuj pętlę
//===================================================

    801a:    bf00          nop                                 // ??? PO CO???
    801c:    bf00          nop                                 // ??? PO CO???
    801e:    370c          adds    r7, #12                     // odtworzenie wartości początkowej stosu
    8020:    46bd          mov    sp, r7                       // przesłanie wartości początkowej stosu do rejestru wskaźnika stosu (SP = stack pointer)
    8022:    bc80          pop    {r7}                         // odtworzenie rejestru R7 do wartości sprzed wykonywania pętli "for"
    8024:    4770          bx    lr                            // wyjście z funkcji (main) do miejsca w pliku startup, który funkcję main wywołał (BL main)
//===
    8026:    bf00          nop                                 // wyrównanie do 4 (ALIGN 4)
    8028:    004c4b3f     .word    0x004c4b3f                  // 5000000 - wartość końcowa licznika/zmiennej "i" w pętli "for"

TRAGEDIA POD KAŻDYM WZGLĘDEM. 
- Oprócz rozmiaru kodu, 
- zbędne operacje umieszczania danej/zmiennej "i" na stosie, 
- każdorazowego odczytu i zapisu danej na stosie (rejestr R3), 
- ponownego odczytu zmiennej "i" ze stosu, pomimo że jest już w rejestrze!,
- i jeszcze każdorazowego ładowania do rejestru R2 wartości końcowej.

Naprawdę jeżeli ktoś zna assembler i zobaczy taki kod, wie od razu że jeżeli pisał to człowiek, to musiał być bardzo nieudolny i słabo znać zasady programowania, optymalizacji czy architekturę mikroprocesora.
Albo... założy że "konstruował to" kompilator.


Dobrze - teraz zajmę się opcją optymalizacji, która jest optymalizacją najsłabszego rzędu - a są to opcje (równoważne) "O", lub "O1":

arm-none-eabi-gcc.exe  -mcpu=cortex-m3 -g3 -O1 -DDEBUG -c main.c -o main.o 

Oto wynik!

void main(){
    8000:    4b01          ldr    r3, [pc, #4]    ; (8008 <main+0x8>)
int i;
for(i=0; i<5000000; i++);
    8002:    3b01          subs    r3, #1        // PROGRAMOWANA BYŁA INKREMENTACJA, A NIE DEKREMENTACJA !!!!!
    8004:    d1fd          bne.n    8002 <main+0x2>
    8006:    4770          bx    lr
    8008:    004c4b40     .word    0x004c4b40

Teraz należy zauważyć 2 rzeczy:
1. Jest to najsłabsza optymalizacja ("O" lub "O1").
2. Kompilator nie wykonał optymalizacji, czy kodu jaki zapisałem [for(i=0; i<5000000; i++);] pod 8002 jest dekrementacja (subs r3, #1) - czyli kompilator skomponował pętlę:

for(int i = #5000000; i != 0; i--);

Tak więc kompilator dokonał "samowolki" o którą go nie prosiłem. Być może w tym konkretnym przykładzie nie ma to wielkiego znaczenia, ale może mieć ogromne w innych... Niemniej doceniam, że kompilator w ogóle to naprawdę zoptymalizował.


Teraz pamiętajmy że "O1" jest to najsłabsza optymalizacja - tak więc strach pomyśleć, jak będzie wyglądać optymalizacja jeszcze SILNIEJSZA "O2" czy "O3" !!!


"Pomysłowość" kompilatora nie zawodzi - faktycznie udało mu się "zoptymalizować" pętlę (tak samo wygląda to dla flagi "O2" i "O3"):

void main(){
int i;
for(i=0; i<5000000; i++);
    8000:    4770          bx    lr
    8002:    bf00          nop

Optymalizacja polegała na usunięciu zmiennej, a skoro nie ma zmiennej, to i nie ma pętli - zatem w najwyższą formą optymalizacji kodu jest jego BRAK!!!

My oczywiście zaprawieni w bojach, koderzy nie damy się tak łatwo oszukać kompilatorowi i również oszukamy kompilator!
Mianowicie zmienną "i" zadeklarujemy jako VOLATILE !!!

volatile int i;

Po takim zabiegu kompilator nie miał wyboru i musiał zauważyć, że zmienna "i" naprawdę istnieje, a tym samym istnieje pętla "for" !

Gdybym miał wypisywać listę moich skarg na jakość skompilowanego kodu - a przypomnijmy że jest to niby najbardziej "agresywna" forma optymalizacji "O3" - to byłaby ona niewiele krótsza od najgorszej opcji bez optymalizacji opisanej tutaj jako "TRAGEDIA POD KAŻDYM WZGLĘDEM".

Oto kod wynikowy dla optymalizacji "O2" lub "O3":

void main(){
volatile int i;
for(i=0; i<5000000; i++);
    8000:    2300          movs    r3, #0                         // R3 = 0
void main(){
    8002:    b082          sub    sp, #8                          // zmiana wskaźnika stosu dla zmiennej - w jaki celu o 8 bajtów dla zmiennej 4 bajtowej ?
for(i=0; i<5000000; i++);
    8004:    9301          str    r3, [sp, #4]
    8006:    4a06          ldr    r2, [pc, #24]                   //; (8020 <main+0x20>) R2 = 5000000

    8008:    9b01          ldr    r3, [sp, #4]                    //1. PO CO ??? można usunąć 1-2-3 - będzie działać
    800a:    4293          cmp    r3, r2                          //2. PO CO ??? można usunąć 1-2-3 - będzie działać
    800c:    dc05          bgt.n    LP1                           //3. PO CO ??? można usunąć 1-2-3 - będzie działać
//==========================================
LP0:
    800e:    9b01          ldr      r3, [sp, #4]                   //4. można usunąć 4 razem z 5i6 - będzie działać
    8010:    3301          adds     r3, #1
    8012:    9301          str      r3, [sp, #4]                   //5. można usunąć 4 razem z 5i6 - będzie działać
    8014:    9b01          ldr      r3, [sp, #4]                   //6. PO CO??? można usunąć
    8016:    4293          cmp      r3, r2
    8018:    ddf9          ble.n    LP0
//==========================================
LP1:
    801a:    b002          add    sp, #8                           // odtworzenie sp
    801c:    4770          bx    lr                                // exit
    801e:    bf00          nop                                     // ALIGN (4)
    8020:    004c4b3f     .word    0x004c4b3f                      // 4 999 999

Lista skarg:


TRAGEDIA... 
- Oprócz rozmiaru kodu, 
- zbędne operacje umieszczania danej/zmiennej "i" na stosie, - to nie ja chciałem, żeby zmienna była VOLATILE - to kompilator i poziom "optymalizacji" zmusił mnie, aby w taki sposób zadeklarować zmienną - inaczej pętli nie byłoby wcale!
- każdorazowego odczytu i zapisu danej na stosie (rejestr R3), 
- ponownego odczytu zmiennej "i" ze stosu, pomimo że jest już w rejestrze!

- wartość końcowa w R2 załadowana wcześniej i nie jest reloadowana za każdym razem - jest czym się zachwycać ???
 
Sumując w tym przypadku najwyższe stopnie optymalizacji są niewiele lepsze od najgorszego przypadku bez optymalizacji i o wiele gorsze od optymalizacji najsłabszej - jeżeli nie liczyć samowolki kompilatora ze zredefiniowaniem pętli for na dekrementację...
Czy zastanawiałeś się jakie skutki w kodzie i jego działaniu implikuje optymalizacja?

  • Nie zgadzam się! 1
Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

Od siebie dodam, że w duże firmy jak Google posiadają swoje własne forki kompilatorów. Chodzi o to że współcześnie, optymalizacje tworzy się nie jako wstawki asemblerowe, a jako patche do kompilatora. Więc o ile z jednego poziomu wywalenie całej pętli przy wydaje się bez sensu, to z innego jest jak najbardziej sensowne. I to ten bardziej abstrakcyjny punkt widzenia obecnie jest na topie w kwestiach optymalizacji.

Link do komentarza
Share on other sites

Dnia 22.10.2023 o 01:16, sebas86 napisał:

Przepraszam, że odkopuję ale popełniasz kilka fundamentalnych błędów.

[...]

  • błędnie zakładasz, że mniej kodu zawsze wykonuje się szybciej,

[...[

 

Nie chciałbym wchodzić w dogłębną polemikę, która zapewne ośmieszyłaby nas dwóch, ale takie "zakładanie że ja błędnie zakładam" , to jak wróżenie z fusów i przedstawianie tego jako dowód naukowy. Zapewne w wielu miejscach nie znam "filozofii" kompilatora, która być może mnie przerasta, być może to tylko budowanie okopów, aby "mój był lepszy"... Zakładam że rozumiesz mój skrót myślowy ("mój był lepszy") i nie muszę się spodziewać jakiejś riposty z nim związanej.

Jeżeli chodzi o optymalizację kompilowanego kodu, to zapraszam do wątku gdzie zmusiłem bluepill'a do szybkiej obsługi interfejsu i8080 16bit, żeby obsługa (szybkość) ekranu nie była tylko z nazwy. Do tego posłużył mi assembler, ale jeżeli jesteś w stanie zrobić to samo za pomocą "gołego C", to ja chętnie to użyję zamiast tworzyć funkcje w assemblerze.

OK, teraz do sedna - Twoje założenie że ja błędnie zakładam... A więc załóżmy taki kod:

 

uint32_t var1[8];

uint32_t i;

for(i=0; i<8; i++) {
  var1[i] = 0;
}

 

Jego odpowiednik w assemblerze może wyglądać na przykład tak:

LDR R0, = VAR1 ; R0 = VARIABLE ADDRESS
MOV R1, #0     ; R1 = STORE OFFSET
MOV R2, R1     ; R2 = 0 -> FILL VALUE
MOV R3, #32    ; R3 = OFFSET COMPARE VALUE

LP0:
STR R2 [R0, R1] ; STORE FILL VALUE FRPM R2 INTO THE VARIABLE POINTERED BY R0+R1

;==================================================================
;= BELOW THIS 3 OPCODES MAKE THIS LOOP SMALLER BUT SLOWER RIGHT ? =
;==================================================================
ADD R1, R1, #4  ; OFFSET INCREMENTATION
CMP R1, R3      ; COMPARE OFFSET VALUE TO END OF THE VARIABLE
BNE LP0

 

I do czegoś podobnego przetworzy to kompilator i... jest to właśnie wersja wolniejsza, ale krótsza.

Teraz udowodnię Tobie, że rozumiem, iż długość wygenerowanego kodu nie musi oznaczać, że jest on wolniejszy, co obala Twoje "błędne założenia", że ja "błędnie zakładam".

Tak więc oto corpus delicti - dłuższy kod, ale szybszy:

 

LDR R0, = VAR1 ; R0 = VARIABLE ADDRESS
MOV R1, #0     ; R1 = 0 -> FILL VALUE

;====================================================================
;= BELOW 8 OPCODES UNROLLING LOOP TO LONGER BUT FASTER CODE RIGHT ? =
;====================================================================

STR R1 [R0, #0]
STR R1 [R0, #4]
STR R1 [R0, #8]
STR R1 [R0, #12]
STR R1 [R0, #16]
STR R1 [R0, #20]
STR R1 [R0, #24]
STR R1 [R0, #28]

 

 

Nie chcę dyskutować o pozostałych Twoich tezach pisanych w trybie dokonanym. Kod który kompilator generuje nie dla mnie tylko dla procesora jest dla mnie bezwartościowy, jeżeli 512 vectordotów biega wolniej jak 70HZ i widać na ekranie jak przegania je raster (szarpanie). W mojej ocenie jednakże ten kod jest dla mnie. Oczywiście możemy się tu przez kilkaset kilobajtów tekstu dalej bezsensownie spierać dla kogo jest kod wygenerowany przez kompilator.

 

Także pozwolę sobie nie polegać na "opinii" kompilatora, który wycina kompilację fragmentów kodu, które według reguł kompilatora miałyby nic nie robić, a sam przykład był właśnie po to, aby sprawdzić czego można się spodziewać po kompilatorze. Zapewne ludzie tacy jak Ty, którzy potrafią o wiele lepiej ode mnie "dogadać się z kompilatorem" mają o wiele mniej pracy nad pisaniem swoich programów - ja naprawdę to rozumiem i doceniam bez cienia sarkazmu.

 

Powtórzę przykład, który również sporządziłem w celu sprawdzenia "czego się można spodziewać" - jest to dyrektywa switch-case:

 

   switch(lcdprop.pFont->Height){
 8003870:    4ba0          ldr    r3, [pc, #640]    ; (8003af4 <Draw_Char+0x2c0>)
 8003872:    689b          ldr    r3, [r3, #8]
 8003874:    88db          ldrh    r3, [r3, #6]
 8003876:    3b08          subs    r3, #8
 8003878:    2b10          cmp    r3, #16
 800387a:    d834          bhi.n    80038e6 <Draw_Char+0xb2>
 800387c:    a201          add    r2, pc, #4    ; (adr r2, 8003884 <Draw_Char+0x50>)
 800387e:    f852 f023     ldr.w    pc, [r2, r3, lsl #2]
 8003882:    bf00          nop
 8003884:    080038c9     .word    0x080038c9
 8003888:    080038e7     .word    0x080038e7
 800388c:    080038e7     .word    0x080038e7
 8003890:    080038e7     .word    0x080038e7
 8003894:    080038cf     .word    0x080038cf
 8003898:    080038e7     .word    0x080038e7
 800389c:    080038e7     .word    0x080038e7
 80038a0:    080038e7     .word    0x080038e7
 80038a4:    080038d5     .word    0x080038d5
 80038a8:    080038e7     .word    0x080038e7
 80038ac:    080038e7     .word    0x080038e7
 80038b0:    080038e7     .word    0x080038e7
 80038b4:    080038db     .word    0x080038db
 80038b8:    080038e7     .word    0x080038e7
 80038bc:    080038e7     .word    0x080038e7
 80038c0:    080038e7     .word    0x080038e7
 80038c4:    080038e1     .word    0x080038e1
    case 8:
        fontsize = 8;
 80038c8:    2308          movs    r3, #8
 80038ca:    623b          str    r3, [r7, #32]
        break;
 80038cc:    e00b          b.n    80038e6 <Draw_Char+0xb2>
    case 12:
        fontsize = 12;
 80038ce:    230c          movs    r3, #12
 80038d0:    623b          str    r3, [r7, #32]
        break;
 80038d2:    e008          b.n    80038e6 <Draw_Char+0xb2>
    case 16:
        fontsize = 32;
 80038d4:    2320          movs    r3, #32
 80038d6:    623b          str    r3, [r7, #32]
        break;
 80038d8:    e005          b.n    80038e6 <Draw_Char+0xb2>
    case 20:
        fontsize = 40;
 80038da:    2328          movs    r3, #40    ; 0x28
 80038dc:    623b          str    r3, [r7, #32]
        break;
 80038de:    e002          b.n    80038e6 <Draw_Char+0xb2>
    case 24:
        fontsize = 72;
 80038e0:    2348          movs    r3, #72    ; 0x48
 80038e2:    623b          str    r3, [r7, #32]
        break;
 80038e4:    bf00          nop
    }

    fontoffset = ((s - ' ') * fontsize);
 80038e6:    78fb          ldrb    r3, [r7, #3]
 80038e8:    3b20          subs    r3, #32
 80038ea:    461a          mov    r2, r3
 80038ec:    6a3b          ldr    r3, [r7, #32]

 

Moim celem było zwrócenie uwagi na racjonalne używanie tej dyrektywy, jeżeli spojrzeć na nadmiarowość generowanego kodu. Zakładam że Ty jako człowiek obeznany z kompilatorem wiesz że taka redundancja może wystąpić podczas używania tej dyrektywy. Oczywiście można pokazać bardziej drastyczny przypadek switch-case, gdzie nadmiarowość będzie w kilobajtach (np. dla case = 32768). Prawdopodobnie Ty odpowiesz, że trzeba wiedzieć co się robi, i co implikuje użycie takich komend.  Z kolei ja jestem przekonany że ludzi tak dobrze znających kompilator, jego reguły i "filozofię" jest o wiele mniej niż 1 z 10. To oznacza, że ponad 9 z 10 ludzi programujących w C dojdzie do wniosku jeżeli błędnie użyje dyrektywy switch-case, że należy wymienić procesor na lepszy/szybszy/z większą ilością pamięci...

 

 

Link do komentarza
Share on other sites

Nie negowałem potrzeby używania asemblera, w programowaniu µC to nawet normalne, że nadal się z niego korzysta. Neguję wartość merytoryczną pierwszego wpisu. Na przykładzie, który podałeś wykazałeś nie to, że czasami warto albo wręcz trzeba użyć ręcznie napisanych wstawek, wykazałeś, że nie wiesz dlaczego kompilator konkretnego języka generuje kod tak, a nie inaczej.

I tak, nawet producenci kompilatorów są świadomi ograniczeń, z tego powodu dostarczone są narzędzia i dokumentacja opisująca jak poprawnie pisać wstawki i jak mieszać ręcznie napisany kod maszynowy z kodem wygenerowanym przez kompilator. Np. w bibliotece standardowej jest sporo kodu, który ktoś ręcznie napisał w asemblerze... Ale i też często nie ma sensu uciekać aż tak nisko, trzeba się nauczyć poprawnie korzystać z narzędzia.

Co do reszty. I super! Tylko dlaczego nie wykorzystałeś tego doświadczenia jako przykład do postu? Byłoby konkretniej i na rzeczywistym przykładzie.

  • Lubię! 1
Link do komentarza
Share on other sites

I mała podpowiedź, jeśli nadal nie sprawdziłeś co robi volatile i dlaczego nie działa tak jak tego oczekujesz, to służę lepszym przykładem na testowanie niemal surowej pętli:

void test()
{
    for(int i = 500000; i > 0; i--)
        asm volatile ("nop");
}

Kod wynikowy z włączonymi optymalizacjami (niezależnie czy będzie to 1, 2, 3 czy S, przetestowane używając https://godbolt.org/  na ARM GCC 13.2.0)

test:
        ldr     r3, .L4
.L2:
        nop
        subs    r3, r3, #1
        bne     .L2
        bx      lr
.L4:
        .word   500000

Więcej informacji:

Edytowano przez sebas86
Link do komentarza
Share on other sites

1 godzinę temu, sebas86 napisał:

I mała podpowiedź, jeśli nadal nie sprawdziłeś co robi volatile i dlaczego nie działa tak jak tego oczekujesz, to służę lepszym przykładem na testowanie niemal surowej pętli:

[...]

 

Chyba kolejne nieporozumienie jak z Twoim założeniem mojego  niezrozumienia zależności długość kodu vs speed- nigdzie nie stwierdziłem że nie wiem co robi volatile , nigdzie nie twierdziłem ja że błędnie jej używam (Ty wcześniej tak stwierdziłeś). Stwierdziłem natomiast że musiałem użyć zmiennej jako volatile, żeby optymalizacja kompilatora nie wycinała zmiennej, bez której nie ma końcowo pętli. 

Dnia 23.09.2023 o 20:10, virtualny napisał:

 

Lista skarg:


TRAGEDIA... 
- Oprócz rozmiaru kodu, 
- zbędne operacje umieszczania danej/zmiennej "i" na stosie, - to nie ja chciałem, żeby zmienna była VOLATILE - to kompilator i poziom "optymalizacji" zmusił mnie, aby w taki sposób zadeklarować zmienną - inaczej pętli nie byłoby wcale!
- każdorazowego odczytu i zapisu danej na stosie (rejestr R3), 
- ponownego odczytu zmiennej "i" ze stosu, pomimo że jest już w rejestrze!

- wartość końcowa w R2 załadowana wcześniej i nie jest reloadowana za każdym razem - jest czym się zachwycać ???
 
Sumując w tym przypadku najwyższe stopnie optymalizacji są niewiele lepsze od najgorszego przypadku bez optymalizacji i o wiele gorsze od optymalizacji najsłabszej - jeżeli nie liczyć samowolki kompilatora ze zredefiniowaniem pętli for na dekrementację...
Czy zastanawiałeś się jakie skutki w kodzie i jego działaniu implikuje optymalizacja?

I przy okazji - dla mnie ma znaczenie, czy kod jest kompilowany 1:1 w sensie inkrementacji czy dekrementacji pętli for, bo chcę przykładowo czyścić framebuffer, i chcę go czyścić od góry do dołu. W przypadku samowolki kompilatora, bez deassemblacji kodu wynikowego będę mylnie zakładał, że mam błędnie o 180 stopni wybraną orientację screena. Tak tylko przykładowo.

Link do komentarza
Share on other sites

Skoro wiesz jak działa to dlaczego się dziwisz, że kompilator wstawia po kilka razy odczyt lub zapis zmiennej? Dlaczego uważasz, że to błąd, skoro to po prostu efekt uboczny stosowania słowa kluczowego oraz innych elementów języka? Nawrzucałeś całą masę nieprawdziwych tez i potencjalnie wprowadzasz w błąd nowych programistów C, do tego napisałeś to tonem jakbyś pozjadał wszystkie rozumy, a przynajmniej wiedział lepiej od autorów kompilatora jak kompilator powinien działać. Wrzuciłem Ci nawet przykład gdzie kompilator, pomijając operację nop, wygenerował dokładnie taką samą pętlę – kompilator ma całą masę reguł, która pozwala generować optymalny kod w takich przypadkach, aby z tego skorzystać trzeba jednak wiedzy.

Może zamiast brnąć dalej po prostu skorzystaj z okazji i naucz się czegoś nowego, nie wprowadzaj też innych w błąd.

Co do przykładów to kompilator nawet sam lepiej ogarnia typowe wzorce, wygeneruje tak samo optymalny kod dla takiego zapisu

for(int i = 0; i < 500000; i++)
  asm volatile ("nop");

I na koniec zacytuję Ciebie:

Cytat

TRAGEDIA POD KAŻDYM WZGLĘDEM. 

Cytat

"Pomysłowość" kompilatora nie zawodzi

Cytat

Sumując w tym przypadku najwyższe stopnie optymalizacji są niewiele lepsze od najgorszego przypadku bez optymalizacji i o wiele gorsze od optymalizacji najsłabszej - jeżeli nie liczyć samowolki kompilatora ze zredefiniowaniem pętli for na dekrementację...
Czy zastanawiałeś się jakie skutki w kodzie i jego działaniu implikuje optymalizacja?

Trochę więcej pokory, ale i szacunku do autorów narzędzi, których nie potrafisz jeszcze dobrze użyć, a już ironizujesz i próbujesz udowodnić, że wiesz lepiej jak powinny działać...

  • Lubię! 1
Link do komentarza
Share on other sites

Cytat

I przy okazji - dla mnie ma znaczenie, czy kod jest kompilowany 1:1 w sensie inkrementacji czy dekrementacji pętli for, bo chcę przykładowo czyścić framebuffer, i chcę go czyścić od góry do dołu. W przypadku samowolki kompilatora, bez deassemblacji kodu wynikowego będę mylnie zakładał, że mam błędnie o 180 stopni wybraną orientację screena. Tak tylko przykładowo.

To jest kolejne błędne założenie. Kompilator stosuje dowolność tam gdzie efekt końcowy jest taki sam lub standard nie definiuje zachowania (albo definiuje wprost jako niezdefiniowane zachowanie), gdybyś użył zmiennej do czegoś innego niż odliczanie to kompilator się dostosuje do kontekstu. W użytym przez Ciebie przykładzie zmienna „i” jest używana tylko do sterowania pętlą, nie ma znaczenia czy będzie zliczała od góry w dół, czy od dołu w górę, czy warunek końca to  „i < 5” czy „i <= 4”, to jest typowy wzorzec powtórzenia operacji n razy, można to zrobić na kilka sposobów bez wpływu na wynik końcowy.

volatile int reg;

void test()
{
    for(int i = 0; i < 500000; i++)
        reg = i;
}

Wynik:

test():
        movs    r3, #0
        ldr     r1, .L4
        ldr     r2, .L4+4
.L2:
        str     r3, [r1]
        adds    r3, r3, #1
        cmp     r3, r2
        bne     .L2
        bx      lr
.L4:
        .word   .LANCHOR0
        .word   500000
reg:
        .space  4

Zmieniona kolejność:

volatile int reg;

void test()
{
    for(int i = 500000; i >= 0; i--)
        reg = i;
}

Wynik:

test():
        ldr     r3, .L4
        ldr     r2, .L4+4
.L2:
        str     r3, [r2]
        subs    r3, r3, #1
        cmp     r3, #-1
        bne     .L2
        bx      lr
.L4:
        .word   500000
        .word   .LANCHOR0
reg:
        .space  4

Jeśli chcesz miarodajnych wyników, testuj prawdziwy kod, kod który chcesz optymalizować, nie jakieś syntetyczne i pozbawione kontekstu wyrywki.

  • Lubię! 1
Link do komentarza
Share on other sites

Kontekst tego wątku i moich testów jest taki, że 95% (lub więcej) niby programistów embedded nie ma zielonego pojęcia o programowaniu i o rzeczach jaki Ty tutaj piszesz. Ponownie potwierdzam że szanuję ludzi jak Ty czy trainee, którzy wiedzą o czym piszą i podejmują polemikę. Ludzie na Waszym poziomie spokojnie sobie poradzą.

Teraz wspomnę o tym jaki poziom reprezentuje owe 95% programistów, żeby mieć jasność. Zaznaczę, że czytałem taki post w czasach gdy nie miałem zielonego pojęcia o programowaniu w C. Nota bene jest to w zasadzie „dyżurny” post naszych „speców” od embedded, a brzmi on mniej więcej tak:

 

„Próbowałem to zaprogramować HAL’em za pomocą struktur, ale w żaden sposób nie chciało mi to działać. Dopiero mi to ruszyło, gdy zaprogramowałem to przez porty.”

 

No i teraz – gdy czytałem to jako kompletny ignorant, ten post i człowiek zrobił na mnie piorunujące wrażenie, które skrótowo zredaguję:

„KURDE CO ZA WIELKI SZPEC!!! CO ZA GENIUSZ!!! WBREW SYSTEMOWI, NIE CHCIAŁO MU SIĘ ZAPROGRAMOWAĆ, ALE ON ZNALAZŁ JAKIEŚ (BYĆ MOŻE NAWET I NIELEGALNE CZY NIEUDOKUMENTOWANE !!!) OBEJŚCIE! PROSZĘ BARDZO BIBLIOTEKI NIE POTRAFIĄ, ALE ON JAK JANOSIK CZY ROBINHOOD POTRAFI!!! MISTRZOSTWO ŚWIATA – GDZIE MI DO TAKICH GENIUSZY!!!”

 

Absolutnie serio piszę o mojej reakcji. Kiedy zacząłem uczyć się C, wykupiłem kilka kursów, w jednym z tych kursów powyższy problem błędów podczas programowania rejestrów przez struktury został dogłębnie omówiony i wyjaśnione zostało, gdzie tkwi błąd i jak się go ustrzec. Gro „embedziarzy” po prostu nie ma zielonego pojęcia co robi kompilator i jak z nim się porozumieć. Wszystko odbywa się na zasadzie kopiuj/wklej i przepisywania formułek jako rzekome linie kodu w IDE. Dlatego właśnie moja sugestia brzmi: Embedziarzu miej pojęcie co się dzieje z twoim kodem. Miej pojęcie co robisz i jakie to rodzi implikacje. Na przykład takie jak podczas optymalizacji, która bywa… zagadkowa.

 

 

 

 

Link do komentarza
Share on other sites

Dołącz do dyskusji, napisz odpowiedź!

Jeśli masz już konto to zaloguj się teraz, aby opublikować wiadomość jako Ty. Możesz też napisać teraz i zarejestrować się później.
Uwaga: wgrywanie zdjęć i załączników dostępne jest po zalogowaniu!

Anonim
Dołącz do dyskusji! Kliknij i zacznij pisać...

×   Wklejony jako tekst z formatowaniem.   Przywróć formatowanie

  Dozwolonych jest tylko 75 emoji.

×   Twój link będzie automatycznie osadzony.   Wyświetlać jako link

×   Twoja poprzednia zawartość została przywrócona.   Wyczyść edytor

×   Nie możesz wkleić zdjęć bezpośrednio. Prześlij lub wstaw obrazy z adresu URL.

×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.