Gdzie deklarować zmienne wykorzystywane w pętlach

0

Witam,

czy .NET ma jakąś ciekawą metodę na ograniczenie zużycia pamięci (głównie chodzi o kompilator).

Przypuśćmy, że mamy pętlę

 
// 1 przypadek
while(warunek)
{
int i = length(jakas zmienna);
i++;
}

// 2 przypadek

int i;
while(warunek)
{
i = length(jakas zmienna);
i++
}

co będzie efektywniejsze w końcowym wyniku?

0

Najprawdopodobniej kompilator sprowadzi obie wersje do jednego kodu.

0

właśnie nie...

w ogóle ten przykład jest bez sensu...

while(warunek)
{
int i = length(jakas zmienna);
i++;
}

po zakończeniu jednego taktu(o ile mogę to tak nazwać) pętli zmienna i zostanie usunięta.
Czyli na następnym powtórzeniu zostanie stworzona od nowa.A po zakończeniu pętli już jej nie będzie.

 
int i;
while(warunek)
{
i = length(jakas zmienna);
i++
}

a ta zmienna dopiero po skończeniu się programu

Jak widzisz to już zasadnicza różnica.

Także pytasz nas która jest szybsza sam możesz to sprawdzić.
Ale ja bym stawiał na przykład 2. Ponieważ komputer co pętle musi wykonać aż trzy czynności a z przykładem 2 tylko dwie.

0
konserwa napisał(a)

po zakończeniu jednego taktu(o ile mogę to tak nazwać) pętli zmienna i zostanie usunięta.
Czyli na następnym powtórzeniu zostanie stworzona od nowa.A po zakończeniu pętli już jej nie będzie.
(...)
a ta zmienna dopiero po skończeniu się programu

a g**** prawda.
wlacz Visual studio, napisz taki kodzik, odpal w trybie Debug, nastepnie postaw breakpointa w petli tuz-po "stworzeniu" zmiennej, F10 niech jej wartosc sie ustali na zadana, nastepnie wy-F10-uj sie i wyjdz z petli, o i teraz zmienna zniknela tak? debugger juz jej nie pokazuje? super. to teraz kliknij gdzies w petli i prawoklik i wybierz "set next statement", i obejrzyj zmienna. i sie zdziwisz, bo sie okaze ze owo "i" pamieta swoja wartosc. z tego co pamietam release zachowuje sie tak samo, tylko moze byc ciezej to dostrzec, bo on lubi calkiem usuwac niepotrzebne zmienne ze stosu metody i wtedy podglad wartosci na breakpoincie nie dziala.

btw. wiekszosc sensownych kompilatorow, nie tylko C# ale i innych jak C/C++ czy jacy, tak sie zachowuje, poniewaz merdanie stosem +-kilka bajtow jest bezsensu. co najwyzej wywolania konstruktorow/destruktorow sa opoznione az do wlasciwego punktu --- ale w przypadku zmiennych o prostych typach jak int, rozwazanie 'kon/destrukcji' jest bez sensu.

kompilator C#, przynajmniej ten z windows, nie wiem jak mono, ma te smieszna ceche, ze w sporej ilosci przypadkow, przynajmniej poki jeszcze ogladamy IL, rezerwuje "miejsce" na wszystkie zmienne lokalne na samym poczatku funkcji. zadne zmienne nie sa w trakcie pracy lokalnego kodu metody 'tworzone' albo 'niszczone'. calosc miejsca jest rezerwowana raz i tylko raz.

---- ale nie zmienia to faktu, ze zmiennym nalezy tak dobierac zakres widocznosci, aby byl pozwalal zrobic to do czego sluza, i ani ciut wiekszy - gdyz pozwala to uniknac czasem kilku typow bledow ktore lubia sie pojawiac gdy co druga iterator petli nazywasz "for(int i=" albo j/x/y/z zamiast "pełniej". o frazie using(..){} odpowiajacej stylowi RAII tez warto przypomniec w tym temacie, acz to tylko do pilnowania scope'a idisposable:)

-- i nie zmienia to faktu, ze to co teraz piszę, nie jest namawianiem do ciągłego new XYZ()'owania tymczasowych rzeczy wewnatrz pętel. ciężkie akcje które da się zaoszczedzic, nalezy wyciagac poza petle z prostej oszczednosci czasu pracy, ma to zwiazek z raczej innymi rzeczami niz "tworzenia" int'a do krecenia sobie petla

0

No to sorka nie mogłem tego sprawdzić(na laptopie nie mam kompilatorów) , nie zmienia to faktu ze dla ciebie jakby te "i" nie istnieje poza pętlą.
wiec jak pisał ktoś tam wyżej, zależy co chcesz zrobić.

1

co będzie efektywniejsze w końcowym wyniku?

taki kod

    static bool JakisWarunek()
    {
        return "asd".Length==3;
    }

    static void test()
    {
        string s = "ala ma kota";
        // 1 przypadek
        while (JakisWarunek())
        {
            int i = s.Length;
            i++;
        }

        // 2 przypadek

        int j;
        while (JakisWarunek())
        {
            j = s.Length;
            j++;
        }

    }

kompiluje się do czegoś takiego (komentarze moje)

00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        esi  
00000004  mov         esi,dword ptr ds:[02C72088h] ; string s = "ala ma kota";

0000000a  call        dword ptr ds:[00149C54h] ; JakisWarunek()
00000010  test        eax,eax 
00000012  je          00000024 
00000014  cmp         dword ptr [esi+8],eax ; WTF
00000017  cmp         dword ptr [esi+8],eax ; WTF
0000001a  call        dword ptr ds:[00149C54h] ; JakisWarunek()
00000020  test        eax,eax 
00000022  jne         00000017
 
00000024  call        dword ptr ds:[00149C54h] ; JakisWarunek()
0000002a  test        eax,eax 
0000002c  je          0000003E
0000002e  cmp         dword ptr [esi+8],eax ; WTF
00000031  cmp         dword ptr [esi+8],eax ; WTF
00000034  call        dword ptr ds:[00149C54h]  ; JakisWarunek()
0000003a  test        eax,eax 
0000003c  jne         00000031

0000003e  pop         esi  
0000003f  pop         ebp  
00000040  ret  

widzimy, że obie zmienne jako nieistotne dla działania programu wyleciały, są tu jakieś nic nie robiące instrukcje cmp, będące „wywołaniem” metody Length(), tak czy inaczej obie pętle wyglądają identycznie.

wniosek: obie zmienne nie istnieją, bo optymalizator je (nie do końca) wyciął. Ale dodajmy tam wyświetlanie:

        while (JakisWarunek())
        {
            int i = s.Length;
            i++;
            Console.WriteLine(i);
        }

        // 2 przypadek

        int j;
        while (JakisWarunek())
        {
            j = s.Length;
            j++;
            Console.WriteLine(j);
        }

o, teraz mamy pełne pętle:

00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi
00000005  mov         edi,dword ptr ds:[02A92088h] 

0000000b  mov         eax,dword ptr ds:[02A9208Ch] 
00000011  cmp         dword ptr [eax+8],3 
00000015  jne         0000003B 
00000017  cmp         dword ptr [edi+8],eax 
0000001a  mov         esi,dword ptr [edi+8] 
0000001d  inc         esi  
0000001e  call        6C87BE90 ; WriteLine(i)
00000023  mov         ecx,eax 
00000025  mov         edx,esi 
00000027  mov         eax,dword ptr [ecx] 
00000029  call        dword ptr [eax+000000BCh] 
0000002f  mov         eax,dword ptr ds:[02A9208Ch] 
00000035  cmp         dword ptr [eax+8],3 
00000039  je          0000001A 

0000003b  mov         eax,dword ptr ds:[02A9208Ch] 
00000041  cmp         dword ptr [eax+8],3 
00000045  jne         0000006B 
00000047  cmp         dword ptr [edi+8],eax 
0000004a  mov         esi,dword ptr [edi+8] 
0000004d  inc         esi  
0000004e  call        6C87BE90 ; WriteLine(i)
00000053  mov         ecx,eax 
00000055  mov         edx,esi 
00000057  mov         eax,dword ptr [ecx] 
00000059  call        dword ptr [eax+000000BCh] 
0000005f  mov         eax,dword ptr ds:[02A9208Ch] 
00000065  cmp         dword ptr [eax+8],3 
00000069  je          0000004A 

0000006b  pop         esi  
0000006c  pop         edi  
0000006d  pop         ebp  
0000006e  ret

znowu jest tu parę instrukcji zbędnych (dlatego programy w C# są ogólnie wolniejsze niż natywne np. w C++) ale nadal obie pętle są identyczne.
Nie ma więc znaczenia dla prędkości, którego wariantu użyjemy.

PS. podobno 64-bitowy Framework ma lepszy optymalizator kodu maszynowego, ale nie mam jak sprawdzić.

1

No tak, niestety nie zawsze JIT radzi sobie dobrze z optymalizowaniem kodu - ale nie jest to wina inżynierów z MS, powód jest znacznie głębszy.
Kompilacja i (szczególnie) optymalizacja może trwać wieki i nikt specjalnie nie będzie narzekał - np. 2 godziny nie stanowiłyby dla żadnej korporacji problemu (ofc chodzi o optymalizację przed wypuszczeniem aplikacji a nie o zwykłą, codzienną kompilację...). C# ma gorzej - kompilacja jest błyskawiczna. Właściwie to kompilator C# nic nie optymalizuje (były nawet pomysły żeby stworzyć optymalizator bytecodu który bierze jeden program i zwraca szybszy ale nie weszły w życie i nie wiadomo ile takie coś mogłoby dać). Cały ciężar optymalizacji spada na JIT - no a JIT... nie może optymalizować ile chce, musi się zmieścić najlepiej w kilka sekund... Cały czas trzeba znajdywać kompromis pomiędzy szybkością uruchamiania programu a szybkością działania.

@Azarien - ciekawa analiza. Disasemblowałeś dump pamięci, użyłeś natywnego debuggera czy masz jakieś gotowe narzędzie? Jeśli tak to jakie?

0

Disasemblowałeś dump pamięci, użyłeś natywnego debuggera czy masz jakieś gotowe narzędzie? Jeśli tak to jakie?

Visual Studio 2008. Ustawiam breakpointa w interesującej funkcji (nie działa dla Main()), odpalam program, program się zatrzymuje, otwieram okienko Disassembly.
Tak, to działa w ten sposób ;-)

Problem tylko żeby zmusić środowisko do debugowania zoptymalizowanego kodu. Dlatego
· kompilujemy w trybie Release
· z włączoną optymalizacją we właściwościach projektu
· trzeba jeszcze wyłączyć opcję Tools|Options|Debugging|General|Supress JIT optimization on module load

1 użytkowników online, w tym zalogowanych: 0, gości: 1