Consejos para optimizar los progtwigs C # / .NET

Parece que la optimización es un arte perdido en estos días. ¿No hubo un momento en que todos los progtwigdores exprimieron cada gramo de eficiencia de su código? A menudo hacerlo mientras camina cinco millas en la nieve?

Con el ánimo de traer de vuelta un arte perdido, ¿cuáles son algunos consejos que conoces para cambios simples (o quizás complejos) para optimizar el código C # / .NET? Dado que es una cosa tan amplia que depende de lo que se intenta lograr, ayudaría a contextualizar su sugerencia. Por ejemplo:

  • Cuando concatene muchas cadenas, use StringBuilder lugar. Ver el enlace en la parte inferior para advertencias sobre esto.
  • Use string.Compare para comparar dos cadenas en lugar de hacer algo como string1.ToLower() == string2.ToLower()

El consenso general que hasta ahora parece estar midiendo es clave. Este tipo de error pasa por alto: la medición no te dice qué es lo que está mal ni qué hacer al respecto si te encuentras con un cuello de botella. Me encontré con el cuello de botella de concatenación una vez y no tenía idea de qué hacer al respecto, por lo que estos consejos son útiles.

Mi punto para incluso publicar esto es tener un lugar para los cuellos de botella comunes y cómo se pueden evitar incluso antes de toparse con ellos. No se trata necesariamente de un código de plug and play que cualquiera debería seguir ciegamente, sino más bien de comprender que se debe pensar en el rendimiento, al menos en cierta medida, y que hay algunas fallas comunes que hay que tener en cuenta.

Puedo ver que también podría ser útil saber por qué una propina es útil y dónde debe aplicarse. Para el consejo de StringBuilder encontré la ayuda que hice hace mucho tiempo aquí en el sitio de Jon Skeet .

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

Hubo una vez al día cuando la fabricación de, por ejemplo, microscopios se practicaba como un arte. Los principios ópticos no se entendieron bien. No hubo estandarización de partes. Los tubos, engranajes y lentes tenían que estar hechos a mano por trabajadores altamente calificados.

En la actualidad, los microscopios se producen como una disciplina de ingeniería. Los principios subyacentes de la física son muy bien entendidos, las piezas comerciales están ampliamente disponibles y los ingenieros constructores de microscopios pueden tomar decisiones informadas sobre cómo optimizar su instrumento para las tareas para las que está diseñado.

Ese análisis de rendimiento es un “arte perdido” es algo muy, muy bueno. Ese arte fue practicado como un 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 sólidos principios de ingeniería.

Me han preguntado docenas de veces a lo largo de los años sobre mi lista de “consejos y trucos” que las personas pueden usar para optimizar su vbscript / su jscript / sus páginas activas del servidor / su VB / código C #. Siempre me resisto a esto. Enfatizar “consejos y trucos” es exactamente la forma incorrecta de enfocar el rendimiento. De esta forma se obtiene un código que es difícil de entender, difícil de razonar, difícil de mantener, que típicamente no es notablemente más rápido que el código directo correspondiente.

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

  • Establezca metas significativas, mensurables y centradas en el cliente.
  • Cree suites de prueba para evaluar su desempeño en relación con estos objectives en condiciones realistas pero controladas y repetibles.
  • Si esas suites demuestran que no estás logrando tus objectives, utiliza herramientas como los perfiladores para descubrir por qué.
  • Optimice el diapasón de lo que el perfilador identifica como el subsistema de peor rendimiento. Mantenga el perfil de cada cambio para que comprenda claramente el impacto en el rendimiento de cada uno.
  • Repita hasta que ocurra una de tres cosas (1) cumpla con sus objectives y envíe el software, (2) revise sus objectives hacia abajo para lograr algo, o (3) su proyecto sea cancelado porque no podría alcanzar sus objectives.

Esto es lo mismo que resolvería cualquier otro problema de ingeniería, como agregar una característica: establecer objectives centrados en el cliente para la función, seguir el progreso en una implementación sólida, solucionar problemas a medida que los encuentra a través de un análisis de depuración cuidadoso, seguir iterando hasta usted envía o falla. El rendimiento es una característica.

El análisis de rendimiento en sistemas modernos complejos requiere disciplina y enfoque en sólidos principios 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 en el mundo real mediante la aplicación de consejos y trucos.

Obtener un buen generador de perfiles.

No se moleste en tratar de optimizar C # (realmente, cualquier código) sin un buen generador de perfiles. En realidad, ayuda mucho tener a mano un rastreador de muestreo y un rastreador.

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

Los primeros tres pasos para crear perfiles siempre deben ser 1) Medir, 2) medir y luego 3) medir …

Pautas de optimización:

  1. No lo hagas a menos que necesites
  2. No lo hagas si es más barato lanzar un 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 hagas a menos que sepas cómo usar una CPU y un generador de perfiles de memoria
  5. No lo hagas si va a hacer que tu código sea ilegible o no se pueda leer

A medida que los procesadores continúan acelerando, el principal cuello de botella en la mayoría de las aplicaciones no es la CPU, sino su ancho de banda: ancho de banda a la memoria fuera del chip, ancho de banda al disco y ancho de banda a la red.

Comience por el otro extremo: use 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 raros casos en que vale la pena hacer algo para optimizar el uso de la CPU, tenga cuidado de no tener un impacto negativo en el uso de la memoria: he visto ‘optimizaciones’ donde los desarrolladores han tratado de usar la memoria para almacenar resultados en la memoria caché 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 más lentamente! (Ver la regla sobre medición).

También he visto casos en los que un algoritmo “tonto” no optimizado ha superado a un algoritmo “inteligente” optimizado. Nunca subestimes lo buenos que se han vuelto los comstackdores-escritores y los diseñadores de chips para convertir el código de bucle “ineficiente” en un código súper eficiente que puede ejecutarse por completo en la memoria en el chip con el pipeline. Su algoritmo ‘inteligente’ basado en un árbol con un bucle interno desenrollado contando hacia atrás que usted pensó era ‘eficiente’ puede ser vencido simplemente porque no pudo permanecer en la memoria del chip durante la ejecución. (Ver la regla sobre medición).

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

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

Si los clientes no son cargados ansiosamente, esto podría generar varios viajes redondos a la base de datos.

  • No use números mágicos, use enumeraciones
  • No codificar valores
  • Use generics cuando sea posible, ya que es seguro y evita el boxeo y el desempaquetado
  • Use un controlador de errores donde sea absolutamente necesario
  • Deseche, elimine, elimine. 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!

De acuerdo, tengo que agregar mi favorito: si la tarea es lo suficientemente larga para la interacción humana, utilice un corte manual en el depurador.

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

Haga esto de 10 a 20 veces y obtendrá una buena idea de lo que la optimización realmente podría marcar la diferencia.

Si identificas un método como un cuello de botella, pero no sabes qué hacer al respecto , esencialmente estás atrapado.

Entonces enumeraré algunas cosas. Todas estas cosas no son balas de plata y aún tendrá que perfilar su código. Solo estoy haciendo sugerencias sobre cosas que podrías hacer y, a veces, puedo ayudar. Especialmente los primeros tres son importantes.

  • Intente resolver el problema utilizando solo (o principalmente) tipos de bajo nivel o matrices de ellos.
  • Los problemas suelen ser pequeños: usar un algoritmo inteligente pero complejo no siempre te hace ganar, especialmente si el algoritmo menos inteligente se puede express en código que solo usa (matrices de) tipos de bajo nivel. Tomemos como ejemplo InsertionSort vs MergeSort para n <= 100 o el algoritmo de búsqueda de Tarjan's Dominator versus el uso de bitvectors para resolver ingenuamente la forma de flujo de datos del problema para n <= 100. (el 100 es, por supuesto, solo para darle una idea, ¡ perfil !)
  • Considere escribir un caso especial que pueda resolverse usando solo tipos de bajo nivel (a menudo instancias de problema de tamaño <64), incluso si tiene que mantener el otro código para casos de problemas mayores.
  • Aprenda aritmética bit a bit para ayudarlo con las dos ideas anteriores.
  • BitArray puede ser tu amigo, en comparación con el diccionario o, peor aún, la lista. Pero ten cuidado 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 del scope, a menudo puede estructurar su algoritmo para que el índice no pueda salir del rango de todos modos, pero no puede eliminar el cheque del BitArray estándar y no es gratuito .
  • Como ejemplo de lo que puedes hacer con solo arreglos de tipos de bajo nivel, la BitMatrix es una estructura bastante poderosa que se puede implementar como una serie de ulongs e incluso puedes recorrerla usando un ulong como “frontal” porque puedes tomar el bit de orden más bajo en tiempo constante (en comparación con la búsqueda en cola en la primera búsqueda, pero obviamente el orden es diferente y depende del índice de los elementos en lugar de simplemente el orden en que los encuentra).
  • La división y el módulo son muy lentos a menos que el lado derecho sea una constante.
  • Las matemáticas de punto flotante no son en general más lentas que las matemáticas enteras (no “algo que puedes hacer”, sino “algo que puedes saltarte haciendo”)
  • La ramificación no es gratis . Si puede evitarlo usando una aritmética simple (cualquier cosa menos división o módulo) a veces puede obtener algún rendimiento. Mover una twig a un bucle es casi siempre una buena idea.

La gente tiene ideas graciosas sobre lo que realmente importa. Stack Overflow está lleno de preguntas sobre, por ejemplo, que ++i más “performant” 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 escribes a propósito un código estúpido, pero si las conjeturas funcionasen, no habría necesidad de perfiladores y técnicas de creación de perfiles.

Dile al comstackdor qué hacer, no cómo hacerlo. Como 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]; 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 computan hasta que los necesite. Si solo usa el primer resultado, no tiene que calcular el rest.

En última instancia (y esto se aplica a toda la progtwigción) minimice los bucles y minimice lo que hace en bucles. Aún más importante es minimizar el número 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.

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

En el mundo real, use un generador de perfiles y observe los bits importantes; generalmente las cosas obvias son cualquier cosa que involucre E / S, cualquier cosa que involucre threading (de nuevo, esto cambia enormemente entre versiones), y cualquier cosa que involucre bucles y búsquedas, pero podría sorprenderse de que el código “obviamente malo” no sea realmente un problema, y qué código “obviamente bueno” es un gran culpable.

Realmente no trato de optimizar mi código, pero a veces voy a utilizar algo como reflector para volver a poner mis progtwigs en el origen. Es interesante comparar lo que estoy mal con lo que producirá el reflector. Algunas veces encuentro que lo que hice en una forma más complicada fue simplificado. Puede que no optimice las cosas, pero me ayuda a ver soluciones más simples a los problemas.