Un uso práctico de la palabra clave "rendimiento" en C # [cerrado]

76

Después de casi 4 años de experiencia, no he visto un código donde se use la palabra clave de rendimiento . ¿Alguien puede mostrarme un uso práctico (junto con la explicación) de esta palabra clave, y si es así, ¿no hay otras formas más fáciles de completar lo que puede hacer?

Saeed Neamati
fuente
99
Todo (o al menos la mayoría) de LINQ se implementa utilizando el rendimiento. También el marco Unity3D ha encontrado un buen uso para él: se usa para pausar funciones (en declaraciones de rendimiento) y reanudarlo más tarde usando el estado en IEnumerable.
Dani
2
¿No debería esto trasladarse a StackOverflow?
Danny Varod
44
@Danny: no es adecuado para Stack Overflow, ya que la pregunta no es resolver un problema específico, sino preguntar para qué yieldse puede usar en general.
ChrisF
99
¿De verdad? No puedo pensar en una sola aplicación donde no la haya usado.
Aaronaught

Respuestas:

107

Eficiencia

La yieldpalabra clave crea efectivamente una enumeración perezosa sobre los elementos de la colección que puede ser mucho más eficiente. Por ejemplo, si su foreachciclo itera solo sobre los primeros 5 elementos de 1 millón de elementos, eso es todo lo que yielddevuelve, y no acumuló una colección de 1 millón de elementos internamente primero. Del mismo modo que se quiere utilizar yieldcon IEnumerable<T>valores de retorno en sus propios escenarios de programación para lograr la misma eficiencia.

Ejemplo de eficiencia obtenida en un escenario determinado

No es un método iterador, uso potencial ineficiente de una gran colección
(la colección intermedia se construye con muchos elementos)

// 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. 

Versión de iterador, eficiente
(no se crea una colección intermedia)

// 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.

Simplifica algunos escenarios de programación

En otro caso, hace que algunos tipos de clasificación y fusión de listas sean más fáciles de programar porque simplemente yieldvuelve a colocar los elementos en el orden deseado en lugar de ordenarlos en una colección intermedia e intercambiarlos allí. Hay muchos escenarios de este tipo.

Un solo ejemplo es la fusión de dos listas:

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;
}

Este método devuelve una lista contigua de elementos, efectivamente una fusión sin necesidad de una colección intermedia.

Más información

La yieldpalabra clave sólo se puede utilizar en el contexto de un método iterador (que tiene un tipo de retorno de IEnumerable, IEnumerator, IEnumerable<T>, o IEnumerator<T>.) Y hay una relación especial con foreach. Los iteradores son métodos especiales. La documentación de rendimiento de MSDN y la documentación del iterador contienen mucha información interesante y explicaciones de los conceptos. Asegúrese de correlacionarlo con la foreachpalabra clave al leerlo también, para complementar su comprensión de los iteradores.

Para conocer cómo los iteradores logran su eficiencia, el secreto está en el código IL generado por el compilador de C #. La IL generada para un método iterador difiere drásticamente de la generada para un método regular (no iterador). Este artículo (¿Qué genera realmente la palabra clave Yield?) Proporciona ese tipo de información.

John K
fuente
2
Son particularmente útiles para algoritmos que toman una secuencia (posiblemente larga) y generan otra donde el mapeo no es uno a uno. Un ejemplo de esto es el recorte de polígonos; cualquier borde en particular puede generar muchos o incluso ningún borde una vez recortado. Los iteradores hacen que esto sea mucho más fácil de expresar, y ceder es una de las mejores formas de escribirlos.
Donal Fellows
+1 Respuesta mucho mejor como la había escrito. Ahora también aprendí que el rendimiento es bueno para un mejor rendimiento.
Jan_V
3
Érase una vez, utilicé el rendimiento para construir paquetes para un protocolo de red binario. Parecía la opción más natural en C #.
György Andrasek
44
¿No database.Customers.Count()enumera toda la enumeración de clientes, por lo que se requiere un código más eficiente para revisar cada elemento?
Stephen
55
Llámame anal, pero eso es concatenación, no fusión. (Y linq ya tiene un método Concat.)
OldFart
4

Hace algún tiempo tuve un ejemplo práctico, supongamos que tiene una situación como esta:

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}", ???));
   }
}

El objeto del botón no conoce su propia posición en la colección. La misma limitación se aplica para Dictionary<T>u otros tipos de colecciones.

Aquí está mi solución usando la yieldpalabra clave:

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);
      }
   }
}

Y así es como lo uso:

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));
   }
}

No tengo que hacer un forciclo sobre mi colección para encontrar el índice. Mi botón tiene una ID y esto también se usa como índice IndexerList<T>, por lo que evita cualquier ID o índice redundante , ¡eso es lo que me gusta! El índice / Id puede ser un número arbitrario.

Wernfried Domscheit
fuente
2

Un ejemplo práctico se puede encontrar aquí:

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

Hay una serie de ventajas de usar el rendimiento sobre el código estándar:

  • Si el iterador se usa para crear una lista, entonces puede generar el retorno y la persona que llama puede decidir si quiere ese resultado en una lista, o no.
  • La persona que llama también puede decidir cancelar la iteración por una razón que está fuera del alcance de lo que está haciendo en la iteración.
  • El código es un poco más corto.

Sin embargo, como dijo Jan_V (solo páseme unos segundos :-) puede vivir sin él porque internamente el compilador producirá un código casi idéntico en ambos casos.

Jalayn
fuente
1

Aquí hay un ejemplo:

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

La clase realiza cálculos de fechas basados ​​en una semana laboral. Puedo decirle a una instancia de la clase que Bob trabaja de 9:30 a 17:30 todos los días de la semana con un descanso de una hora para el almuerzo a las 12:30. Con este conocimiento, la función AscendingShifts () generará objetos de turno de trabajo entre las fechas proporcionadas. Para enumerar todos los turnos de trabajo de Bob entre el 1 de enero y el 1 de febrero de este año, lo usaría así:

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

La clase realmente no itera sobre una colección. Sin embargo, los cambios entre dos fechas pueden considerarse como una colección. El yieldoperador hace posible iterar sobre esta colección imaginada sin crear la colección misma.

Hormiga
fuente
1

Tengo una pequeña capa de datos db que tiene una commandclase en la que establece el texto del comando SQL, el tipo de comando y devuelve un IEnumerable de 'parámetros de comando'.

Básicamente, la idea es tener comandos CLR escritos en lugar de llenar manualmente SqlCommandpropiedades y parámetros todo el tiempo.

Entonces, hay una función que se ve así:

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 };
}

La clase que hereda esta commandclase tiene las propiedades Agey Name.

Luego puede renovar un commandobjeto lleno de sus propiedades y pasarlo a una dbinterfaz que realmente realiza la llamada de comando.

En general, es muy fácil trabajar con comandos SQL y mantenerlos escritos.

John
fuente
1

Aunque el caso de fusión ya se ha cubierto en la respuesta aceptada, permítame mostrarle el método de extensión de parámetros de fusión de rendimiento ™:

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;
}

Lo uso para construir paquetes de un protocolo de red:

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();
}

El MarkChecksummétodo, por ejemplo, se ve así. Y también tiene un yield:

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;
    }
}

Pero tenga cuidado al usar métodos agregados como Sum () en un método de enumeración, ya que desencadenan un proceso de enumeración separado.

Yegor
fuente
1

El repositorio de ejemplos de Elastic Search .NET tiene un excelente ejemplo de cómo usar yield returnpara dividir una colección en varias colecciones con un tamaño especificado:

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);
        }
    }
pholly
fuente
0

Ampliando la respuesta de Jan_V, acabo de llegar a un caso del mundo real relacionado con él:

Necesitaba usar las versiones Kernel32 de FindFirstFile / FindNextFile. Obtiene un identificador de la primera llamada y lo alimenta a todas las llamadas posteriores. Envuelva esto en un enumerador y obtendrá algo que puede usar directamente con foreach.

Loren Pechtel
fuente