¿Cuál es la pesimismo más ridícula que has visto?

Todos sabemos que la optimización prematura es la raíz de todo mal, ya que conduce a un código ilegible / inmanejable. Peor aún es la pesimismo, cuando alguien implementa una “optimización” porque piensan que será más rápido, pero termina siendo más lento, además de tener fallas, ser imposible de mantener, etc. ¿Cuál es el ejemplo más ridículo de esto que has visto? ?

En un proyecto anterior heredamos algunos (por lo demás excelentes) progtwigdores de sistemas integrados que tenían una experiencia masiva de Z-8000.

Nuestro nuevo entorno fue Sparc Solaris de 32 bits.

Uno de los muchachos fue y cambió todas las posiciones por pantalones cortos para acelerar nuestro código, ya que tomar 16 bits de la RAM era más rápido que capturar 32 bits.

Tuve que escribir un progtwig de demostración para mostrar que capturar valores de 32 bits en un sistema de 32 bits era más rápido que tomar valores de 16 bits, y explicar que para obtener un valor de 16 bits la CPU tenía que hacer un ancho de 32 bits. acceso a la memoria y luego enmascarar o cambiar los bits no necesarios para el valor de 16 bits.

Las bases de datos son pesimización playland.

Los favoritos incluyen:

  • Divida una tabla en múltiplos (por rango de fechas, rango alfabético, etc.) porque es “demasiado grande”.
  • Cree una tabla de archivo para registros retirados, pero continúe UNIÉNDALO con la tabla de producción.
  • Duplicar bases de datos completas por (división / cliente / producto / etc.)
  • Resistencia al agregar columnas a un índice porque lo hace demasiado grande.
  • Cree muchas tablas de resumen porque volver a calcular a partir de datos sin procesar es demasiado lento.
  • Crea columnas con subcampos para ahorrar espacio.
  • Denormalize en fields-as-an-array.

Eso está fuera de mi cabeza.

Creo que no hay una regla absoluta: algunas cosas se optimizan mejor por adelantado y otras no.

Por ejemplo, trabajé en una empresa donde recibimos paquetes de datos de satélites. Cada paquete cuesta mucho dinero, por lo que todos los datos fueron altamente optimizados (es decir, empaquetados). Por ejemplo, la latitud / longitud no se envió como valores absolutos (flotantes), sino como desplazamientos relativos a la esquina “noroeste” de una zona “actual”. Tuvimos que descomprimir todos los datos antes de que pudieran usarse. Pero creo que esto no es pesimismo, es una optimización inteligente para reducir los costos de comunicación.

Por otro lado, nuestros arquitectos de software decidieron que los datos desempaquetados deberían formatearse en un documento XML muy legible y almacenarse en nuestra base de datos como tal (en lugar de tener cada campo almacenado en una columna correspondiente). Su idea era que “XML es el futuro”, “el espacio en disco es barato” y “el procesador es barato”, por lo que no había necesidad de optimizar nada. El resultado fue que nuestros paquetes de 16 bytes se convirtieron en documentos de 2kB almacenados en una columna, y para consultas simples, ¡teníamos que cargar megabytes de documentos XML en la memoria! Recibimos más de 50 paquetes por segundo, por lo que puede imaginarse cuán horrible se volvió el rendimiento (por cierto, la empresa quebró).

Entonces, de nuevo, no hay una regla absoluta. Sí, a veces la optimización demasiado temprano es un error. Pero a veces el lema “CPU / espacio en el disco / memoria es barato” es la verdadera raíz de todo mal.

Oh, Dios mío, creo que los he visto a todos. La mayoría de las veces, es un esfuerzo para solucionar problemas de rendimiento por alguien demasiado perezoso para solucionar los problemas de CAUSA de esos problemas de rendimiento o incluso para investigar si realmente hay un problema de rendimiento. En muchos de estos casos me pregunto si no es solo el caso de esa persona que quiere probar una tecnología en particular y está buscando desesperadamente un clavo que se ajuste a su nuevo y shiny martillo.

Aquí hay un ejemplo reciente:

El arquitecto de datos viene a mí con una propuesta elaborada para particionar verticalmente una tabla clave en una aplicación bastante grande y compleja. Él quiere saber qué tipo de esfuerzo de desarrollo sería necesario para ajustarse al cambio. La conversación fue así:

Yo: ¿Por qué estás considerando esto? ¿Cuál es el problema que estás tratando de resolver?

Él: la Tabla X es demasiado amplia, la estamos dividiendo por motivos de rendimiento.

Yo: ¿Qué te hace pensar que es demasiado ancho?

Él: el consultor dijo que son demasiadas columnas para tener en una mesa.

Yo: ¿ Y esto está afectando el rendimiento?

Él: Sí, los usuarios han informado de ralentizaciones intermitentes en el módulo XYZ de la aplicación.

Yo: ¿cómo sabes que el ancho de la mesa es la fuente del problema?

Él: Esa es la tabla clave utilizada por el módulo XYZ, y es como 200 columnas. Debe ser el problema.

Yo (explicando): Pero el módulo XYZ, en particular, usa la mayoría de las columnas en esa tabla, y las columnas que utiliza son impredecibles porque el usuario configura la aplicación para mostrar los datos que desea mostrar de esa tabla. Es probable que el 95% de las veces termináramos uniendo todas las mesas de nuevo de todos modos, lo que perjudicaría el rendimiento.

Él: el consultor dijo que es demasiado amplio y que necesitamos cambiarlo.

Yo: ¿Quién es este consultor? No sabía que contratamos a un consultor, ni hablaron con el equipo de desarrollo en absoluto.

Él: Bueno, todavía no los hemos contratado. Esto es parte de una propuesta que ofrecieron, pero insistieron en que necesitábamos volver a diseñar esta base de datos.

Yo: Uh huh. Entonces, el consultor que vende servicios de rediseño de bases de datos piensa que necesitamos un rediseño de la base de datos …

La conversación siguió y siguió así. Después, eché otro vistazo a la tabla en cuestión y determiné que probablemente podría reducirse con una normalización simple sin necesidad de estrategias de partición exóticas. Esto, por supuesto, resultó ser un punto discutible una vez que investigué los problemas de desempeño (no reportados anteriormente) y los rastreé a dos factores:

  1. Faltan índices en algunas columnas clave.
  2. Algunos analistas de datos fraudulentos que bloqueaban periódicamente las tablas clave (incluida la “demasiado amplia”) al consultar la base de datos de producción directamente con MSAccess.

Por supuesto, el arquitecto todavía está presionando por una partición vertical de la tabla que cuelga del metaproblema “demasiado ancho”. Incluso reforzó su caso al obtener una propuesta de otro consultor de bases de datos que pudo determinar que necesitábamos cambios de diseño importantes en la base de datos sin mirar la aplicación o ejecutar ningún análisis de rendimiento.

He visto personas que usan alphadrive-7 para incubar totalmente CHX-LT. Esta es una práctica poco común. La práctica más común es inicializar el transformador ZT para que la amortiguación se reduzca (debido a una mayor resistencia de sobrecarga neta) y crear bytegraphications de estilo java.

¡Totalmente pesimista!

No hay nada que rompa la tierra, lo admito, pero he atrapado a personas que usan StringBuffer para concatenar cadenas fuera de un bucle en Java. Fue algo simple como girar

String msg = "Count = " + count + " of " + total + "."; 

dentro

 StringBuffer sb = new StringBuffer("Count = "); sb.append(count); sb.append(" of "); sb.append(total); sb.append("."); String msg = sb.toString(); 

Solía ​​ser una práctica bastante común usar la técnica en un bucle, porque era considerablemente más rápido. El caso es que StringBuffer está sincronizado, por lo que hay gastos adicionales si solo concatena algunas cadenas. (Sin mencionar que la diferencia es absolutamente trivial en esta escala.) Otros dos puntos sobre esta práctica:

  1. StringBuilder no está sincronizado, por lo que debe preferirse a StringBuffer en los casos en que no se pueda invocar su código desde varios subprocesos.
  2. Los comstackdores modernos de Java convertirán la concatenación de cadenas legible en bytecode optimizado para usted cuando sea apropiado de todos modos.

Una vez vi una base de datos MSSQL que usaba una tabla ‘Root’. La tabla raíz tenía cuatro columnas: GUID (uniqueidentifier), ID (int), LastModDate (datetime) y CreateDate (datetime). Todas las tablas en la base de datos fueron Clave externa a la tabla raíz. Cada vez que se creaba una nueva fila en cualquier tabla en la base de datos, tenía que usar un par de procedimientos almacenados para insertar una entrada en la tabla raíz antes de poder acceder a la tabla real que le interesaba (en lugar de la base de datos que realizaba el trabajo usted con algunos desencadenadores desencadenadores simples).

Esto creó un desorden inútil y dolores de cabeza, requirió que todo lo escrito encima usara sprocs (y eliminé mis esperanzas de presentar LINQ a la compañía. Era posible pero no valía la pena el dolor de cabeza), y para colmo no lo hice incluso logra lo que se suponía que debía hacer.

El desarrollador que eligió este camino lo defendió bajo la suposición de que esto ahorró toneladas de espacio porque no estábamos usando Guids en las tablas en sí (pero … ¿no es un GUID generado en la tabla Root para cada fila que hacemos?) , mejoró el rendimiento de alguna manera y facilitó la auditoría de los cambios en la base de datos.

Ah, y el diagtwig de la base de datos parecía una araña mutante del infierno.

¿Qué hay de POBI – pesimismo, obviamente, por intención?

Mi colega mío en los 90 estaba cansado de que el CEO lo pateara en el culo solo porque el CEO pasó el primer día de cada lanzamiento del software ERP (uno personalizado) con la localización de problemas de rendimiento en las nuevas funcionalidades. Incluso si las nuevas funcionalidades machacaban gigabytes e imposibilitaban lo imposible, siempre encontraba algún detalle, o incluso un tema aparentemente importante, sobre los que quejarse. Él creía saber mucho sobre progtwigción y obtenía sus patadas pateando culos de progtwigdor.

Debido a la naturaleza incompetente de la crítica (él era un CEO, no un informático), mi colega nunca logró hacerlo bien. Si no tienes un problema de rendimiento, no puedes eliminarlo …

Hasta que por un lanzamiento, puso muchas llamadas a función Delay (200) (era Delphi) en el nuevo código. Tomó solo 20 minutos después de la puesta en funcionamiento, y le ordenaron aparecer en la oficina del CEO para buscar sus insultos vencidos en persona.

Lo único inusual hasta ahora fueron mis colegas mudos cuando regresó, sonriendo, bromeando, yendo a por BigMac o dos, mientras que él normalmente pateaba mesas, llamaba al CEO y a la compañía, y pasaba el rest del día rechazado hasta la muerte .

Naturalmente, mi colega descansaba uno o dos días en su escritorio, mejorando sus habilidades de puntería en Quake; luego, en el segundo o tercer día borró las llamadas de Delay, reconstruyó y lanzó un “parche de emergencia” del cual difundió la palabra que había pasado 2 días y 1 noche para arreglar los agujeros de rendimiento.

Esta fue la primera (y única) vez que el malvado CEO dijo “¡buen trabajo!” a él. Eso es todo lo que cuenta, ¿verdad?

Esto fue real POBI.

Pero también es un tipo de optimización de procesos sociales, por lo que está 100% bien.

Creo.

“Independencia de la base de datos”. Esto significaba que no había procs, triggers, etc. almacenados, ni siquiera claves externas.

 var stringBuilder = new StringBuilder(); stringBuilder.Append(myObj.a + myObj.b + myObj.c + myObj.d); string cat = stringBuilder.ToString(); 

El mejor uso de un StringBuilder que he visto.

Usar una expresión regular para dividir una cadena cuando basta un string.split simple

Nadie parece haber mencionado la clasificación, así que lo haré.

Varias veces, he descubierto que alguien había hecho una explosión de burbujas a mano, porque la situación “no requería” una llamada al algoritmo de solución rápida “demasiado sofisticado” que ya existía. El desarrollador se mostró satisfecho cuando su bubble-set artesanal funcionó lo suficientemente bien en las diez filas de datos que están utilizando para las pruebas. No pasó tan bien después de que el cliente agregó un par de miles de filas.

Muy tarde para este hilo lo sé, pero lo vi recientemente:

 bool isFinished = GetIsFinished(); switch (isFinished) { case true: DoFinish(); break; case false: DoNextStep(); break; default: DoNextStep(); } 

Ya sabes, solo en caso de que un booleano tenga algunos valores extra …

El peor ejemplo que puedo pensar es una base de datos interna en mi empresa que contiene información sobre todos los empleados. Recibe una actualización nocturna de Recursos Humanos y tiene un servicio web ASP.NET en la parte superior. Muchas otras aplicaciones usan el servicio web para poblar cosas como campos de búsqueda / desplegable.

El pesimismo es que el desarrollador pensó que las llamadas repetidas al servicio web serían demasiado lentas para realizar consultas SQL repetidas. Entonces, ¿qué hizo él? El evento de inicio de la aplicación se lee en toda la base de datos y lo convierte en objetos en la memoria, almacenados indefinidamente hasta que se recicla el grupo de aplicaciones. Este código era muy lento, tomaría 15 minutos cargarlo en menos de 2000 empleados. Si recicló inadvertidamente el grupo de aplicaciones durante el día, podría tomar 30 minutos o más, ya que cada solicitud de servicio web iniciaría múltiples recargas simultáneas. Por esta razón, las nuevas contrataciones no aparecerían en la base de datos el primer día en que se creó su cuenta y, por lo tanto, no podrían acceder a la mayoría de las aplicaciones internas en los primeros días, haciendo girar sus pulgares.

El segundo nivel de pesimismo es que el gerente de desarrollo no quiere tocarlo por temor a romper aplicaciones dependientes, pero aún así tenemos cortes esporádicos en toda la compañía de aplicaciones críticas debido al diseño deficiente de un componente tan simple.

Una vez trabajé en una aplicación que estaba llena de código como este:

  1 tuple *FindTuple( DataSet *set, int target ) { 2 tuple *found = null; 3 tuple *curr = GetFirstTupleOfSet(set); 4 while (curr) { 5 if (curr->id == target) 6 found = curr; 7 curr = GetNextTuple(curr); 8 } 9 return found; 10 } 

Simplemente eliminando found , devolviendo null al final, y cambiando la sexta línea a:

  return curr; 

Duplicó el rendimiento de la aplicación.

Una vez tuve que intentar modificar el código que incluía estas gems en la clase Constants

 public static String COMMA_DELIMINATOR=","; public static String COMMA_SPACE_DELIMINATOR=", "; public static String COLIN_DELIMINATOR=":"; 

Cada uno de estos se utilizó varias veces en el rest de la aplicación para diferentes propósitos. COMMA_DELIMINATOR ensucia el código con más de 200 usos en 8 paquetes diferentes.

El gran número uno de todos los tiempos que encuentro una y otra vez en el software interno:

No utilizar las características del DBMS por razones de “portabilidad” porque “es posible que deseemos cambiar a otro proveedor más adelante”.

Lee mis labios. Para cualquier trabajo interno: ¡NO SUCEDERÁ!

Tenía un compañero de trabajo que intentaba burlar el optimizador de nuestro comstackdor de C y el código de reescritura de rutina que solo él podía leer. Uno de sus trucos favoritos era cambiar un método legible como (inventando algún código):

 int some_method(int input1, int input2) { int x; if (input1 == -1) { return 0; } if (input1 == input2) { return input1; } ... a long expression here ... return x; } 

dentro de esto:

 int some_method() { return (input == -1) ? 0 : (input1 == input2) ? input 1 : ... a long expression ... ... a long expression ... ... a long expression ... } 

Es decir, la primera línea de un método que alguna vez fue legible se convertiría en ” return ” y todas las otras lógicas serían reemplazadas por expresiones de ternario profundamente anidadas. Cuando trataste de discutir sobre cómo esto no se podía mantener, señaló el hecho de que la salida de ensamblaje de su método era tres o cuatro instrucciones de ensamblaje más cortas. No era necesariamente más rápido, pero siempre era un poco más corto. Este era un sistema integrado en el que el uso de memoria a veces importaba, pero había optimizaciones mucho más sencillas que las que podrían haber dejado el código legible.

Luego, después de esto, por alguna razón decidió que ptr->structElement era demasiado ilegible, por lo que comenzó a cambiar todos estos en (*ptr).structElement en la teoría de que era más legible y más rápido también.

Convertir el código legible en código ilegible para, como mucho, una mejora del 1% y, a veces, un código realmente más lento.

En uno de mis primeros trabajos como desarrollador en toda regla, asumí el control de un proyecto para un progtwig que sufría problemas de escalabilidad. Funcionaría razonablemente bien en pequeños conjuntos de datos, pero se bloqueará por completo cuando se proporcionen grandes cantidades de datos.

Mientras buscaba, descubrí que el progtwigdor original buscaba acelerar las cosas paralelizando el análisis, lanzando un nuevo hilo para cada fuente de datos adicional. Sin embargo, se había equivocado al decir que todos los hilos requerían un recurso compartido, en el cual estaban interlockings. Por supuesto, todos los beneficios de concurrencia desaparecieron. Además, colapsó la mayoría de los sistemas para lanzar más de 100 subprocesos solo para bloquearlos a todos menos a uno. Mi robusta máquina de desarrollo fue una excepción, ya que agitó un conjunto de datos de 150 fonts en alrededor de 6 horas.

Entonces, para solucionarlo, eliminé los componentes de subprocesamiento múltiple y limpié la E / S. Sin más cambios, el tiempo de ejecución en el conjunto de datos de 150 fonts cayó por debajo de 10 minutos en mi máquina, y desde el infinito hasta menos de media hora en la máquina promedio de la empresa.

Supongo que podría ofrecer esta joya:

 unsigned long isqrt(unsigned long value) { unsigned long tmp = 1, root = 0; #define ISQRT_INNER(shift) \ { \ if (value >= (tmp = ((root < < 1) + (1 << (shift))) << (shift))) \ { \ root += 1 << shift; \ value -= tmp; \ } \ } // Find out how many bytes our value uses // so we don't do any uneeded work. if (value & 0xffff0000) { if ((value & 0xff000000) == 0) tmp = 3; else tmp = 4; } else if (value & 0x0000ff00) tmp = 2; switch (tmp) { case 4: ISQRT_INNER(15); ISQRT_INNER(14); ISQRT_INNER(13); ISQRT_INNER(12); case 3: ISQRT_INNER(11); ISQRT_INNER(10); ISQRT_INNER( 9); ISQRT_INNER( 8); case 2: ISQRT_INNER( 7); ISQRT_INNER( 6); ISQRT_INNER( 5); ISQRT_INNER( 4); case 1: ISQRT_INNER( 3); ISQRT_INNER( 2); ISQRT_INNER( 1); ISQRT_INNER( 0); } #undef ISQRT_INNER return root; } 

Como la raíz cuadrada se calculó en un lugar muy sensible, tuve la tarea de buscar una forma de hacerlo más rápido. Esta pequeña refactorización redujo el tiempo de ejecución en un tercio (para la combinación de hardware y comstackdor utilizado, YMMV):

 unsigned long isqrt(unsigned long value) { unsigned long tmp = 1, root = 0; #define ISQRT_INNER(shift) \ { \ if (value >= (tmp = ((root < < 1) + (1 << (shift))) << (shift))) \ { \ root += 1 << shift; \ value -= tmp; \ } \ } ISQRT_INNER (15); ISQRT_INNER (14); ISQRT_INNER (13); ISQRT_INNER (12); ISQRT_INNER (11); ISQRT_INNER (10); ISQRT_INNER ( 9); ISQRT_INNER ( 8); ISQRT_INNER ( 7); ISQRT_INNER ( 6); ISQRT_INNER ( 5); ISQRT_INNER ( 4); ISQRT_INNER ( 3); ISQRT_INNER ( 2); ISQRT_INNER ( 1); ISQRT_INNER ( 0); #undef ISQRT_INNER return root; } 

Por supuesto, hay maneras más rápidas y mejores de hacerlo, pero creo que es un buen ejemplo de una pesimización.

Editar: Ahora que lo pienso, el ciclo desenrollado también fue una buena pesimista. A pesar del control de la versión, puedo presentar también la segunda etapa de refactorización, que tuvo un rendimiento incluso mejor que el anterior:

 unsigned long isqrt(unsigned long value) { unsigned long tmp = 1 < < 30, root = 0; while (tmp != 0) { if (value >= root + tmp) { value -= root + tmp; root += tmp < < 1; } root >>= 1; tmp >>= 2; } return root; } 

Este es exactamente el mismo algoritmo, aunque una implementación ligeramente diferente, por lo que supongo que califica.

Esto podría ser en un nivel más alto que lo que buscabas, pero corregirlo (si estás permitido) también implica un mayor nivel de dolor:

Insistir en rodar manualmente un Object Relationship Manager / Data Access Layer en lugar de utilizar una de las bibliotecas establecidas, probadas y maduras que existen (incluso después de que te lo hayan indicado).

Todas las restricciones de clave externa se eliminaron de una base de datos, porque de lo contrario habría tantos errores.

Esto no encaja exactamente con la pregunta, pero lo mencionaré de todos modos como una advertencia. Estaba trabajando en una aplicación distribuida que funcionaba lentamente, y volé a DC para sentarme en una reunión destinada principalmente a resolver el problema. El líder del proyecto comenzó a delinear una nueva architecture destinada a resolver el retraso. Me ofrecí como voluntario para tomar algunas medidas durante el fin de semana que aislaron el cuello de botella de un solo método. Resultó que faltaba un registro en una búsqueda local, lo que hacía que la aplicación tuviera que ir a un servidor remoto en cada transacción. Al volver a agregar el registro a la tienda local, se eliminó la demora, se resolvió el problema. Tenga en cuenta que la re-architecture no habría solucionado el problema.

Comprobando antes de CADA operación de javascript si el objeto sobre el que está operando existe.

 if (myObj) { //or its evil cousin, if (myObj != null) { label.text = myObj.value; // we know label exists because it has already been // checked in a big if block somewhere at the top } 

Mi problema con este tipo de código es que a nadie parece importarle si no existe. Simplemente no hacer nada? ¿No le das los comentarios al usuario?

Estoy de acuerdo en que los errores Object expected del Object expected son molestos, pero esta no es la mejor solución para eso.

¿Qué hay del extremismo YAGNI? Es una forma de pesimismo prematuro. Parece que cada vez que aplica YAGNI, entonces termina necesitándolo, lo que resulta en 10 veces más esfuerzo para agregarlo que si lo hubiera agregado al principio. Si creas un progtwig exitoso, entonces las probabilidades son: ¿TE VA A NECESITAR? Si está acostumbrado a crear progtwigs cuya vida se agote rápidamente, continúe practicando YAGNI porque entonces supongo que YAGNI.

No es exactamente una optimización prematura, pero ciertamente equivocada, esto fue leído en el sitio web de la BBC, en un artículo sobre Windows 7.

El Sr. Curran dijo que el equipo de Microsoft Windows había estado estudiando detenidamente cada aspecto del sistema operativo para hacer mejoras. “Pudimos reducir 400 milisegundos el tiempo de apagado recortando ligeramente la música de apagado del archivo WAV.

Ahora, aún no he probado Windows 7, así que podría estar equivocado, pero estoy dispuesto a apostar que hay otros problemas que son más importantes que el tiempo que se tarda en apagarse. Después de todo, una vez que veo el mensaje ‘Apagando Windows’, el monitor se apaga y me alejo, ¿cómo me benefician esos 400 milisegundos?

Someone in my department once wrote a string class. An interface like CString , but without the Windows dependence.

One “optimization” they did was to not allocate any more memory than necessary. Apparently not realizing that the reason classes like std::string do allocate excess memory is so that a sequence of += operations can run in O(n) time.

Instead, every single += call forced a reallocation, which turned repeated appends into an O(n²) Schlemiel the Painter’s algorithm .

An ex-coworker of mine (a soab , actually) was assigned to build a new module for our Java ERP that should have collected and analyzed customers’ data (retail industry). He decided to split EVERY Calendar/Datetime field in its components (seconds, minutes, hours, day, month, year, day of week, bimester, trimester (!)) because “how else would I query for ‘every monday’?”

No offense to anyone, but I just graded an assignment (java) that had this

 import java.lang.*;