c# - Uso apropiado de 'rendimiento rendimiento'


La palabra clave yield es una de esas palabras clave en C # que me sigue desconcertando, y nunca he tenido la certeza de estar utilizándola correctamente.

De los siguientes dos códigos, ¿cuál es el preferido y por qué?

Versión 1: Uso del retorno de rendimiento

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Versión 2: devolver la lista

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}

Answers



Tiendo a usar rendimiento-rendimiento cuando calculo el siguiente artículo en la lista (o incluso el siguiente grupo de artículos).

Usando su Versión 2, debe tener la lista completa antes de regresar. Al usar rendimiento-retorno, solo necesita tener el siguiente ítem antes de regresar.

Entre otras cosas, esto ayuda a extender el costo computacional de cálculos complejos en un marco de tiempo más grande. Por ejemplo, si la lista está conectada a una GUI y el usuario nunca va a la última página, nunca se calculan los elementos finales en la lista.

Otro caso donde el rendimiento-retorno es preferible es si el IEnumerable representa un conjunto infinito. Considere la lista de números primos, o una lista infinita de números aleatorios. Nunca puede devolver el IEnumerable completo de una vez, por lo que utiliza yield-return para devolver la lista de forma incremental.

En tu ejemplo particular, tienes la lista completa de productos, así que usaría la Versión 2.




Llenar una lista temporal es como descargar todo el video, mientras que usar el yield es como transmitir ese video.




Como un ejemplo conceptual para entender cuándo debe usar el yield , digamos que el método ConsumeLoop() procesa los elementos devueltos / cedidos por ProduceList() :

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Sin yield , la llamada a ProduceList() puede llevar mucho tiempo porque debe completar la lista antes de volver:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Usando el yield , se reorganiza, trabajando de forma similar "en paralelo":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

Y, por último, como muchos ya lo han sugerido, debería usar la Versión 2 porque de todos modos ya tiene la lista completa.




Esto parecerá una sugerencia extraña, pero aprendí a usar la palabra clave yield en C # leyendo una presentación sobre generadores en Python: http://www.dabeaz.com/generators/Generators.pdf de David M. Beazley. No necesita saber mucho de Python para entender la presentación, no lo hice. Lo encontré muy útil para explicar no solo cómo funcionan los generadores, sino por qué debería importarte.




Sé que esta es una vieja pregunta, pero me gustaría ofrecer un ejemplo de cómo la palabra clave yield puede ser utilizada de forma creativa. Realmente me he beneficiado de esta técnica. Espero que esto ayude a cualquier persona que tropiece con esta pregunta.

Nota: No piense en la palabra clave yield como una mera manera de crear una colección. Una gran parte del poder del rendimiento proviene del hecho de que la ejecución se detiene en su método o propiedad hasta que el código de llamada repite el siguiente valor. Aquí está mi ejemplo:

El uso de la palabra clave yield (junto con la implementación de coretinas Caliburn.Micro de Rob Eisenburg) me permite expresar una llamada asincrónica a un servicio web como este:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Lo que esto hará es encender mi BusyIndicator, llamar al método de inicio de sesión en mi servicio web, establecer mi indicador IsLoggedIn en el valor de retorno y luego apagar el BusyIndicator.

Así es como funciona esto: IResult tiene un método Execute y un evento Completed. Caliburn.Micro toma el IEnumerator de la llamada a HandleButtonClick () y lo pasa a un método Coroutine.BeginExecute. El método BeginExecute comienza a iterar a través de IResults. Cuando se devuelve el primer IResult, la ejecución se pausa dentro de HandleButtonClick (), y BeginExecute () asocia un controlador de eventos al evento Completado y llama a Execute (). IResult.Execute () puede realizar una tarea sincrónica o asincrónica y desencadena el evento Completado cuando termina.

LoginResult se ve así:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Puede ayudar establecer algo así y pasar por la ejecución para ver lo que está sucediendo.

¡Espero que esto ayude a alguien! Realmente he disfrutado explorando las diferentes maneras en que se puede usar el rendimiento.




Los dos pedazos de código realmente están haciendo dos cosas diferentes. La primera versión atraerá miembros a medida que los necesite. La segunda versión cargará todos los resultados en la memoria antes de que empieces a hacer algo con ella.

No hay una respuesta correcta o incorrecta a esta. Cuál es preferible solo depende de la situación. Por ejemplo, si hay un límite de tiempo para completar su consulta y necesita hacer algo semi complicado con los resultados, la segunda versión podría ser preferible. Pero tenga cuidado con los grandes resultados, especialmente si está ejecutando este código en modo de 32 bits. Me han mordido las excepciones OutOfMemory varias veces al hacer este método.

Sin embargo, la clave a tener en cuenta es esta: las diferencias están en la eficiencia. Por lo tanto, probablemente deberías elegir el que simplifique tu código y cambiarlo solo después del perfil.




El rendimiento tiene dos grandes usos

Ayuda a proporcionar iteraciones personalizadas sin crear colecciones temp. (cargando todos los datos y bucles)

Ayuda a hacer una iteración con estado. ( transmisión)

A continuación se muestra un video simple que he creado con demostración completa para apoyar los dos puntos anteriores

http://www.youtube.com/watch?v=4fju3xcm21M




Esto es un poco más allá del punto, pero ya que la pregunta está etiquetada como las mejores prácticas, seguiré adelante y daré mi granito de arena. Para este tipo de cosas, prefiero convertirlo en una propiedad:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Claro, es un poco más placa de caldera, pero el código que usa esto se verá mucho más limpio:

prices = Whatever.AllProducts.Select (product => product.price);

vs

prices = Whatever.GetAllProducts().Select (product => product.price);

Nota: No haría esto por ningún método que tarde un tiempo en hacer su trabajo.




Esto es lo que Chris Sells dice sobre esas declaraciones en The C # Programming Language ;

A veces olvido que el retorno de rendimiento no es lo mismo que el retorno, ya que el código después de un rendimiento puede ser ejecutado. Por ejemplo, el código después del primer retorno aquí nunca se puede ejecutar:

    int F() {
return 1;
return 2; // Can never be executed
}

Por el contrario, el código después del primer retorno de rendimiento aquí se puede ejecutar:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Esto a menudo me muerde en una declaración if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

En estos casos, recordar que el rendimiento no es "definitivo", como el retorno es útil.




Suponiendo que sus productos de la clase LINQ utilicen un rendimiento similar para enumerar / iterar, la primera versión es más eficiente porque solo arroja un valor cada vez que se itera.

El segundo ejemplo es convertir el enumerador / iterador a una lista con el método ToList (). Esto significa que itera manualmente sobre todos los elementos en el enumerador y luego devuelve una lista plana.




¿Y qué hay de esto?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Supongo que esto es mucho más limpio. Sin embargo, no tengo VS2008 a mano para verificar. En cualquier caso, si los Productos implementan IEnumerable (como parece, se usa en una declaración foreach), lo devolvería directamente.




El rendimiento puede ser muy poderoso para los algoritmos donde se necesita iterar a través de millones de objetos. Considere el siguiente ejemplo en el que necesita calcular posibles viajes para compartir el viaje. Primero generamos posibles viajes:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Luego itere a través de cada viaje:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips(trips))
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Si usa List en lugar de yield, necesitará asignar 1 millón de objetos a la memoria (~ 190 mb) y este sencillo ejemplo tardará ~ 1400ms en ejecutarse. Sin embargo, si usa el rendimiento, no necesita poner todos estos objetos temporales en la memoria y obtendrá una velocidad de algoritmo significativamente más rápida: este ejemplo solo tardará ~ 400 ms en ejecutarse sin consumo de memoria.




Regrese la lista directamente. Beneficios:

  • Está más claro
  • La lista es reutilizable (el iterador no es) no es verdad, gracias Jon

Debe usar el iterador (rendimiento) desde el momento en que cree que probablemente no tendrá que iterar hasta el final de la lista o cuando no tenga fin. Por ejemplo, la llamada del cliente va a buscar el primer producto que satisfaga algún predicado, puede considerar usar el iterador, aunque es un ejemplo artificial, y probablemente haya mejores formas de lograrlo. Básicamente, si sabe de antemano que toda la lista deberá calcularse, solo hágalo por adelantado. Si crees que no será así, considera usar la versión del iterador.




Hubiera usado la versión 2 del código en este caso. Como tiene la lista completa de productos disponibles y eso es lo que espera el "consumidor" de esta llamada a un método, se le solicitará que envíe la información completa a la persona que llama.

Si el que llama de este método requiere "una" información a la vez y el consumo de la siguiente información es bajo demanda, entonces sería beneficioso usar el retorno de rendimiento que asegurará que el comando de ejecución se devolverá a la persona que llama cuando una unidad de información está disponible.

Algunos ejemplos en los que uno podría usar el retorno de rendimiento es:

  1. Cálculo complejo, paso a paso, donde la persona que llama está esperando los datos de un paso a la vez
  2. Paginación en GUI - donde el usuario nunca puede llegar a la última página y solo se requiere que se revele un subconjunto de información en la página actual

Para responder a sus preguntas, habría utilizado la versión 2.




La frase clave de retorno de rendimiento se usa para mantener la máquina de estado para una colección en particular. Dondequiera que el CLR vea la frase clave de retorno de rendimiento utilizada, CLR implementa un patrón de enumerador para ese fragmento de código. Este tipo de implementación ayuda al desarrollador en todo tipo de plomería que, de otro modo, tendríamos que hacer en ausencia de la palabra clave.

Supongamos que el desarrollador está filtrando una colección, iterando a través de la colección y luego extrayendo esos objetos en una nueva colección. Este tipo de fontanería es bastante monótono.

Más sobre la palabra clave aquí en este artículo .




No creo que nadie lo haya mencionado y espero estar en lo correcto al decir esto, pero un posible problema con la versión 1 del código sería que la conexión de la base de datos permanece abierta mientras no hayas terminado de iterar a través de la colección. .




El uso del rendimiento es similar al retorno de la palabra clave, excepto que devolverá un generador . Y el objeto generador solo atravesará una vez .

el rendimiento tiene dos beneficios:

  1. No necesita leer estos valores dos veces;
  2. Puede obtener muchos nodos secundarios pero no tiene que ponerlos todos en la memoria.

Hay otra explicación clara que quizás te ayude.