Consejos para optimizar programas C # / .NET [cerrado]

78

Parece que la optimización es un arte perdido en estos días. ¿No hubo un momento en que todos los programadores exprimían hasta la última gota de eficiencia de su código? ¿Lo hace a menudo mientras camina cinco millas en la nieve?

En el espíritu de traer de vuelta un arte perdido, ¿cuáles son algunos consejos que conozca para cambios simples (o quizás complejos) para optimizar el código C # / .NET? Dado que es algo tan amplio que depende de lo que uno esté tratando de lograr, ayudaría a proporcionar contexto con su consejo. Por ejemplo:

  • Al concatenar muchas cadenas, utilice StringBuilderen su lugar. Consulte el enlace en la parte inferior para conocer las advertencias al respecto.
  • Úselo string.Comparepara comparar dos cadenas en lugar de hacer algo comostring1.ToLower() == string2.ToLower()

El consenso general que hasta ahora parece estar midiendo es clave. Este tipo de no entiende el punto: medir no le dice qué está mal, o qué hacer al respecto si se encuentra con un cuello de botella. Una vez me encontré con el cuello de botella de la concatenación de cadenas y no tenía idea de qué hacer al respecto, por lo que estos consejos son útiles.

Mi punto para publicar esto es tener un lugar para los cuellos de botella comunes y cómo pueden evitarse incluso antes de toparse con ellos. Ni siquiera se trata necesariamente de un código plug and play que cualquiera deba seguir ciegamente, sino más bien de comprender que se debe pensar en el rendimiento, al menos un poco, y que hay algunos errores comunes a los que hay que prestar atención.

Sin embargo, puedo ver que podría ser útil saber también por qué es útil una sugerencia y dónde debe aplicarse. Para el StringBuilderconsejo, encontré la ayuda que hice hace mucho tiempo aquí en el sitio de Jon Skeet .

Bob
fuente
10
También es importante caminar por la línea entre la optimización y la legibilidad.
Ryan Elkins
3
El "puñado de cuerdas"; el número no es el problema, es si están en una sola declaración de concatenación compuesta o en múltiples declaraciones.
Marc Gravell
2
StringBuilder suele ser más lento que el operador +. El compilador de C # traduce automáticamente el + repetido en las sobrecargas apropiadas de String.Concat.
Richard Berg
2
Tendrás que tener dificultades para luchar contra CLR mientras está optimizando IL en tiempo de ejecución y trataste de hacer lo mismo en tiempo de compilación: tira y afloja. En los viejos tiempos, optimizaba las instrucciones para la máquina y la máquina las ejecutaba de forma tonta.
John K

Respuestas:

106

Parece que la optimización es un arte perdido en estos días.

Hubo una vez al día en que la fabricación de, digamos, microscopios se practicaba como un arte. Los principios ópticos se entendieron mal. No hubo estandarización de piezas. Los tubos, engranajes y lentes tenían que ser hechos a mano por trabajadores altamente calificados.

En estos días, los microscopios se producen como una disciplina de ingeniería. Los principios subyacentes de la física se comprenden muy bien, las piezas listas para usar están ampliamente disponibles y los ingenieros de construcción de microscopios pueden tomar decisiones informadas sobre cómo optimizar mejor su instrumento para las tareas para las que está diseñado.

Que el análisis de la interpretación es un "arte perdido" es algo muy, muy bueno. Ese arte se practicaba como arte . La optimización debe abordarse por lo que es: un problema de ingeniería que se puede resolver mediante la aplicación cuidadosa de principios de ingeniería sólidos.

Me han pedido decenas de veces a lo largo de los años mi lista de "consejos y trucos" que la gente puede utilizar para optimizar su vbscript / su jscript / sus páginas de servidor activo / su VB / su código C #. Siempre me resisto a esto. Hacer hincapié en "consejos y trucos" es exactamente la forma incorrecta de abordar el rendimiento. De esa forma se obtiene un código que es difícil de entender, difícil de razonar, difícil de mantener, que normalmente no es notablemente más rápido que el código sencillo correspondiente.

La forma correcta de abordar el rendimiento es abordarlo como un problema de ingeniería como cualquier otro problema:

  • Establezca metas significativas, medibles y centradas en el cliente.
  • Cree conjuntos de pruebas para probar su rendimiento frente a estos objetivos en condiciones realistas pero controladas y repetibles.
  • Si esas suites muestran que no está cumpliendo sus objetivos, utilice herramientas como los perfiladores para averiguar por qué.
  • Optimice lo que el generador de perfiles identifica como el subsistema de peor rendimiento. Siga perfilando cada cambio para que comprenda claramente el impacto en el rendimiento de cada uno.
  • Repita hasta que suceda una de tres cosas (1) cumpla sus objetivos y envíe el software, (2) revise sus objetivos a la baja a algo que pueda lograr, o (3) su proyecto se cancele porque no pudo cumplir sus objetivos.

Esto es lo mismo que resolvería cualquier otro problema de ingeniería, como agregar una función: establezca objetivos centrados en el cliente para la función, realice un seguimiento del progreso para realizar una implementación sólida, solucione los problemas a medida que los encuentre a través de un análisis de depuración cuidadoso, siga iterando hasta envía o falla. El rendimiento es una característica.

El análisis del desempeño en sistemas modernos complejos requiere disciplina y enfoque en principios sólidos de ingeniería, no en una bolsa llena de trucos que son estrictamente aplicables a situaciones triviales o poco realistas. Nunca he resuelto un problema de rendimiento del mundo real mediante la aplicación de consejos y trucos.

Eric Lippert
fuente
1
Iba a escribir una regla similar, pero la tuya es mejor. Bravo.
Richard Berg
7
Solo hay algunos casos en los que se sabe que existe una mejor manera de realizar la misma tarea sin perder los recursos. No creo que esté perfectamente bien programar como quieras, siempre y cuando cumplas con algún objetivo y parezca que funciona bien. O que lo mejor es programar, luego ejecutar un generador de perfiles y luego regresar y cambiar las áreas problemáticas. ¿Qué hay de malo en que uno tenga una buena idea de lo que se necesita para optimizar ciertos bits de código incluso antes de que comiencen?
Bob
5
@Bob: No hay nada de malo en usar los recursos con inteligencia. Donde las cosas van mal es cuando las personas (1) gastan mucho tiempo (= dinero) en microoptimizaciones que no marcan la diferencia, (2) escriben programas que están mal y (3) escriben programas que no son claros. Para lo que debería optimizar es primero, la corrección. Segundo, buen estilo de codificación. En tercer lugar, rendimiento. Una vez que el código sea correcto y elegante, será mucho más fácil hacerlo efectivo.
Eric Lippert
3
Eso está bien, pero notará que no estoy diciendo que uno no deba codificar la corrección primero, o el estilo en segundo lugar, o lo que sea. Pero también es cierto que a veces (o tal vez muchas veces en estos días), los programadores no tienen en cuenta el rendimiento ni la optimización en absoluto. ¿Es suficiente tener 1 y 2 para compensar un total indiferencia de 3? No veo cómo es una mala idea rendir homenaje a la optimización y aprender un par de cosas sobre lo que se necesita
Bob
7
@Bob: Estoy de acuerdo en que algunos programadores no se preocupan por el rendimiento. Pero no estoy siguiendo tu punto. Una lista de consejos y trucos no los convertirá de repente en personas que se preocupan por el rendimiento. Suponiendo, en aras del argumento, que puede convertir a personas que actualmente no están interesadas en personas interesadas, una lista de consejos y trucos no las ayudará a lograr un buen desempeño. Puede aplicar consejos y trucos a un cuerpo de código durante todo el día y nunca saber si está progresando en absoluto en relación con sus objetivos. Tienes que tener metas y medir tu progreso.
Eric Lippert
45

Consiga un buen perfilador.

No se moleste en intentar optimizar C # (en realidad, cualquier código) sin un buen generador de perfiles. De hecho, es de gran ayuda tener un generador de perfiles de rastreo y de muestreo a mano.

Sin un buen generador de perfiles, es probable que cree optimizaciones falsas y, lo que es más importante, optimice rutinas que no son un problema de rendimiento en primer lugar.

Los primeros tres pasos para la elaboración de perfiles siempre deben ser 1) Medir, 2) medir y luego 3) medir ....

Reed Copsey
fuente
1
Yo diría, no mida , capture . stackoverflow.com/questions/406760/…
Mike Dunlavey
23
Lo olvidaste4) measure
Nifle
1
@Nifle: Si estás cazando elefantes, ¿necesitas medirlos?
Mike Dunlavey
1
@RobbieDee: Vea la respuesta de Conrad Albrecht .
Mike Dunlavey
1
@MikeDunlavey Lo siento, me estaba divirtiendo un poco contigo, pero gracias ... :-)
Robbie Dee
21

Pautas de optimización:

  1. No lo hagas a menos que lo necesites
  2. No lo haga si es más barato lanzar nuevo hardware al problema en lugar de un desarrollador
  3. No lo haga a menos que pueda medir los cambios en un entorno de producción equivalente
  4. No lo haga a menos que sepa cómo usar una CPU y un generador de perfiles de memoria
  5. No lo hagas si hará que tu código sea ilegible o imposible de mantener.

A medida que los procesadores continúan haciéndose más rápidos, el principal cuello de botella en la mayoría de las aplicaciones no es la CPU, es el ancho de banda: ancho de banda a la memoria fuera del chip, ancho de banda al disco y ancho de banda a la red.

Empiece por el otro extremo: utilice YSlow para ver por qué su sitio web es lento para los usuarios finales, luego retroceda y corrija los accesos a la base de datos para que no sean demasiado anchos (columnas) ni demasiado profundos (filas).

En los casos muy raros en los que vale la pena hacer algo para optimizar el uso de la CPU, tenga cuidado de no afectar negativamente el uso de la memoria: he visto 'optimizaciones' en las que los desarrolladores han intentado usar la memoria para almacenar en caché los resultados para ahorrar ciclos de CPU. El efecto neto fue reducir la memoria disponible para almacenar en caché las páginas y los resultados de la base de datos, lo que hizo que la aplicación se ejecutara mucho más lentamente. (Vea la regla sobre la medición).

También he visto casos en los que un algoritmo "tonto" no optimizado ha vencido a un algoritmo optimizado "inteligente". Nunca subestime lo buenos que se han vuelto los escritores de compiladores y diseñadores de chips para convertir el código de bucle "ineficiente" en código súper eficiente que puede ejecutarse completamente en la memoria del chip con canalización. Su algoritmo 'inteligente' basado en árbol con un bucle interno desenvuelto que cuenta hacia atrás que pensó que era 'eficiente' puede ser superado simplemente porque no pudo permanecer en la memoria del chip durante la ejecución. (Vea la regla sobre la medición).

Ian Mercer
fuente
10
Del mismo modo, no se obsesione con el análisis big-O. El algoritmo de búsqueda de cadenas ingenuas de O (nm) es, para casos comerciales comunes, miles de veces más rápido que los algoritmos O (n + m) que preprocesan las cadenas de búsqueda en busca de patrones. La búsqueda de cadenas ingenuas que coinciden con el primer carácter a menudo se compila en una sola instrucción de máquina, lo que es increíblemente rápido en los procesadores modernos que hacen un uso intensivo de cachés de memoria optimistas.
Eric Lippert
16

Cuando trabaje con ORM, tenga en cuenta las selecciones N + 1.

List<Order> _orders = _repository.GetOrders(DateTime.Now);
foreach(var order in _orders)
{
    Print(order.Customer.Name);
}

Si los clientes no se cargan con entusiasmo, esto podría resultar en varios viajes de ida y vuelta a la base de datos.

Aaron
fuente
14
  • No uses números mágicos, usa enumeraciones
  • No codifique valores
  • Use genéricos siempre que sea posible, ya que es seguro para tipos y evita el empaquetado y desempaquetado
  • Utilice un controlador de errores donde sea absolutamente necesario
  • Desechar, desechar, desechar. CLR no sabe cómo cerrar las conexiones de su base de datos, así que ciérrelas después de usarlas y elimine los recursos no administrados.
  • ¡Usa el sentido común!
SoftwareGeek
fuente
15
Por mucho que esté de acuerdo en que son cosas buenas para hacer, las dos primeras cosas aquí no tienen ningún impacto en el rendimiento, solo la capacidad de mantenimiento ...
Reed Copsey
cierto, pero sigue siendo un código optimizado.
SoftwareGeek
4
Además, el tercero (boxeo) rara vez es un verdadero punto de pellizco; se exagera como un problema; al igual que las excepciones, no suele ser un problema.
Marc Gravell
1
"pero sigue siendo un código optimizado", esa es una gran afirmación; lo único que esperaría que fuera un problema importante es "disponer"; y es más probable que surja como excepciones (fuera de los identificadores, etc.), no como degradación del rendimiento.
Marc Gravell
En realidad, el patrón del finalizador es bastante malo si la optimización es su objetivo. Los objetos con finalizadores se promueven automáticamente a Gen-1 (o peor). Además, forzar su código de finalizador para que se ejecute en el subproceso GC generalmente no es óptimo si hay algo remotamente caro en esa lista de Todo. En pocas palabras: es una característica dirigida a la conveniencia y la corrección, no una diseñada para la velocidad bruta. Detalles: msdn.microsoft.com/en-us/magazine/bb985010.aspx
Richard Berg
9

De acuerdo, debo agregar mi favorito: si la tarea es lo suficientemente larga para la interacción humana, use una pausa manual en el depurador.

Vs. un generador de perfiles, esto le brinda una pila de llamadas y valores variables que puede usar para comprender realmente lo que está sucediendo.

Haga esto de 10 a 20 veces y tendrá una buena idea de qué optimización realmente podría marcar la diferencia.

Conrad Albrecht
fuente
1
++ Amén. He estado haciendo eso desde antes de que existieran los perfiladores. ¡Y tu programa DrawMusic se ve increíble!
Mike Dunlavey
6
Esto es esencialmente lo que hacen los perfiladores, excepto que lo hacen mejor que tú de mil formas diferentes (más rápido, más a menudo, más preciso, etc.). También dan pilas de llamadas. Ésta es la solución del pobre (y del anciano que tiene miedo de aprender cosas nuevas).
BlueRaja - Danny Pflughoeft
@ BlueRaja-DannyPflughoeft: Te engañan. Te dicen con gran precisión que no hay mucho que hacer. La diferencia entre este método y los perfiladores es que en este método puede ver cosas para acelerar que no se pueden extraer de simples estadísticas. En su lugar, toman miles de muestras cuando la información que puede llevarlo al problema es evidente en los primeros 10 si realmente puede ver las muestras sin procesar. Seguro que has visto esta publicación .
Mike Dunlavey
@ BlueRaja-DannyPflughoeft: Mira los resultados. ¿Cuál es la mayor proporción de aceleración que ha obtenido con un generador de perfiles?
Mike Dunlavey
2
@ BlueRaja-DannyPflughoeft: Estoy seguro de que no lo harías, y cuando llegues a mi edad te encontrarás con personas como tú. Pero dejemos eso de lado. Aquí hay algo de código fuente. Si puede acelerarlo en 3 órdenes de magnitud, sin mirar cómo lo hice, usando cualquier otro método, tendrá derecho a alardear :)
Mike Dunlavey
9

Si identifica un método como un cuello de botella, pero no sabe qué hacer al respecto , básicamente está atascado.

Así que enumeraré algunas cosas. Todas estas cosas no son soluciones mágicas y aún tendrás que perfilar tu código. Solo estoy haciendo sugerencias sobre cosas que podría hacer y, a veces, puede ayudar. Especialmente los tres primeros son importantes.

  • Intente resolver el problema utilizando solo (o: principalmente) tipos de bajo nivel o matrices de ellos.
  • Los problemas suelen ser pequeños: el uso de un algoritmo inteligente pero complejo no siempre lo hace ganar, especialmente si el algoritmo menos inteligente se puede expresar en un código que solo usa (matrices de) tipos de bajo nivel. Tomemos, por ejemplo, InsertionSort vs MergeSort para n <= 100 o el algoritmo de búsqueda Dominator de Tarjan frente al uso de vectores de bits para resolver ingenuamente la forma de flujo de datos del problema para n <= 100. (el 100 es, por supuesto, solo para darte una idea: ¡ perfil !)
  • Considere escribir un caso especial que pueda resolverse usando solo tipos de bajo nivel (a menudo casos de problemas de tamaño <64), incluso si tiene que mantener el otro código para casos de problemas más grandes.
  • Aprenda aritmética bit a bit para ayudarle con las dos ideas anteriores.
  • BitArray puede ser su amigo, en comparación con el Diccionario o, peor aún, la Lista. Pero tenga en cuenta que la implementación no es óptima; Puede escribir una versión más rápida usted mismo. En lugar de probar que sus argumentos están fuera de rango, etc., a menudo puede estructurar su algoritmo para que el índice no pueda salir del rango de todos modos, pero no puede eliminar la verificación del BitArray estándar y no es gratis .
  • Como ejemplo de lo que puede hacer con solo arreglos de tipos de bajo nivel, BitMatrix es una estructura bastante poderosa que se puede implementar como solo un arreglo de ulongs e incluso puede atravesarlo usando un ulong como "frente" porque puede tomar el bit de orden más bajo en tiempo constante (comparado con la cola en la primera búsqueda de amplitud, pero obviamente el orden es diferente y depende del índice de los elementos en lugar de simplemente el orden en el que los encuentre).
  • La división y el módulo son realmente lentos a menos que el lado derecho sea una constante.
  • Las matemáticas de coma flotante ya no son en general más lentas que las matemáticas de números enteros (no es "algo que puedes hacer", sino "algo que puedes omitir")
  • La ramificación no es gratuita . Si puede evitarlo usando una aritmética simple (cualquier cosa menos división o módulo), a veces puede obtener algo de rendimiento. Mover una rama fuera de un bucle casi siempre es una buena idea.
harold
fuente
Hay algunas cosas buenas que me ayudaron mucho, ¡gracias!
Robbie Dee
8

La gente tiene ideas divertidas sobre lo que realmente importa. Stack Overflow está lleno de preguntas sobre, por ejemplo, es ++imás "eficiente" que i++. Aquí hay un ejemplo de ajuste de rendimiento real , y es básicamente el mismo procedimiento para cualquier idioma. Si el código simplemente se escribe de cierta manera "porque es más rápido", eso es adivinar.

Claro, no escribe código estúpido a propósito, pero si adivinar funciona, no habría necesidad de perfiladores ni técnicas de creación de perfiles.

Mike Dunlavey
fuente
6

La verdad es que no existe el código optimizado perfecto. Sin embargo, puede optimizar para una parte específica del código, en un sistema conocido (o conjunto de sistemas) en un tipo (y recuento) de CPU conocido, una plataforma conocida (¿Microsoft? ¿ Mono ?), Una versión de marco / BCL conocida , una versión CLI conocida, una versión del compilador conocida (errores, cambios de especificación, ajustes), una cantidad conocida de memoria total y disponible, un origen de ensamblaje conocido ( ¿ GAC ? ¿disco? ¿remoto?), con actividad del sistema en segundo plano conocida de otros procesos.

En el mundo real, use un generador de perfiles y observe los bits importantes; Por lo general, las cosas obvias son cualquier cosa que involucre E / S, cualquier cosa que involucre subprocesos (nuevamente, esto cambia enormemente entre versiones) y cualquier cosa que involucre bucles y búsquedas, pero es posible que se sorprenda de lo que el código "obviamente malo" no es realmente un problema, y qué código "obviamente bueno" es un gran culpable.

Peter Mortensen
fuente
5

Dígale al compilador qué hacer, no cómo hacerlo. Por ejemplo, foreach (var item in list)es mejor que for (int i = 0; i < list.Count; i++)y m = list.Max(i => i.value);es mejor que list.Sort(i => i.value); m = list[list.Count - 1];.

Al decirle al sistema lo que quiere hacer, puede descubrir la mejor manera de hacerlo. LINQ es bueno porque sus resultados no se calculan hasta que los necesita. Si solo usa el primer resultado, no tiene que calcular el resto.

En última instancia (y esto se aplica a toda la programación) minimiza los bucles y minimiza lo que haces en los bucles. Aún más importante es minimizar la cantidad de bucles dentro de sus bucles. ¿Cuál es la diferencia entre un algoritmo O (n) y un algoritmo O (n ^ 2)? El algoritmo O (n ^ 2) tiene un bucle dentro de un bucle.

Gabe
fuente
Ironacly LINQ agrega salchicha extra, y uno debería preguntarse si existe una solución sin ella.
user3800527
2

Realmente no trato de optimizar mi código, pero a veces lo reviso y uso algo como un reflector para devolver mis programas a la fuente. Es interesante comparar luego lo que me equivoqué con lo que producirá el reflector. A veces encuentro que lo que hice en una forma más complicada se simplificó. Puede que no optimice las cosas, pero me ayuda a ver soluciones más simples a los problemas.

Aaron Havens
fuente