Pregunta de ingeniero de nivel de entrada con respecto a la gestión de memoria

9

Han pasado unos meses desde que comencé mi posición como desarrollador de software de nivel de entrada. Ahora que he superado algunas curvas de aprendizaje (por ejemplo, el lenguaje, la jerga, la sintaxis de VB y C #), estoy empezando a centrarme en temas más esotéricos, como escribir un mejor software.

Una simple pregunta que le presenté a un compañero de trabajo fue respondida con "Me estoy enfocando en las cosas equivocadas". Si bien respeto a este compañero de trabajo, no estoy de acuerdo con que sea una "cosa incorrecta" en la que centrarse.

Aquí estaba el código (en VB) y seguido de la pregunta.

Nota: La función GenerateAlert () devuelve un entero.

Dim alertID as Integer = GenerateAlert()
_errorDictionary.Add(argErrorID, NewErrorInfo(Now(), alertID))    

vs ...

 _errorDictionary.Add(argErrorID, New ErrorInfo(Now(), GenerateAlert()))

Originalmente escribí este último y lo reescribí con el "Dim alertID" para que a otra persona le resulte más fácil de leer. Pero aquí estaba mi preocupación y pregunta:

Si uno escribe esto con el ID de alerta débil, de hecho ocuparía más memoria; finito pero más, y ¿debería llamarse a este método muchas veces podría conducir a un problema? ¿Cómo manejará .NET este objeto AlertID? Fuera de .NET, uno debe deshacerse manualmente del objeto después de su uso (cerca del final del sub).

Quiero asegurarme de convertirme en un programador experto que no solo se base en la recolección de basura. ¿Estoy pensando demasiado en esto? ¿Me estoy centrando en las cosas equivocadas?

Sean Hobbs
fuente
1
Fácilmente podría argumentar que él es 100% ya que la primera versión es más legible. Apuesto a que el compilador incluso puede ocuparse de lo que le preocupa. Incluso si no fuera así, estás optimizando prematuramente.
Plataforma
66
No estoy del todo seguro de que realmente use más memoria con un número entero anónimo frente a un número entero con nombre. En cualquier caso, esto realmente es una optimización prematura. Si necesita preocuparse por la eficiencia en este nivel (estoy casi seguro de que no), es posible que necesite C ++ en lugar de C #. Es bueno comprender los problemas de rendimiento y lo que sucede debajo del capó, pero este es un pequeño árbol en un gran bosque.
psr
55
El entero con nombre y anónimo no usaría más memoria, especialmente porque un entero anónimo es solo un entero con nombre que USTED no nombró (el compilador aún debe nombrarlo). Como máximo, el entero con nombre tendría un alcance diferente, por lo que podría vivir más tiempo. El entero anónimo viviría solo mientras el método lo necesitara, el nombrado viviría tanto como su padre lo necesitara.
Joel Etherton
Veamos ... Si Integer es una clase, se asignará en el montón. La variable local (en la pila más probable) tendrá una referencia a ella. La referencia se pasará al objeto errorDictionary. Si el tiempo de ejecución está haciendo un recuento de referencias (o tal), entonces cuando no haya más referencias, (el objeto) se desasignará del montón. Cualquier cosa en la pila se "desasigna" automáticamente una vez que sale el método. Si es un primitivo, (muy probablemente) terminará en la pila.
Paul
Su colega tenía razón: el problema planteado por su pregunta no debería haber sido sobre la optimización, sino sobre la legibilidad .
haylem

Respuestas:

25

"La optimización prematura es la raíz de todo mal (o al menos la mayor parte) en la programación". - Donald Knuth

Cuando se trata de su primer pase, simplemente escriba su código para que sea correcto y limpio. Si luego se determina que su código es crítico para el rendimiento (existen herramientas para determinar esto llamado perfiladores), puede reescribirse. Si no se determina que su código es crítico para el rendimiento, la legibilidad es mucho más importante.

¿Vale la pena profundizar en estos temas de rendimiento y optimización? Absolutamente, pero no en el dólar de su empresa si es innecesario.

Christopher Berman
fuente
1
¿De quién más debería ser el dólar? Su empleador se beneficia del aumento de sus habilidades en lugar de más que usted.
Marcin
¿Temas que no contribuyen directamente a su tarea actual? Debes perseguir estas cosas en tu propio tiempo. Si me sentara e investigara cada elemento de CompSci que despertó mi curiosidad en el transcurso del día, no haría nada. Para eso son mis tardes.
Christopher Berman
2
Extraño. Algunos de nosotros tenemos una vida personal, y como digo, el empleador se beneficia principalmente de la investigación. La clave es no gastar todo el día en ello.
Marcin
66
Bien por usted. Sin embargo, en realidad no lo convierte en una regla universal. Además, si desalienta a sus trabajadores para que no aprendan en el trabajo, todo lo que ha hecho es desalentarlos para que no aprendan y los alienta a encontrar otro empleador que realmente pague por el desarrollo del personal.
Marcin
2
Entiendo las opiniones anotadas en los comentarios anteriores; Me gustaría tener en cuenta que pregunté durante mi almuerzo. :). Nuevamente, gracias a todos por sus comentarios aquí y en todo el sitio de Stack Exchange; ¡Es invaluable!
Sean Hobbs
5

Para un programa .NET promedio, sí, es pensar demasiado. Puede haber ocasiones en las que deseará llegar a los detalles de lo que está sucediendo dentro de .NET, pero esto es relativamente raro.

Una de las transiciones difíciles que tuve fue cambiar de usar C y MASM a programar en VB clásico en los años 90. Estaba acostumbrado a optimizar todo por tamaño y velocidad. Tuve que abandonar este pensamiento en su mayor parte y dejar que VB haga lo suyo para ser efectivo.

jfrankcarr
fuente
5

Como siempre decía mi compañero de trabajo:

  1. Hazlo funcionar
  2. Repara todos los errores, haz que funcione perfectamente
  3. Hazlo SOLIDO
  4. Aplique la optimización si está funcionando lentamente

En otras palabras, siempre tenga en cuenta KISS (manténgalo simple estúpido). Debido a que el exceso de ingeniería, el exceso de pensamiento de alguna lógica de código puede ser un problema para cambiar la lógica la próxima vez. Sin embargo, mantener el código limpio y simple siempre es una buena práctica .

Sin embargo, por tiempo y experiencia sabría mejor qué código huele y necesitaría optimización muy pronto.

Yusubov
fuente
3

¿Se debe escribir esto con el ID de alerta débil?

La legibilidad es importante. En su ejemplo, sin embargo, no estoy seguro de que está realmente haciendo las cosas que más legible. GenerateAlert () tiene un buen nombre y no agrega mucho ruido. Probablemente hay mejores usos de su tiempo.

de hecho ocuparía más memoria;

Sospecho que no. Esa es una optimización relativamente sencilla para el compilador.

Si se llama a este método muchas veces, ¿podría provocar un problema?

El uso de una variable local como intermediario no tiene ningún impacto en el recolector de basura. Si la memoria de generación de GenerateAlert () new, entonces importará. Pero eso importará independientemente de la variable local o no.

¿Cómo manejará .NET este objeto AlertID?

AlertID no es un objeto. El resultado de GenerateAlert () es el objeto. AlertID es la variable, que si es una variable local es simplemente espacio asociado con el método para realizar un seguimiento de las cosas.

Fuera de .NET, se debe deshacerse manualmente del objeto después de su uso

Esta es una pregunta de dicier que depende del contexto involucrado y la semántica de propiedad de la instancia proporcionada por GenerateAlert (). En general, cualquier cosa que haya creado la instancia debería eliminarla. Su programa probablemente se vería significativamente diferente si fuera diseñado con la gestión manual de la memoria en mente.

Quiero asegurarme de convertirme en un programador experto que no solo se retransmite tras la recolección de basura. ¿Estoy pensando demasiado en esto? ¿Me estoy centrando en las cosas equivocadas?

Un buen programador utiliza las herramientas disponibles, incluido el recolector de basura. Es mejor pensar demasiado que vivir ajeno. Puede que te estés enfocando en las cosas equivocadas, pero como estamos aquí, también podrías aprender sobre eso.

Telastyn
fuente
2

Haz que funcione, hazlo limpio, hazlo SÓLIDO, LUEGO haz que funcione tan rápido como sea necesario .

Ese debería ser el orden normal de las cosas. Su primera prioridad es hacer algo que pase las pruebas de aceptación que se salgan de los requisitos. Esta es su primera prioridad porque es la primera prioridad de su cliente; cumpliendo los requisitos funcionales dentro de los plazos de desarrollo. La siguiente prioridad es escribir código limpio y legible que sea fácil de entender y que su posteridad pueda mantener sin ningún WTF cuando sea necesario (casi nunca es una cuestión de "si"; usted o alguien después de usted TENDRÁ que ir volver y cambiar / arreglar algo). La tercera prioridad es hacer que el código se adhiera a la metodología SOLID (o GRASP si lo prefiere), que coloca el código en fragmentos modulares, reutilizables y reemplazables que nuevamente ayudan al mantenimiento (no solo pueden entender lo que hizo y por qué, pero hay líneas limpias a lo largo de las cuales puedo eliminar y reemplazar quirúrgicamente piezas de código). La última prioridad es el rendimiento; Si el código es lo suficientemente importante como para cumplir con las especificaciones de rendimiento, es casi seguro que sea lo primero correcto, limpio y SÓLIDO.

Haciéndose eco de Christopher (y Donald Knuth), "la optimización prematura es la raíz de todo mal". Además, el tipo de optimizaciones que está considerando son menores (se creará una referencia a su nuevo objeto en la pila independientemente de si le da un nombre en el código fuente o no) y de un tipo que no cause ninguna diferencia en el compilado ILLINOIS. Los nombres de las variables no se transfieren al IL, por lo que dado que está declarando la variable justo antes de su primer uso (y probablemente solo), apostaría algo de dinero a la cerveza que el IL es idéntico entre sus dos ejemplos. Entonces, su compañero de trabajo tiene 100% de razón; está buscando en el lugar equivocado si está buscando una instancia con nombre de variable vs en línea para optimizar algo.

Las micro optimizaciones en .NET casi nunca valen la pena (estoy hablando del 99,99% de los casos). En C / C ++, tal vez, SI sabes lo que estás haciendo. Cuando trabajas en un entorno .NET, ya estás lo suficientemente lejos del metal del hardware que hay una sobrecarga significativa en la ejecución del código. Por lo tanto, dado que ya se encuentra en un entorno que indica que ha renunciado a la velocidad vertiginosa y, en cambio, está buscando escribir código "correcto", si algo en un entorno .NET realmente no funciona lo suficientemente rápido, su complejidad es demasiado alto, o deberías considerar paralelizarlo. Aquí hay algunos consejos básicos a seguir para la optimización; Le garantizo que su productividad en la optimización (velocidad ganada por el tiempo dedicado) se disparará:

  • Cambiar la forma de la función es más importante que cambiar los coeficientes : la complejidad de WRT Big-Oh, puede reducir a la mitad el número de pasos que se deben ejecutar en un algoritmo de N 2 , y aún tiene un algoritmo de complejidad cuadrática a pesar de que se ejecuta en la mitad del tiempo que solía hacerlo. Si ese es el límite inferior de complejidad para este tipo de problema, que así sea, pero si hay una solución NlogN, lineal o logarítmica para el mismo problema, obtendrá más al cambiar los algoritmos para reducir la complejidad que al optimizar el que tiene.
  • El hecho de que no pueda ver la complejidad no significa que no le esté costando : muchas de las frases más elegantes de la palabra tienen un rendimiento terrible (por ejemplo, el corrector principal Regex es una función de complejidad exponencial, aunque eficiente la evaluación principal que implica dividir el número entre todos los números primos menores que su raíz cuadrada es del orden de O (Nlog (sqrt (N))). Linq es una gran biblioteca porque simplifica el código, pero a diferencia de un motor SQL, el .Net el compilador no intentará encontrar la forma más eficiente de ejecutar su consulta. Debe saber qué sucederá cuando use un método y, por lo tanto, por qué un método podría ser más rápido si se coloca antes (o más tarde) en la cadena, mientras produce Los mismos resultados.
  • OTOH, casi siempre hay una compensación entre la complejidad de la fuente y la complejidad del tiempo de ejecución : SelectionSort es muy fácil de implementar; probablemente podría hacerlo en 10LOC o menos. MergeSort es un poco más complejo, Quicksort más y RadixSort aún más. Pero, a medida que los algoritmos aumentan en la complejidad de la codificación (y, por lo tanto, en el tiempo de desarrollo "inicial"), disminuyen en la complejidad del tiempo de ejecución; MergeSort y QuickSort son NlogN, y RadixSort generalmente se considera lineal (técnicamente es NlogM donde M es el número más grande en N).
  • Descanse rápido : si hay una verificación que se puede hacer a bajo costo, que es significativamente probable que sea cierto y que significa que puede seguir adelante, haga esa verificación primero. Si su algoritmo, por ejemplo, solo se preocupa por los números que terminan en 1, 2 o 3, el caso más probable (dados datos completamente al azar) es un número que termina en algún otro dígito, por lo tanto, pruebe que el número NO termine en 1, 2 o 3, antes de hacer cualquier verificación para ver si el número termina en 1, 2 o 3. Si una lógica requiere A y B, y P (A) = 0.9 mientras P (B) = 0.1, entonces verifique B primero, a menos que sea! A y luego B (como if(myObject != null && myObject.someProperty == 1)), o B tome más de 9 veces más tiempo que A para evaluar ( if(myObject != null && some10SecondMethodReturningBool())).
  • No haga ninguna pregunta para la que ya conozca la respuesta : si tiene una serie de condiciones de "falla" y una o más de esas condiciones dependen de una condición más simple que también debe verificarse, nunca verifique ambas Estos independientemente. Por ejemplo, si tiene un cheque que requiere A, y un cheque que requiere A y B, debe marcar A y, si es cierto, debe marcar B. Si! A, entonces! A y B, así que ni se moleste.
  • Cuantas más veces haga algo, más deberá prestar atención a cómo se hace : este es un tema común en el desarrollo, en muchos niveles; en un sentido de desarrollo general, "si una tarea común lleva mucho tiempo o es incómoda, continúe haciéndola hasta que esté frustrado y tenga los conocimientos necesarios para encontrar una mejor manera". En términos de código, mientras más veces se ejecute un algoritmo ineficiente, más ganará en el rendimiento general al optimizarlo. Existen herramientas de creación de perfiles que pueden tomar un ensamblado binario y sus símbolos de depuración y mostrarle, después de analizar algunos casos de uso, qué líneas de código se ejecutaron más. Esas líneas, y las líneas que corren esas líneas, son a lo que debe prestar más atención, porque cualquier aumento en la eficiencia que logre allí se multiplicará.
  • Un algoritmo más complejo se parece a un algoritmo menos complejo si le arrojas suficiente hardware . Hay ocasiones en las que solo debes darte cuenta de que tu algoritmo se está acercando a los límites técnicos del sistema (o parte de él) en el que lo estás ejecutando; a partir de ese momento, si necesita ser más rápido, ganará más simplemente ejecutándolo en un mejor hardware. Esto también se aplica a la paralelización; un algoritmo de complejidad N 2 , cuando se ejecuta en N núcleos, parece lineal. Por lo tanto, si está seguro de haber alcanzado el límite de menor complejidad para el tipo de algoritmo que está escribiendo, busque formas de "dividir y conquistar".
  • Es rápido cuando es lo suficientemente rápido : a menos que esté empaquetando a mano el conjunto para apuntar a un chip en particular, siempre hay algo que ganar. Sin embargo, a menos que QUIERA ser ensamblado a mano, siempre debe tener en cuenta lo que el cliente llamaría "suficientemente bueno". Nuevamente, "la optimización prematura es la raíz de todo mal"; cuando su cliente lo llama lo suficientemente rápido, está listo hasta que ya no piense que es lo suficientemente rápido.
KeithS
fuente
0

El único momento para preocuparse por la optimización desde el principio es cuando sabe que está tratando con algo que es enorme o algo que sabe que se ejecutará una gran cantidad de veces.

La definición de "enorme" obviamente varía en función de cómo son sus sistemas de destino.

Loren Pechtel
fuente
0

Preferiría la versión de dos líneas simplemente porque es más fácil avanzar con un depurador. Una línea con varias llamadas incrustadas lo hace más difícil.

Slapout
fuente