Uso apropiado de 'rendimiento de rendimiento'


903

La palabra clave de rendimiento es una de esas palabras clave en C # que continúa desconcertando, y nunca he estado seguro de que la estoy usando correctamente.

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

Versión 1: uso del 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>();
    }
}

38
yieldestá atado IEnumerable<T>y es amable. De alguna manera es una evaluación perezosa
Jaider

Aquí hay una gran respuesta a la pregunta similar. stackoverflow.com/questions/15381708/…
Sanjeev Rai

1
Aquí hay un buen ejemplo de uso: stackoverflow.com/questions/3392612/…
ValGe

66
Veo un buen caso para usar yield returnsi el código que itera a través de los resultados de le GetAllProducts()permite al usuario la posibilidad de cancelar prematuramente el procesamiento.
JMD

2
Encontré este hilo realmente útil: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski

Respuestas:


806

Tiendo a usar el rendimiento-rendimiento cuando calculo el siguiente elemento de la lista (o incluso el siguiente grupo de elementos).

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

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

Otro caso en el que es preferible el rendimiento-rendimiento es si 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 el rendimiento-retorno para devolver la lista de forma incremental.

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


31
No creo que en su ejemplo en la pregunta 3 se combinen dos beneficios. 1) Extiende el costo computacional (a veces un beneficio, a veces no) 2) Puede perezosamente evitar el cálculo indefinidamente en muchos casos de uso. No menciona el inconveniente potencial que mantiene alrededor del estado intermedio. Si tiene cantidades significativas de estado intermedio (digamos un HashSet para eliminación duplicada), el uso del rendimiento puede inflar su huella de memoria.
Kennet Belenky

8
Además, si cada elemento individual es muy grande, pero solo es necesario acceder a ellos secuencialmente, el rendimiento es mejor.
Kennet Belenky

2
Y finalmente ... hay una técnica un poco torpe pero ocasionalmente efectiva para usar el rendimiento para escribir código asincrónico en una forma muy serializada.
Kennet Belenky

12
Otro ejemplo que puede ser interesante es cuando lee archivos CSV bastante grandes. Desea leer cada elemento pero también desea extraer su dependencia. El rendimiento que devuelve un IEnumerable <> le permitirá devolver cada fila y procesar cada fila individualmente. No es necesario leer un archivo de 10 Mb en la memoria. Solo una línea a la vez.
Maxime Rouiller

1
Yield returnparece ser una forma abreviada de escribir su propia clase de iterador personalizado (implementar IEnumerator). Por lo tanto, los beneficios mencionados también se aplican a las clases de iterador personalizadas. De todos modos, ambas construcciones mantienen un estado intermedio. En su forma más simple, se trata de mantener una referencia al objeto actual.
J. Ouwehand

641

Completar una lista temporal es como descargar todo el video, mientras que usarlo yieldes como transmitir ese video.


180
Soy perfectamente consciente de que esta respuesta no es una respuesta técnica, pero creo que la semejanza entre rendimiento y transmisión de video sirve como un buen ejemplo al comprender la palabra clave de rendimiento. Todo lo técnico ya se ha dicho sobre este tema, así que traté de explicar "en otras palabras". ¿Existe una regla comunitaria que diga que no puede explicar sus ideas en términos no técnicos?
anar khalilov

13
No estoy seguro de quién lo rechazó o por qué (desearía que lo hubieran comentado), pero creo que de alguna manera lo describe desde una perspectiva no técnica.
senfo

22
Aún comprendiendo el concepto y esto ayudó a enfocarlo aún más, bonita analogía.
Tony

11
Me gusta esta respuesta, pero no responde la pregunta.
ANeves

73

Como ejemplo conceptual para comprender cuándo debe usar yield, supongamos 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 esto yield, la llamada a ProduceList()puede tomar mucho tiempo porque debe completar la lista antes de regresar:

//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 yield, se reorganiza, trabajando "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, debe usar la Versión 2 porque ya tiene la lista completa de todos modos.


30

Sé que esta es una vieja pregunta, pero me gustaría ofrecer un ejemplo de cómo la palabra clave de rendimiento se puede usar de manera creativa. Realmente me he beneficiado de esta técnica. Esperemos que esto sea de ayuda para cualquier otra persona que se encuentre con esta pregunta.

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

El uso de la palabra clave de rendimiento (junto con la implementación de las rutinas 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 volver a apagar el BusyIndicator.

Así es como funciona: IResult tiene un método de ejecución y un evento completado. 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 los IResults. Cuando se devuelve el primer IResult, la ejecución se detiene dentro de HandleButtonClick (), y BeginExecute () adjunta un controlador de eventos al evento Completed y llama a Execute (). IResult.Execute () puede realizar una tarea sincrónica o asincrónica y dispara el evento Completed cuando se realiza.

LoginResult se parece a esto:

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 ser útil configurar algo como esto y pasar por la ejecución para ver lo que está sucediendo.

¡Espero que esto ayude a alguien! Realmente disfruté explorando las diferentes formas en que se puede usar el rendimiento.


1
su ejemplo de código es un excelente ejemplo sobre cómo usar el rendimiento FUERA de un bloque for o foreach. La mayoría de los ejemplos muestran el rendimiento de rendimiento dentro de un iterador. Muy útil ya que estaba a punto de hacer la pregunta sobre SO ¡Cómo usar el rendimiento fuera de un iterador!
shelbypereira

Nunca se me ha ocurrido usarlo yieldde esta manera. Parece una forma elegante de emular el patrón asíncrono / espera (que supongo que se usaría en lugar de yieldsi se reescribiera hoy). ¿Ha descubierto que estos usos creativos de yieldhan producido rendimientos decrecientes (sin juego de palabras) a lo largo de los años a medida que C # ha evolucionado desde que respondió a esta pregunta? ¿O todavía se te ocurren casos de uso inteligentes y modernizados como este? Y si es así, ¿te importaría compartir otro escenario interesante para nosotros?
Lopsided

27

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


1
La presentación proporciona una descripción general simple. Ray Chen analiza los detalles de cómo funciona en C # en enlaces en stackoverflow.com/a/39507/939250. El primer enlace explica en detalle que hay un segundo retorno implícito al final de los métodos de retorno de rendimiento.
Donal Lafferty

18

El rendimiento de rendimiento puede ser muy poderoso para algoritmos en los que necesita iterar a través de millones de objetos. Considere el siguiente ejemplo donde necesita calcular posibles viajes para compartir viajes. 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, repita cada viaje:

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

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


2
debajo de las mantas ¿qué es el rendimiento? Pensé que era una lista, por lo tanto, ¿cómo mejoraría el uso de la memoria?
llega

1
@rolls yieldfunciona debajo de las cubiertas implementando una máquina de estado internamente. Aquí hay una respuesta SO con 3 publicaciones detalladas en el blog de MSDN que explican la implementación con gran detalle. Escrito por Raymond Chen @ MSFT
Shiva

13

Las dos piezas de código realmente están haciendo dos cosas diferentes. La primera versión atraerá a los miembros cuando los necesite. La segunda versión cargará todos los resultados en la memoria antes de comenzar 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 conjuntos de resultados, especialmente si está ejecutando este código en modo de 32 bits. Me han mordido las excepciones de OutOfMemory varias veces al hacer este método.

Sin embargo, lo clave a tener en cuenta es esto: las diferencias están en la eficiencia. Por lo tanto, probablemente debería elegir el que simplifique su código y cambiarlo solo después de la creación de perfiles.


11

El rendimiento tiene dos grandes usos

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

Ayuda a hacer iteraciones con estado. ( transmisión)

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

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


10

Esto es lo que Chris Sells cuenta sobre esas declaraciones en el lenguaje de programación C # ;

A veces olvido que el rendimiento del rendimiento no es lo mismo que el rendimiento, ya que el código después de un rendimiento puede ejecutarse. 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
}

En contraste, el código después del primer rendimiento devuelto 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, es útil recordar que el rendimiento del rendimiento no es "final" como el rendimiento.


para reducir la ambigüedad, por favor aclare cuando diga can, is that, will o might? ¿podría ser posible que el primero regrese y no ejecute el segundo rendimiento?
Johno Crawford

@JohnoCrawford, la segunda declaración de rendimiento solo se ejecutará si se enumera el segundo / siguiente valor de IEnumerable. Es completamente posible que no ocurra, por ejemplo F().Any(), esto volverá después de intentar enumerar solo el primer resultado. En general, no debe confiar en un IEnumerable yieldcambio de estado del programa, ya que en realidad no puede activarse
Zac Faragher

8

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

El segundo ejemplo es convertir el enumerador / iterador en 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.


8

Esto está un poco fuera del punto, pero dado que la pregunta está etiquetada como mejores prácticas, seguiré adelante y arrojaré mis dos centavos. 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 de 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 pueda tomar un tiempo para hacer su trabajo.


7

¿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 Products implementa IEnumerable (como parece, se usa en una declaración foreach), lo devolvería directamente.


2
Edite OP para incluir más información en lugar de publicar respuestas.
Brian Rasmussen

Bueno, tendrías que decirme qué OP significa exactamente :-) Gracias
petr k.

Publicación original, supongo. No puedo editar publicaciones, así que este parecía ser el camino a seguir.
petr k.

5

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 de método, se le solicitará que envíe la información completa a la persona que llama.

Si la persona 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 utilizar el rendimiento de rendimiento, lo 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 se podría utilizar el rendimiento del rendimiento son:

  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 la GUI: donde el usuario nunca puede llegar a la última página y solo se requiere que se divulgue un subconjunto de información en la página actual

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


3

Devuelve la lista directamente. Beneficios:

  • Esta mas claro
  • La lista es reutilizable. (el iterador no es) no es cierto, gracias Jon

Debería usar el iterador (rendimiento) cuando piense que probablemente no tendrá que iterar hasta el final de la lista, o cuando no tenga fin. Por ejemplo, la llamada del cliente buscará el primer producto que satisfaga algún predicado, puede considerar usar el iterador, aunque ese es un ejemplo artificial, y probablemente haya mejores formas de lograrlo. Básicamente, si sabe de antemano que será necesario calcular la lista completa, simplemente hágalo por adelantado. Si crees que no lo hará, entonces considera usar la versión iteradora.


No olvide que está regresando en IEnumerable <T>, no en IEnumerator <T>; puede llamar a GetEnumerator nuevamente.
Jon Skeet

Incluso si sabe de antemano que será necesario calcular la lista completa, podría ser beneficioso utilizar el rendimiento del rendimiento. Un ejemplo es cuando la colección contiene cientos de miles de artículos.
Val

1

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

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

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


-4

El uso del rendimiento es similar a la palabra clave return , 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 guardarlos todos en la memoria.

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.