Praktyczne użycie słowa kluczowego „wydajność” w języku C # [zamknięty]


76

Po prawie 4 latach doświadczeń, nie widziałem kodu gdzie wydajność jest używane słowo kluczowe. Czy ktoś może mi pokazać praktyczne użycie (wraz z objaśnieniem) tego słowa kluczowego, a jeśli tak, to czy nie istnieją inne sposoby łatwiejszego wypełnienia tego, co może zrobić?


9
Wszystkie (lub przynajmniej większość) LINQ jest implementowane przy użyciu wydajności. Również framework Unity3D znalazł dla niego dobre zastosowanie - służy do wstrzymywania funkcji (w instrukcjach dotyczących wydajności) i wznawiania go później przy użyciu stanu w IEnumerable.
Dani

2
Czy nie należy tego przenieść do StackOverflow?
Danny Varod

4
@ Danny - nie nadaje się do przepełnienia stosu, ponieważ pytanie nie dotyczy rozwiązania konkretnego problemu, ale pytania o to, co yieldmożna ogólnie wykorzystać.
ChrisF

9
Na serio? Nie mogę wymyślić jednej aplikacji, w której jej nie użyłem.
Aaronaught

Odpowiedzi:


107

Wydajność

Słowo yieldkluczowe skutecznie tworzy leniwe wyliczenie ponad elementami kolekcji, które mogą być znacznie bardziej wydajne. Na przykład, jeśli twoja foreachpętla dokonuje iteracji tylko pierwszych 5 elementów z 1 miliona przedmiotów, to wszystko to yieldzwraca, a najpierw nie zbudowałeś kolekcji 1 miliona przedmiotów. Podobnie będzie chciał skorzystać yieldz IEnumerable<T>wartości zwracanych własnymi scenariuszami programowania, aby osiągnąć te same korzyści.

Przykład wydajności uzyskanej w określonym scenariuszu

Nie jest to metoda iteracyjna, potencjalnie nieefektywne użycie dużej kolekcji
(kolekcja pośrednia jest zbudowana z dużą ilością przedmiotów)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

Wersja Iterator, wydajna
(nie jest budowana kolekcja pośrednia)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

Uprość niektóre scenariusze programowania

W innym przypadku ułatwia to programowanie sortowania i scalania list, ponieważ po prostu yieldelementy z powrotem w pożądanej kolejności zamiast sortować je do kolekcji pośredniej i zamieniać je tam. Istnieje wiele takich scenariuszy.

Jednym z przykładów jest połączenie dwóch list:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

Ta metoda daje jedną ciągłą listę elementów, co skutecznie łączy się bez pośredniej kolekcji.

Więcej informacji

yieldKluczowe może być stosowany tylko w kontekście metody iteracyjnej (o typ zwrotnego IEnumerable, IEnumerator, IEnumerable<T>, i IEnumerator<T>.) I nie jest szczególny stosunek foreach. Iteratory to specjalne metody. Dokumentacja wydajności MSDN i dokumentacja iteratora zawiera wiele interesujących informacji i objaśnień pojęć. Pamiętaj, aby skorelować go z foreachkluczowych czytając o nim też uzupełnić swoją wiedzę iteratorów.

Aby dowiedzieć się, w jaki sposób iteratory osiągają swoją wydajność, sekret znajduje się w kodzie IL wygenerowanym przez kompilator C #. IL wygenerowana dla metody iteracyjnej różni się drastycznie od generowanej dla zwykłej metody (nie iteracyjnej). Ten artykuł (co naprawdę generuje słowo kluczowe Yield?) Zawiera tego rodzaju informacje.


2
Są one szczególnie przydatne w przypadku algorytmów, które mają (prawdopodobnie długą) sekwencję i generują inny, w którym mapowanie nie jest jeden do jednego. Przykładem tego jest obcinanie wielokątów; jakakolwiek konkretna krawędź po wygenerowaniu może generować wiele lub nawet żadnych krawędzi. Iteratory sprawiają, że jest to niezwykle łatwe do wyrażenia, a ustępowanie jest jednym z najlepszych sposobów na ich napisanie.
Donal Fellows

+1 Znacznie lepsza odpowiedź, jak napisałem. Teraz dowiedziałem się również, że wydajność jest dobra dla lepszej wydajności.
Jan_V,

3
Dawno, dawno temu wykorzystywałem dochód do budowania pakietów dla binarnego protokołu sieciowego. Wydawało się to najbardziej naturalnym wyborem w C #.
György Andrasek

4
Czy database.Customers.Count()wyliczenie nie obejmuje całego wyliczenia klientów, co wymaga bardziej wydajnego kodu, aby przejść przez każdy element?
Stephen

5
Nazywaj mnie analnym, ale to jest konkatenacja, a nie łączenie się. (A linq już ma metodę Concat.)
OldFart

4

Jakiś czas temu miałem praktyczny przykład, załóżmy, że masz taką sytuację:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

Obiekt przycisku nie zna swojej pozycji w kolekcji. To samo ograniczenie dotyczy Dictionary<T>innych typów kolekcji.

Oto moje rozwiązanie za pomocą yieldsłowa kluczowego:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

I tak to wykorzystuję:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

Nie muszę tworzyć forpętli w mojej kolekcji, aby znaleźć indeks. Mój przycisk ma identyfikator, który służy również jako indeks IndexerList<T>, więc unikniesz zbędnych identyfikatorów lub indeksów - to lubię! Indeks / identyfikator może być dowolną liczbą.


2

Praktyczny przykład można znaleźć tutaj:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

Istnieje wiele zalet używania wydajności w stosunku do standardowego kodu:

  • Jeśli iterator jest używany do zbudowania listy, możesz uzyskać zwrot, a osoba dzwoniąca może zdecydować, czy chce uzyskać wynik, czy nie.
  • Dzwoniący może również zdecydować o anulowaniu iteracji z powodu, który jest poza zakresem tego, co robisz w iteracji.
  • Kod jest nieco krótszy.

Jednak, jak powiedział Jan_V (po prostu pobij mnie o kilka sekund :-) możesz żyć bez niego, ponieważ wewnętrznie kompilator wygeneruje kod prawie identyczny w obu przypadkach.


1

Oto przykład:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

Klasa wykonuje obliczenia daty na podstawie tygodnia roboczego. Mogę powiedzieć instancji klasy, że Bob pracuje od 9:30 do 17:30 każdego tygodnia z godzinną przerwą na lunch o 12:30. Dzięki tej wiedzy funkcja AscendingShift () da obiekty robocze zmiany między podanymi datami. Aby wymienić wszystkie zmiany robocze Boba między 1 stycznia a 1 lutego tego roku, możesz użyć tego w następujący sposób:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

Klasa tak naprawdę nie przechodzi przez kolekcję. Jednak przesunięcia między dwiema datami można traktować jako zbiór. yieldOperator umożliwia iteracyjne nad tej wyobrażonej kolekcję bez tworzenia samej kolekcji.


1

Mam małą warstwę danych db, która ma commandklasę, w której ustawiasz tekst polecenia SQL, typ polecenia i zwracasz IEnumerable z „parametrów polecenia”.

Zasadniczo chodzi o to, aby wpisywać polecenia CLR zamiast ręcznie wypełniać SqlCommandwłaściwości i parametry przez cały czas.

Jest więc funkcja, która wygląda następująco:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

Klasa, która dziedziczy tę commandklasę, ma właściwości Agei Name.

Następnie możesz nowy commandobiekt wypełnić jego właściwości i przekazać go do dbinterfejsu, który faktycznie wywołuje polecenie.

Podsumowując, praca z poleceniami SQL jest naprawdę łatwa.


1

Chociaż przypadek scalenia został już uwzględniony w zaakceptowanej odpowiedzi, pozwól, że pokażę Ci metodę rozszerzania parametrów łączenia z wydajnością ™:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

Używam tego do budowania pakietów protokołu sieciowego:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

Na MarkChecksumprzykład metoda wygląda następująco. I ma yieldteż:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

Należy jednak zachować ostrożność, stosując metody agregujące, takie jak Sum () w metodzie wyliczania, ponieważ uruchamiają one osobny proces wyliczania.


1

Repozytorium przykładowe Elastic Search .NET ma świetny przykład użycia yield returndo podzielenia kolekcji na wiele kolekcji o określonym rozmiarze:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Rozwijając odpowiedź Jan_V, właśnie uderzyłem w związany z nią przypadek w świecie rzeczywistym:

Musiałem użyć wersji FindFirstFile / FindNextFile w wersji Kernel32. Otrzymujesz uchwyt od pierwszego połączenia i podajesz go do wszystkich kolejnych połączeń. Zapakuj to w moduł wyliczający, a otrzymasz coś, czego możesz bezpośrednio użyć z foreach.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.