Różnica między deklarowaniem zmiennych przed lub w pętli?

312

Zawsze zastanawiałem się, czy, ogólnie rzecz biorąc, zadeklarowanie zmiennej wyrzucającej przed pętlą, w przeciwieństwie do wielokrotnego powtarzania się w pętli, robi jakąkolwiek różnicę (wydajność)? (Zupełnie bez sensu) przykład w Javie:

a) deklaracja przed pętlą:

double intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

b) deklaracja (wielokrotnie) pętli wewnętrznej:

for(int i=0; i < 1000; i++){
    double intermediateResult = i;
    System.out.println(intermediateResult);
}

Który jest lepszy, a lub b ?

Podejrzewam, że powtarzanie deklaracji zmiennych (przykład b ) powoduje większy narzut teoretycznie , ale kompilatory są na tyle inteligentne, że nie ma to znaczenia. Przykład b ma tę zaletę, że jest bardziej zwarty i ogranicza zakres zmiennej do miejsca, w którym jest używana. Mimo to mam tendencję do kodowania zgodnie z przykładem a .

Edycja: Szczególnie interesuje mnie sprawa Java.

Rabarberski
źródło
Ma to znaczenie przy pisaniu kodu Java dla platformy Android. Google sugeruje, aby dla kodu o krytycznym czasie deklarować inkrementację zmiennych poza pętlą for, tak jakby wewnątrz pętli for, deklarowała go za każdym razem w tym środowisku. Różnica wydajności jest bardzo zauważalna w przypadku drogich algorytmów.
AaronCarson,
1
@AaronCarson, czy możesz podać link do tej sugestii Google
Witalij Zinczenko

Odpowiedzi:

256

Który jest lepszy a lub b ?

Z punktu widzenia wydajności musisz to zmierzyć. (I moim zdaniem, jeśli można zmierzyć różnicę, kompilator nie jest zbyt dobry).

Z punktu widzenia konserwacji b jest lepsze. Deklaruj i inicjalizuj zmienne w tym samym miejscu, w możliwie najwęższym zakresie. Nie zostawiaj luki między deklaracją a inicjalizacją i nie zanieczyszczaj przestrzeni nazw, których nie potrzebujesz.

Daniel Earwicker
źródło
5
Zamiast Double, jeśli dotyczy String, to czy lepiej „b”?
Antoops
3
@Antoops - tak, b jest lepszy z powodów, które nie mają nic wspólnego z typem danych deklarowanej zmiennej. Dlaczego miałoby być inaczej dla ciągów?
Daniel Earwicker
215

Cóż, sprawdziłem przykłady A i B po 20 razy, zapętlając 100 milionów razy. (JVM - 1.5.0)

Odp .: średni czas wykonania: 0,074 sek

B: średni czas wykonania: 0,067 sek

Ku mojemu zaskoczeniu B był nieco szybszy. Tak szybko, jak komputery są teraz trudne do stwierdzenia, czy można to dokładnie zmierzyć. Kodowałbym to również w sposób A, ale powiedziałbym, że to tak naprawdę nie ma znaczenia.

znak
źródło
12
Pokonałeś mnie Właśnie miałem opublikować moje wyniki do profilowania, stałem się mniej więcej taki sam i tak zaskakująco B jest szybszy naprawdę pomyślałbym A, gdybym musiał postawić na to.
Mark Davidson
14
Nic dziwnego - gdy zmienna jest lokalna dla pętli, nie musi być zachowywana po każdej iteracji, aby mogła pozostać w rejestrze.
142
+1 za przetestowanie go , a nie tylko opinię / teorię, którą OP mógł sam wymyślić.
MGOw
3
@ GoodPerson szczerze mówiąc, chciałbym, żeby to było zrobione. Przeprowadziłem ten test około 10 razy na moim komputerze dla 50 000 000-100 000 000 iteracji z prawie identycznym fragmentem kodu (który chciałbym udostępnić każdemu, kto chce uruchomić statystyki). Odpowiedzi zostały podzielone prawie równo w obu kierunkach, zwykle z marginesem 900 ms (ponad 50 milionów iteracji), co nie jest tak naprawdę duże. Choć moją pierwszą myślą jest to, że będzie to „hałas”, może się po prostu pochylić. Ten wysiłek wydaje mi się jednak czysto akademicki (dla większości aplikacji z prawdziwego życia). Chciałbym zobaczyć wynik;) Czy ktoś się zgodził?
javatarz
3
Wyświetlanie wyników testu bez dokumentowania konfiguracji jest bezwartościowe. Jest to szczególnie prawdziwe w tym przypadku, gdy oba fragmenty kodu wytwarzają identyczny kod bajtowy, więc każda zmierzona różnica jest tylko oznaką niewystarczających warunków testowych.
Holger
66

To zależy od języka i dokładnego użycia. Na przykład w C # 1 nie miało znaczenia. W C # 2, jeśli zmienna lokalna jest przechwycona przez anonimową metodę (lub wyrażenie lambda w C # 3), może to zrobić bardzo znaczącą różnicę.

Przykład:

using System;
using System.Collections.Generic;

class Test
{
    static void Main()
    {
        List<Action> actions = new List<Action>();

        int outer;
        for (int i=0; i < 10; i++)
        {
            outer = i;
            int inner = i;
            actions.Add(() => Console.WriteLine("Inner={0}, Outer={1}", inner, outer));
        }

        foreach (Action action in actions)
        {
            action();
        }
    }
}

Wynik:

Inner=0, Outer=9
Inner=1, Outer=9
Inner=2, Outer=9
Inner=3, Outer=9
Inner=4, Outer=9
Inner=5, Outer=9
Inner=6, Outer=9
Inner=7, Outer=9
Inner=8, Outer=9
Inner=9, Outer=9

Różnica polega na tym, że wszystkie akcje przechwytują tę samą outerzmienną, ale każda ma własną oddzielną innerzmienną.

Jon Skeet
źródło
3
w przykładzie B (oryginalne pytanie), czy faktycznie tworzy nową zmienną za każdym razem? co dzieje się w oczach stosu?
Royi Namir
@Jon, czy to był błąd w C # 1.0? Idealnie nie powinno Outerbyć 9?
nawfal
@nawfal: Nie wiem o co ci chodzi. Wyrażeń lambda nie było w wersji 1.0 ... a Zewnętrzna to 9. Jaki błąd masz na myśli?
Jon Skeet
@nawfal: Chodzi mi o to, że w C # 1.0 nie było żadnych funkcji językowych, w których można by odróżnić deklarację zmiennej w pętli od deklaracji na zewnątrz (zakładając, że obie zostały skompilowane). To zmieniło się w C # 2.0. Bez błędów.
Jon Skeet
@JonSkeet O tak, rozumiem teraz, całkowicie przeoczyłem fakt, że nie można zamknąć takich zmiennych w 1.0, mój zły! :)
nawfal
35

Oto, co napisałem i skompilowałem w .NET.

double r0;
for (int i = 0; i < 1000; i++) {
    r0 = i*i;
    Console.WriteLine(r0);
}

for (int j = 0; j < 1000; j++) {
    double r1 = j*j;
    Console.WriteLine(r1);
}

To właśnie otrzymuję z .NET Reflector, gdy CIL jest ponownie renderowany do kodu.

for (int i = 0; i < 0x3e8; i++)
{
    double r0 = i * i;
    Console.WriteLine(r0);
}
for (int j = 0; j < 0x3e8; j++)
{
    double r1 = j * j;
    Console.WriteLine(r1);
}

Oba wyglądają dokładnie tak samo po kompilacji. W językach zarządzanych kod jest konwertowany na kod CL / bajtowy, aw momencie wykonania jest konwertowany na język maszynowy. Tak więc w języku maszynowym podwójne nie może nawet zostać utworzone na stosie. Może to być tylko rejestr, ponieważ kod odzwierciedla, że ​​jest to zmienna tymczasowa dla . Istnieje bardzo duża różnica w wydajności między nimi. Istnieje wiele takich przypadków, w których kompilator nie może zoptymalizować kodu, ponieważ nie może zrozumieć, co jest zamierzone w większym zakresie. Ale może zoptymalizować dla Ciebie podstawowe rzeczy.WriteLine funkcji. Istnieje cały zestaw zasad optymalizacji tylko dla pętli. Przeciętny facet nie powinien się tym martwić, zwłaszcza w językach zarządzanych. Zdarzają się sytuacje, w których można zoptymalizować zarządzanie kodem, na przykład, jeśli trzeba połączyć dużą liczbę ciągów za pomocą funkcji just string a; a+=anotherstring[i]vs usingStringBuilder

cząstka
źródło
int j = 0 dla (; j <0x3e8; j ++) w ten sposób zadeklarowano raz zmienną czasową, a nie każdą dla cyklu. 2) zadanie jest grubsze niż wszystkie inne opcje. 3) Zatem zasadą najlepszych praktyk jest każde oświadczenie poza iteracją.
luka
24

To jest gotcha w VB.NET. Wynik Visual Basic nie zainicjuje ponownie zmiennej w tym przykładzie:

For i as Integer = 1 to 100
    Dim j as Integer
    Console.WriteLine(j)
    j = i
Next

' Output: 0 1 2 3 4...

Spowoduje to wydrukowanie 0 po raz pierwszy (zmienne Visual Basic mają wartości domyślne, gdy są deklarowane!), Ale i każdym razem po tym.

Jeśli dodasz = 0, otrzymasz, czego możesz się spodziewać:

For i as Integer = 1 to 100
    Dim j as Integer = 0
    Console.WriteLine(j)
    j = i
Next

'Output: 0 0 0 0 0...
Michael Haren
źródło
1
Używam VB.NET od lat i nie spotkałem się z tym !!
ChrisA
12
Tak, w praktyce jest to nieprzyjemne.
Michael Haren
Oto odniesienie do tego od Paula Vicka: panopticoncentral.net/archive/2006/03/28/11552.aspx
ferventcoder
1
@eschneider @ferventcoder Niestety @PaulV postanowił porzucić swoje stare posty na blogu , więc teraz jest to martwy link.
Mark Hurd
tak, właśnie niedawno się na to natknąłem; szukał oficjalnych dokumentów na ten temat ...
Eric Schneider
15

Zrobiłem prosty test:

int b;
for (int i = 0; i < 10; i++) {
    b = i;
}

vs

for (int i = 0; i < 10; i++) {
    int b = i;
}

Skompilowałem te kody za pomocą gcc - 5.2.0. A potem zdemontowałem main () z tych dwóch kodów i oto wynik:

1º:

   0x00000000004004b6 <+0>:     push   rbp
   0x00000000004004b7 <+1>:     mov    rbp,rsp
   0x00000000004004ba <+4>:     mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret

vs

   0x00000000004004b6 <+0>: push   rbp
   0x00000000004004b7 <+1>: mov    rbp,rsp
   0x00000000004004ba <+4>: mov    DWORD PTR [rbp-0x4],0x0
   0x00000000004004c1 <+11>:    jmp    0x4004cd <main+23>
   0x00000000004004c3 <+13>:    mov    eax,DWORD PTR [rbp-0x4]
   0x00000000004004c6 <+16>:    mov    DWORD PTR [rbp-0x8],eax
   0x00000000004004c9 <+19>:    add    DWORD PTR [rbp-0x4],0x1
   0x00000000004004cd <+23>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x00000000004004d1 <+27>:    jle    0x4004c3 <main+13>
   0x00000000004004d3 <+29>:    mov    eax,0x0
   0x00000000004004d8 <+34>:    pop    rbp
   0x00000000004004d9 <+35>:    ret 

Które są dokładnie takie same jak wynik. nie jest dowodem na to, że oba kody produkują to samo?

UserX
źródło
3
tak, i fajnie, że to zrobiłeś, ale wróciło to do tego, co ludzie mówili o zależności od języka / kompilatora. Zastanawiam się, jak wpłynęłoby to na działanie JIT lub interpretowanego języka.
user137717
12

Jest to zależne od języka - IIRC C # optymalizuje to, więc nie ma żadnej różnicy, ale JavaScript (na przykład) wykona cały szereg alokacji pamięci za każdym razem.

annakata
źródło
Tak, ale to niewiele. Przeprowadziłem prosty test z pętlą for wykonującą się 100 milionów razy i stwierdziłem, że największa różnica na korzyść deklarowania poza pętlą wynosiła 8 ms. Zwykle było to bardziej jak 3-4 i czasami deklarowanie poza pętlą wykonywało WORSE (do 4 ms), ale to nie było typowe.
user137717
11

Zawsze używałbym A (zamiast polegać na kompilatorze) i mógłbym również przepisać do:

for(int i=0, double intermediateResult=0; i<1000; i++){
    intermediateResult = i;
    System.out.println(intermediateResult);
}

To nadal ogranicza intermediateResultzakres pętli, ale nie zmienia stanu podczas każdej iteracji.

Tryptyk
źródło
12
Czy koncepcyjnie chcesz, aby zmienna żyła przez czas trwania pętli zamiast oddzielnie dla każdej iteracji? Rzadko tak robię. Napisz kod, który ujawni twoją intencję tak jasno, jak to możliwe, chyba że masz bardzo, bardzo dobry powód, aby zrobić inaczej.
Jon Skeet
4
Ach, niezły kompromis, nigdy o tym nie myślałem! IMO, kod staje się jednak nieco mniej „przejrzysty” wizualnie)
Rabarberski,
2
@Jon - Nie mam pojęcia, co OP robi z wartością pośrednią. Pomyślałem, że warto rozważyć.
Tryptyk
6

Moim zdaniem b jest lepszą strukturą. W a ostatnia wartość parametru intermedResult pozostaje w pobliżu po zakończeniu pętli.

Edycja: Nie ma to większego znaczenia w przypadku typów wartości, ale typy referencyjne mogą być nieco ciężkie. Osobiście podoba mi się, że zmienne mają być usuwane jak najszybciej w celu wyczyszczenia, i b robi to za ciebie,

Władca
źródło
sticks around after your loop is finished- chociaż nie ma to znaczenia w języku takim jak Python, gdzie powiązane nazwy trzymają się do końca funkcji.
new123456
@ new123456: OP poprosił o specyfikę Java, nawet jeśli pytanie zostało zadane nieco ogólnikowo. Wiele języków pochodnych C ma zakres blokowy: C, C ++, Perl (ze mysłowem kluczowym), C # i Java, żeby wymienić 5, których używałem.
Władca,
Wiem - to była obserwacja, a nie krytyka.
new123456
5

Podejrzewam, że kilka kompilatorów może zoptymalizować oba pod kątem tego samego kodu, ale na pewno nie wszystkie. Więc powiedziałbym, że lepiej ci z tym pierwszym. Jedynym powodem jest to, że chcesz się upewnić, że deklarowana zmienna jest używana tylko w pętli.

Gulasz S
źródło
5

Zasadniczo deklaruję moje zmienne w możliwie najbardziej wewnętrznym zakresie. Więc jeśli nie używasz pośredniego wyniku poza pętlą, wybrałbym B.

Chris
źródło
5

Współpracownik woli pierwszą formę, mówiąc, że jest to optymalizacja, woląc ponownie użyć deklaracji.

Wolę drugi (i przekonać mojego współpracownika! ;-)), po przeczytaniu:

  • Zmniejsza zakres zmiennych tam, gdzie są one potrzebne, co jest dobrą rzeczą.
  • Java optymalizuje się na tyle, aby nie wpływać znacząco na wydajność. IIRC, być może druga forma jest jeszcze szybsza.

W każdym razie należy do kategorii przedwczesnej optymalizacji, która polega na jakości kompilatora i / lub JVM.

PhiLho
źródło
5

Istnieje różnica w języku C #, jeśli używasz zmiennej w lambda itp. Ale ogólnie kompilator zasadniczo robi to samo, zakładając, że zmienna jest używana tylko w pętli.

Biorąc pod uwagę, że są one w zasadzie takie same: Zauważ, że wersja b sprawia, że ​​czytelnicy stają się bardziej oczywiste, że zmienna nie jest i nie może być używana po pętli. Ponadto wersję b można znacznie łatwiej refaktoryzować. Trudniej jest wyodrębnić ciało pętli do własnej metody w wersji a.Co więcej, wersja b zapewnia, że ​​nie wystąpi żaden efekt uboczny takiego refaktoryzacji.

Dlatego wersja denerwuje mnie bez końca, ponieważ nie ma z tego żadnych korzyści i znacznie utrudnia rozumowanie na temat kodu ...

Mark Sowul
źródło
5

Cóż, zawsze możesz zrobić na to cel:

{ //Or if(true) if the language doesn't support making scopes like this
    double intermediateResult;
    for (int i=0; i<1000; i++) {
        intermediateResult = i;
        System.out.println(intermediateResult);
    }
}

W ten sposób deklarujesz zmienną tylko raz, a ona umrze, gdy opuścisz pętlę.

Marcelo Faísca
źródło
4

Zawsze myślałem, że jeśli zadeklarujesz zmienne w pętli, marnujesz pamięć. Jeśli masz coś takiego:

for(;;) {
  Object o = new Object();
}

Następnie nie tylko trzeba utworzyć obiekt dla każdej iteracji, ale dla każdego obiektu musi być przypisane nowe odwołanie. Wygląda na to, że jeśli śmieciarz jest powolny, będziesz mieć kilka zwisających referencji, które należy oczyścić.

Jeśli jednak masz to:

Object o;
for(;;) {
  o = new Object();
}

Następnie tworzysz tylko jedno odniesienie i za każdym razem przypisujesz do niego nowy obiekt. Jasne, może upłynąć trochę dłużej, zanim wyjdzie poza zakres, ale wtedy jest tylko jedno wiszące odniesienie do rozwiązania.

R. Carr
źródło
3
Nowe odwołanie nie jest przydzielane dla każdego obiektu, nawet jeśli odniesienie jest zadeklarowane w pętli „for”. W obu przypadkach: 1) „o” jest zmienną lokalną, a miejsce stosu jest przydzielane dla niej raz na początku funkcji. 2) W każdej iteracji powstaje nowy Obiekt. Więc nie ma różnicy w wydajności. Jeśli chodzi o organizację kodu, czytelność i łatwość konserwacji, lepiej zadeklarować odniesienie w pętli.
Ajoy Bhatia,
1
Chociaż nie mogę mówić w języku Java, w .NET odwołanie nie jest „przydzielane” dla każdego obiektu w pierwszym przykładzie. Na stosie znajduje się pojedynczy wpis dla tej zmiennej lokalnej (do metody). Dla twoich przykładów utworzona IL jest identyczna.
Jesse C. Slicer,
3

Myślę, że to zależy od kompilatora i trudno jest udzielić ogólnej odpowiedzi.

SquidScareMe
źródło
3

Moja praktyka jest następująca:

  • jeśli typ zmiennej jest prosty (int, double, ...) , preferuję wariant b (wewnątrz).
    Powód: ograniczenie zakresu zmiennej.

  • jeśli typ zmiennej nie jest prosty (jakiś rodzaj classlub struct) , wolę wariant a (na zewnątrz).
    Powód: zmniejszenie liczby połączeń ctor-dtor.

gruby
źródło
1

Z punktu widzenia wydajności, na zewnątrz jest (znacznie) lepsze.

public static void outside() {
    double intermediateResult;
    for(int i=0; i < Integer.MAX_VALUE; i++){
        intermediateResult = i;
    }
}

public static void inside() {
    for(int i=0; i < Integer.MAX_VALUE; i++){
        double intermediateResult = i;
    }
}

Obie funkcje wykonałem 1 miliard razy każda. outside () zajęło 65 milisekund. inside () zajęło 1,5 sekundy.

Alex
źródło
2
To musiała być wtedy niezoptymalizowana kompilacja debugowania, co?
Tomasz Przychodzki
int j = 0 dla (; j <0x3e8; j ++) w ten sposób zadeklarowano raz zmienną czasową, a nie każdą dla cyklu. 2) zadanie jest grubsze niż wszystkie inne opcje. 3) Zatem zasadą najlepszych praktyk jest każde oświadczenie poza iteracją.
luka
1

Testowałem pod kątem JS z Node 4.0.0, jeśli ktoś jest zainteresowany. Zadeklarowanie poza pętlą spowodowało poprawę wydajności o ~ 0,5 ms średnio ponad 1000 prób przy 100 milionach iteracji pętli na próbę. Więc powiem: naprzód i napisz to w najbardziej czytelny / łatwy do utrzymania sposób, którym jest B, imo. Umieściłem mój kod w skrzypcach, ale użyłem modułu Node o wydajności. Oto kod:

var now = require("../node_modules/performance-now")

// declare vars inside loop
function varInside(){
    for(var i = 0; i < 100000000; i++){
        var temp = i;
        var temp2 = i + 1;
        var temp3 = i + 2;
    }
}

// declare vars outside loop
function varOutside(){
    var temp;
    var temp2;
    var temp3;
    for(var i = 0; i < 100000000; i++){
        temp = i
        temp2 = i + 1
        temp3 = i + 2
    }
}

// for computing average execution times
var insideAvg = 0;
var outsideAvg = 0;

// run varInside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varInside()
    var end = now()
    insideAvg = (insideAvg + (end-start)) / 2
}

// run varOutside a million times and average execution times
for(var i = 0; i < 1000; i++){
    var start = now()
    varOutside()
    var end = now()
    outsideAvg = (outsideAvg + (end-start)) / 2
}

console.log('declared inside loop', insideAvg)
console.log('declared outside loop', outsideAvg)
użytkownik137717
źródło
0

A) jest bezpiecznym zakładem niż B) ......... Wyobraź sobie, że jeśli inicjujesz strukturę w pętli zamiast „int” lub „float”, to co?

lubić

typedef struct loop_example{

JXTZ hi; // where JXTZ could be another type...say closed source lib 
         // you include in Makefile

}loop_example_struct;

//then....

int j = 0; // declare here or face c99 error if in loop - depends on compiler setting

for ( ;j++; )
{
   loop_example loop_object; // guess the result in memory heap?
}

Na pewno napotkasz problemy z wyciekiem pamięci! Dlatego uważam, że „A” jest bezpieczniejszy, podczas gdy „B” jest podatny na gromadzenie pamięci, szczególnie w przypadku pracy w pobliżu bibliotek źródłowych. Możesz sprawdzić użycie narzędzia „Valgrind” w systemie Linux, a konkretnie podrzędnego narzędzia „Helgrind”.

entuzjastyczny
źródło
0

To interesujące pytanie. Z mojego doświadczenia wynika, że ​​podczas debaty na ten temat nad kodem należy rozważyć ostateczne pytanie:

Czy jest jakiś powód, dla którego zmienna musiałaby być globalna?

Sensowne jest deklarowanie zmiennej tylko raz, globalnie, a nie wiele razy lokalnie, ponieważ lepiej organizuje kod i wymaga mniej wierszy kodu. Jeśli jednak trzeba go zadeklarować lokalnie w ramach jednej metody, zainicjowałbym go w tej metodzie, aby było jasne, że zmienna dotyczy wyłącznie tej metody. Uważaj, aby nie wywoływać tej zmiennej poza metodą, w której jest ona inicjowana, jeśli wybierzesz tę drugą opcję - twój kod nie będzie wiedział o czym mówisz i zgłosi błąd.

Na marginesie: nie powielaj nazw lokalnych zmiennych między różnymi metodami, nawet jeśli ich cele są prawie identyczne; robi się po prostu mylące.

Joshua Siktar
źródło
1
lol Nie zgadzam się z tak wielu powodów ... Jednak nie głosuj w dół ... Szanuję twoje prawo wyboru
Grantly
0

to jest lepsza forma

double intermediateResult;
int i = byte.MinValue;

for(; i < 1000; i++)
{
intermediateResult = i;
System.out.println(intermediateResult);
}

1) w ten sposób zadeklarowano raz zmienną, a nie każdą dla cyklu. 2) zadanie jest grubsze niż wszystkie inne opcje. 3) Zatem zasadą najlepszych praktyk jest każde oświadczenie poza iteracją.

luka
źródło
0

Próbowałem tego samego w Go i porównałem dane wyjściowe kompilatora za pomocą go tool compile -S pomocą go 1.9.4

Zero różnicy, jak na wyjściu asemblera.

SteveOC 64
źródło
0

Długo miałem to samo pytanie. Przetestowałem więc jeszcze prostszy fragment kodu.

Wniosek: Dla takich przypadkach istnieje NO różnica wydajności.

Obudowa z zewnętrzną pętlą

int intermediateResult;
for(int i=0; i < 1000; i++){
    intermediateResult = i+2;
    System.out.println(intermediateResult);
}

Obudowa z wewnętrzną pętlą

for(int i=0; i < 1000; i++){
    int intermediateResult = i+2;
    System.out.println(intermediateResult);
}

Sprawdziłem skompilowany plik w dekompilatorze IntelliJ i w obu przypadkach otrzymałem to samo Test.class

for(int i = 0; i < 1000; ++i) {
    int intermediateResult = i + 2;
    System.out.println(intermediateResult);
}

Zdemontowałem również kod dla obu przypadków, używając metody podanej w tej odpowiedzi . Pokażę tylko części istotne dla odpowiedzi

Obudowa z zewnętrzną pętlą

Code:
  stack=2, locals=3, args_size=1
     0: iconst_0
     1: istore_2
     2: iload_2
     3: sipush        1000
     6: if_icmpge     26
     9: iload_2
    10: iconst_2
    11: iadd
    12: istore_1
    13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
    16: iload_1
    17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
    20: iinc          2, 1
    23: goto          2
    26: return
LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13      13     1 intermediateResult   I
            2      24     2     i   I
            0      27     0  args   [Ljava/lang/String;

Obudowa z wewnętrzną pętlą

Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: sipush        1000
         6: if_icmpge     26
         9: iload_1
        10: iconst_2
        11: iadd
        12: istore_2
        13: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_2
        17: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        20: iinc          1, 1
        23: goto          2
        26: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       7     2 intermediateResult   I
            2      24     1     i   I
            0      27     0  args   [Ljava/lang/String;

Jeśli zwrócisz szczególną uwagę, tylko Slotprzypisane do ii intermediateResultw LocalVariableTablezostaną zamienione jako wynik ich kolejności pojawiania się. Ta sama różnica w gnieździe znajduje odzwierciedlenie w innych wierszach kodu.

  • Żadna dodatkowa operacja nie jest wykonywana
  • intermediateResult nadal jest zmienną lokalną w obu przypadkach, więc nie ma różnicy w czasie dostępu.

PREMIA

Kompilatory wykonują mnóstwo optymalizacji, spójrz na to, co dzieje się w tym przypadku.

Zerowy przypadek roboczy

for(int i=0; i < 1000; i++){
    int intermediateResult = i;
    System.out.println(intermediateResult);
}

Dekompilacja zerowej pracy

for(int i = 0; i < 1000; ++i) {
    System.out.println(i);
}
twitu
źródło
-1

Nawet jeśli wiem, że mój kompilator jest wystarczająco inteligentny, nie chcę na nim polegać i użyję wariantu a).

Wariant b) ma dla mnie sens tylko wtedy, gdy rozpaczliwie potrzebujesz zrobić wynik pośredni niedostępny po treści pętli. Ale i tak nie wyobrażam sobie takiej desperackiej sytuacji ...

EDYCJA: Jon Skeet zrobił bardzo dobry punkt, pokazując, że deklaracja zmiennej w pętli może mieć znaczącą różnicę semantyczną.

Abgan
źródło