Deklarowanie zmiennej wewnątrz lub na zewnątrz pętli foreach: co jest szybsze / lepsze?

93

Który z nich jest szybszy / lepszy?

Ten:

List<User> list = new List<User>();
User u;

foreach (string s in l)
{
    u = new User();
    u.Name = s;
    list.Add(u);
}

Albo ten:

List<User> list = new List<User>();

foreach (string s in l)
{
    User u = new User();
    u.Name = s;
    list.Add(u);
}

Moje umiejętności rozwijania początkujących mówią mi, że pierwsza jest lepsza, ale mój przyjaciel mówi mi, że się mylę, ale nie może podać dobrego powodu, dla którego druga jest lepsza.

Czy jest jakaś różnica w wydajności?

Marcus
źródło

Odpowiedzi:

114

Pod względem wydajności oba przykłady są kompilowane do tego samego IL, więc nie ma różnicy.

Druga jest lepsza, ponieważ wyraźniej wyraża twoje zamiary, jeśli ujest używana tylko wewnątrz pętli.

dtb
źródło
10
Zauważ, że nie ma różnicy, czy zmienna jest opanowana przez wyrażenie lambda lub anonimowego delegata; zobacz Outer Variable Trap .
dtb
Czy możesz wyjaśnić, dlaczego oba są skompilowane do tego samego IL? Jestem prawie pewien, że C # nie podnosi deklaracji zmiennych do górnej części funkcji, tak jak robi to javascript.
styfle
4
@styfle oto odpowiedź na twoje pytanie.
David Sherret,
Poniższe linki Stack Overflow zawierają bardziej szczegółowe odpowiedzi: 1) Jon Hanna i 2) StriplingWarrior
user3613932
14

W każdym razie najlepszym sposobem byłoby użycie konstruktora, który przyjmuje nazwę ... lub, w przeciwnym razie, wykorzystanie notacji w nawiasach klamrowych:

foreach (string s in l)
{
    list.Add(new User(s));
}

lub

foreach (string s in l)
{
    list.Add(new User() { Name = s });
}

lub nawet lepiej, LINQ:

var list = l.Select( s => new User { Name = s});

Teraz, podczas gdy pierwszy przykład mógłby w niektórych przypadkach być niewyraźnie szybszy, drugi jest lepszy, ponieważ jest bardziej czytelny, a kompilator może odrzucić zmienną (i całkowicie ją pominąć), ponieważ nie jest używana poza foreachzakresem.

Tordek
źródło
6
Nekrofilny komentarz dnia: „lub nawet lepiej, LINQ”. Jasne, że to jedna linia kodu i to sprawia, że ​​jako programiści czujemy się dobrze. Ale wersja czterowierszowa jest DUŻO bardziej zrozumiała, a tym samym łatwa do utrzymania.
Oskar Austegard
5
Prawie wcale. Dzięki wersji LINQ wiem, że to, co robię, jest niezmienne i działa nad każdym elementem.
Tordek
6

Deklaracja nie powoduje wykonania żadnego kodu, więc nie jest to problem z wydajnością.

Drugie jest tym, co masz na myśli, i mniej prawdopodobne jest, że popełnisz głupi błąd, jeśli zrobisz to w drugi sposób, więc użyj tego. Zawsze staraj się deklarować zmienne w możliwie najmniejszym zakresie.

Poza tym lepszym sposobem jest użycie Linq:

List<User> users = l.Select(name => new User{ Name = name }).ToList();
Mark Byers
źródło
2
Uwielbiam wiersz „Zawsze staraj się deklarować zmienne w możliwie najmniejszym zakresie”. Myślę, że pojedyncza linijka może bardzo dobrze odpowiedzieć na to pytanie.
Manjoor,
5

Zawsze, gdy masz pytanie dotyczące wydajności, jedyną rzeczą do zrobienia jest pomiar - wykonaj pętlę wokół testu i zmień czas.

Aby odpowiedzieć na Twoje pytanie - bez mierzenia :-) lub patrzenia na wygenerowany ilasm - żadna różnica nie byłaby zauważalna w znaczącej liczbie iteracji i najdroższej operacji w Twoim kodzie, prawdopodobnie będzie to przypisanie użytkowników przez kilka zamówień wielkości, więc skoncentruj się na przejrzystości kodu (tak jak ogólnie powinieneś) i idź z 2.

Och, jest późno i myślę, że staram się tylko powiedzieć, że nie martw się o tego typu rzeczy ani nie daj się wciągnąć w takie szczegóły.

K.

Kevin Shea
źródło
dzięki za wskazówkę, myślę, że będę miał czas na kilka innych rzeczy, o których byłem ranny. hehe: D
Marcus
Jeśli chcesz dokładniej przyjrzeć się temu, co wpływa na wydajność, zapoznaj się z użyciem profilera kodu. Jeśli nic więcej, zacznie ci to dawać wyobrażenie o tym, jaki typ kodu i operacji zajmuje najwięcej czasu. ProfileSharp i EqatecProfilers są bezpłatne i wystarczające, aby zacząć.
Kevin Shea,
1

Drugi jest lepszy. Masz zamiar mieć nowego użytkownika w każdej iteracji.

Jarrett Widman
źródło
1

Technicznie rzecz biorąc, pierwszy przykład pozwoli zaoszczędzić kilka nanosekund, ponieważ ramka stosu nie będzie musiała być przenoszona, aby przydzielić nową zmienną, ale jest to tak mała ilość czasu procesora, że ​​tego nie zauważysz, jeśli kompilator tego nie zrobi zoptymalizować każdą różnicę w dowolnym momencie.

Erik Funkenbusch
źródło
Jestem prawie pewien, że CLR nie przydziela „nowej zmiennej” w każdej iteracji pętli.
dtb
Cóż, kompilator może to zoptymalizować, ale miejsce na stosie musi być przydzielone dla wszystkich zmiennych w pętli. Byłoby to zależne od implementacji i jedna implementacja może po prostu zachować tę samą ramkę stosu, podczas gdy inna (powiedzmy Mono) mogłaby zwolnić stos, a następnie odtworzyć go w każdej pętli.
Erik Funkenbusch,
16
Wszystkie zmienne lokalne w metodzie (najwyższego poziomu lub zagnieżdżone w pętli) są kompilowane do zmiennych na poziomie metody w języku IL. Miejsce na zmienne jest przydzielane przed wykonaniem metody, a nie po osiągnięciu gałęzi z deklaracją w C #.
dtb
1
@dtb Czy masz źródło dla tego roszczenia?
styfle
1

W tym scenariuszu druga wersja jest lepsza.

Ogólnie rzecz biorąc, jeśli potrzebujesz tylko dostępu do wartości w treści iteracji, wybierz drugą wersję. Z drugiej strony, jeśli istnieje jakiś ostateczny stan, który zmienna będzie utrzymywać poza ciałem pętli, zadeklaruj, a następnie użyj pierwszej wersji.

csj
źródło
0

Nie powinno być zauważalnej różnicy w wydajności.

Jacob Adams
źródło
0

Poszedłem sprawdzić ten problem. Zaskakująco dowiedziałem się na moich brudnych testach, że druga opcja jest przez cały czas nawet nieco szybsza.

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    void Method1()
    {
      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }
    }

    void Method2()
    {

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }
    }
  }

  public class User { public string Name; }
}

Sprawdziłem CIL, ale nie jest identyczny.

wprowadź opis obrazu tutaj

Przygotowałem więc coś, co chciałem być znacznie lepszym testem.

namespace Test
{
  class Loop
  { 

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    public void Method1()
    {
      sw.Restart();

      C c;
      C c1;
      C c2;
      C c3;
      C c4;

      int i = 1000;
      while (i-- > 0)
      {
        c = new C();
        c1 = new C();
        c2 = new C();
        c3 = new C();
        c4 = new C();        
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    public void Method2()
    {
      sw.Restart();

      int i = 1000;
      while (i-- > 0)
      {
        var c = new C();
        var c1 = new C();
        var c2 = new C();
        var c3 = new C();
        var c4 = new C();
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  class C { }
}

Również w tym przypadku druga metoda zawsze wygrywała, ale potem zweryfikowałem CIL, nie znajdując żadnej różnicy.

wprowadź opis obrazu tutaj

Nie jestem guru czytającym CIL, ale nie widzę problemu dekleracji. Jak już wspomniano, deklaracja nie jest alokacją, więc nie ma na niej żadnej utraty wydajności.

Test

namespace Test
{
  class Foreach
  {
    string[] names = new[] { "ABC", "MNL", "XYZ" };

    public TimeSpan method1 = new TimeSpan();
    public TimeSpan method2 = new TimeSpan();

    Stopwatch sw = new Stopwatch();

    void Method1()
    {
      sw.Restart();

      List<User> list = new List<User>();
      User u;

      foreach (string s in names)
      {
        u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method1 = method1.Add(sw.Elapsed);
    }

    void Method2()
    {
      sw.Restart();

      List<User> list = new List<User>();

      foreach (string s in names)
      {
        User u = new User();
        u.Name = s;
        list.Add(u);
      }

      sw.Stop();
      method2 = method2.Add(sw.Elapsed);
    }
  }

  public class User { public string Name; }
Ucho
źródło