Recientemente me entrevisté en Amazon. Durante una sesión de codificación, el entrevistador preguntó por qué declaraba una variable en un método. Le expliqué mi proceso y él me retó a resolver el mismo problema con menos variables. Por ejemplo (esto no fue de la entrevista), comencé con el Método A y luego lo mejoré al Método B, al eliminarlo int s
. Estaba satisfecho y dijo que esto reduciría el uso de memoria por este método.
Entiendo la lógica detrás de esto, pero mi pregunta es:
¿Cuándo es apropiado usar el Método A versus el Método B y viceversa?
Puede ver que el Método A tendrá un mayor uso de memoria, ya que int s
se declara, pero solo tiene que realizar un cálculo, es decir a + b
. Por otro lado, el Método B tiene un menor uso de memoria, pero tiene que realizar dos cálculos, es decir, a + b
dos veces. ¿Cuándo uso una técnica sobre la otra? O, ¿una de las técnicas siempre se prefiere sobre la otra? ¿Qué cosas hay que considerar al evaluar los dos métodos?
Método A:
private bool IsSumInRange(int a, int b)
{
int s = a + b;
if (s > 1000 || s < -1000) return false;
else return true;
}
Método B:
private bool IsSumInRange(int a, int b)
{
if (a + b > 1000 || a + b < -1000) return false;
else return true;
}
fuente
int s
mientras estaban totalmente bien con esos números mágicos para los límites superior e inferior?Respuestas:
En lugar de especular sobre lo que puede o no suceder, veamos, ¿de acuerdo? Tendré que usar C ++ ya que no tengo un compilador de C # a mano (aunque vea el ejemplo de C # de VisualMelon ), pero estoy seguro de que los mismos principios se aplican independientemente.
Incluiremos las dos alternativas que encontró en la entrevista. También incluiremos una versión que utilice
abs
según lo sugerido por algunas de las respuestas.Ahora compílelo sin optimización alguna:
g++ -c -o test.o test.cpp
Ahora podemos ver exactamente lo que esto genera:
objdump -d test.o
Podemos ver en las direcciones de la pila (por ejemplo, el
-0x4
inmov %edi,-0x4(%rbp)
versus el-0x14
inmov %edi,-0x14(%rbp)
) queIsSumInRangeWithVar()
usa 16 bytes adicionales en la pila.Debido a que
IsSumInRangeWithoutVar()
no asigna espacio en la pila para almacenar el valor intermedios
, tiene que volver a calcularlo, lo que hace que esta implementación tenga 2 instrucciones más.Divertido, se
IsSumInRangeSuperOptimized()
parece muchoIsSumInRangeWithoutVar()
, excepto que se compara con -1000 primero y 1000 segundo.Ahora vamos a compilar con optimizaciones sólo las más básicas:
g++ -O1 -c -o test.o test.cpp
. El resultado:¿Lo mirarías? Cada variante es idéntica . El compilador puede hacer algo bastante inteligente:
abs(a + b) <= 1000
es equivalente aa + b + 1000 <= 2000
considerarsetbe
hacer una comparación sin signo, por lo que un número negativo se convierte en un número positivo muy grande. Lalea
instrucción puede realizar todas estas adiciones en una sola instrucción y eliminar todas las ramas condicionales.Para responder a su pregunta, casi siempre lo que hay que optimizar no es la memoria o la velocidad, sino la legibilidad . Leer el código es mucho más difícil que escribirlo, y leer el código que se ha manipulado para "optimizar" es mucho más difícil que leer el código que se ha escrito para que quede claro. La mayoría de las veces, estas "optimizaciones" tienen un impacto insignificante o, como en este caso, exactamente cero en el rendimiento real.
¡Midamos! He transcrito los ejemplos a Python:
Ejecutar con Python 3.5.2, esto produce el resultado:
El desmontaje en Python no es terriblemente interesante, ya que el "compilador" de bytecode no hace mucho en cuanto a optimización.
El rendimiento de las tres funciones es casi idéntico. Podríamos sentir la tentación de ir
IsSumInRangeWithVar()
debido a su ganancia de velocidad marginal. Aunque agregaré, ya que estaba probando diferentes parámetrostimeit
, a vecesIsSumInRangeSuperOptimized()
salió más rápido, por lo que sospecho que pueden ser factores externos responsables de la diferencia, en lugar de cualquier ventaja intrínseca de cualquier implementación.Si este es realmente un código crítico para el rendimiento, un lenguaje interpretado es simplemente una elección muy pobre. Al ejecutar el mismo programa con pypy, obtengo:
El solo uso de pypy, que usa la compilación JIT para eliminar gran parte de la sobrecarga del intérprete, ha producido una mejora en el rendimiento de 1 o 2 órdenes de magnitud. Me sorprendió bastante ver que
IsSumInRangeWithVar()
es un orden de magnitud más rápido que los demás. Así que cambié el orden de los puntos de referencia y corrí nuevamente:¡Entonces parece que no es realmente nada acerca de la implementación lo que lo hace rápido, sino el orden en el que hago la evaluación comparativa!
Me encantaría profundizar más en esto, porque honestamente no sé por qué sucede esto. Pero creo que el punto ha sido señalado: las micro optimizaciones, como declarar un valor intermedio como variable o no, rara vez son relevantes. Con un lenguaje interpretado o un compilador altamente optimizado, el primer objetivo sigue siendo escribir código claro.
Si se requiere una mayor optimización, referencia . Recuerde que las mejores optimizaciones provienen no de los pequeños detalles sino de la imagen algorítmica más grande: pypy será un orden de magnitud más rápido para la evaluación repetida de la misma función que cpython porque usa algoritmos más rápidos (compilador JIT frente a interpretación) para evaluar el programa. Y también hay que considerar el algoritmo codificado: una búsqueda a través de un árbol B será más rápida que una lista vinculada.
Después de asegurarse de que está utilizando las herramientas y algoritmos adecuados para el trabajo, prepárese para profundizar en los detalles del sistema. Los resultados pueden ser muy sorprendentes, incluso para desarrolladores experimentados, y es por eso que debe tener un punto de referencia para cuantificar los cambios.
fuente
Para responder la pregunta planteada:
Hay dos cosas que debes establecer:
Para responder a la primera pregunta, debe saber cuáles son los requisitos de rendimiento para su aplicación. Si no hay requisitos de rendimiento, entonces no hay razón para optimizar de una forma u otra. Los requisitos de rendimiento lo ayudan a llegar al lugar de "lo suficientemente bueno".
El método que proporcionó por sí solo no causaría ningún problema de rendimiento por sí solo, pero tal vez dentro de un ciclo y procesando una gran cantidad de datos, debe comenzar a pensar de manera un poco diferente sobre cómo aborda el problema.
Detectar lo que limita la aplicación.
Comience a observar el comportamiento de su aplicación con un monitor de rendimiento. Esté atento al uso de CPU, disco, red y memoria mientras se está ejecutando. Uno o más elementos se maximizarán mientras todo lo demás se usa moderadamente, a menos que alcance el equilibrio perfecto, pero eso casi nunca sucede).
Cuando necesita mirar más profundo, normalmente usaría un generador de perfiles . Hay perfiladores de memoria y perfiladores de procesos , y miden cosas diferentes. El acto de perfilar tiene un impacto significativo en el rendimiento, pero está instrumentando su código para descubrir qué está mal.
Digamos que ves que tu CPU y el uso del disco alcanzaron su punto máximo. Primero verificará si hay "puntos calientes" o código que se llama con más frecuencia que el resto o que requiere un porcentaje significativamente más largo del procesamiento.
Si no puede encontrar ningún punto caliente, entonces comenzará a mirar la memoria. Quizás esté creando más objetos de los necesarios y su recolección de basura esté trabajando horas extras.
Reclamando el rendimiento
Piensa críticamente. La siguiente lista de cambios está en orden de cuánto retorno de inversión obtendrá:
En situaciones como esta, debe aplicar el método científico. Presente una hipótesis, realice los cambios y pruébelo. Si cumple con sus objetivos de rendimiento, ya está. Si no, vaya a lo siguiente en la lista.
Respondiendo la pregunta en negrita:
Honestamente, este es el último paso para tratar de lidiar con problemas de rendimiento o memoria. El impacto del Método A frente al Método B será realmente diferente según el idioma y la plataforma (en algunos casos).
Casi cualquier lenguaje compilado con un optimizador a mitad de camino generará un código similar con cualquiera de esas estructuras. Sin embargo, esas suposiciones no necesariamente siguen siendo ciertas en los lenguajes patentados y de juguete que no tienen un optimizador.
Precisamente, lo que tendrá un mejor impacto depende de si
sum
es una variable de pila o una variable de montón. Esta es una opción de implementación de lenguaje. En C, C ++ y Java, por ejemplo, las primitivas numéricas como unint
son variables de pila por defecto. Su código no tiene más impacto en la memoria mediante la asignación a una variable de pila de la que tendría con un código totalmente integrado.Otras optimizaciones que puede encontrar en las bibliotecas C (particularmente las más antiguas) en las que puede tener que decidir entre copiar una matriz bidimensional en primer lugar o en primer lugar es una optimización dependiente de la plataforma. Requiere cierto conocimiento de cómo el conjunto de chips al que se dirige optimiza mejor el acceso a la memoria. Hay diferencias sutiles entre arquitecturas.
La conclusión es que la optimización es una combinación de arte y ciencia. Requiere un poco de pensamiento crítico, así como un cierto grado de flexibilidad en la forma de abordar el problema. Busque cosas grandes antes de culpar a las cosas pequeñas.
fuente
"esto reduciría la memoria" - em, no. Incluso si esto fuera cierto (que, para cualquier compilador decente no lo es), la diferencia probablemente sería insignificante para cualquier situación del mundo real.
Sin embargo, recomendaría usar el método A * (método A con un ligero cambio):
pero por dos razones completamente diferentes:
Al dar a la variable
s
un nombre explicativo, el código se vuelve más claroevita tener la misma lógica de suma dos veces en el código, por lo que el código se vuelve más SECO, lo que significa menos errores propensos a cambios.
fuente
sum
variable, lo que conducirá a un uso de memoria cero. E incluso si no, esta es solo una palabra de memoria en un método de "hoja". Teniendo en cuenta cuán increíblemente derrochador de memoria Java o C # puede ser de otra manera debido a su GC y modelo de objeto, unaint
variable local literalmente no utiliza ninguna memoria notable. Esta es una microoptimización sin sentido.sum
sería un detalle de implementación y dudo que alguien pueda hacer un caso convincente sobre si un truco tonto como evitar un localint
conduciría a esto o esa cantidad de uso de memoria a largo plazo. La legibilidad de la OMI es más importante. La legibilidad puede ser subjetiva, pero FWIW, personalmente prefiero que nunca hagas el mismo cálculo dos veces, no para el uso de la CPU, sino porque solo tengo que inspeccionar tu adición una vez cuando estoy buscando un error.Puedes hacerlo mejor que los dos con
La mayoría de los procesadores (y, por lo tanto, los compiladores) pueden hacer abs () en una sola operación. No solo tiene menos sumas, sino también menos comparaciones, que generalmente son más costosas desde el punto de vista computacional. También elimina la ramificación, que es mucho peor en la mayoría de los procesadores porque impide que sea posible la canalización.
El entrevistador, como han dicho otras respuestas, es la vida vegetal y no tiene por qué realizar una entrevista técnica.
Dicho esto, su pregunta es válida. Y la respuesta a cuándo optimiza y cómo, es cuándo ha demostrado que es necesario, y lo ha perfilado para demostrar exactamente qué partes lo necesitan . Knuth dijo que la optimización prematura es la raíz de todos los males, porque es demasiado fácil tratar de cubrir con oro secciones sin importancia, o hacer cambios (como su entrevistador) que no tienen ningún efecto, mientras se pierden los lugares que realmente lo necesitan. Hasta que tenga una prueba sólida, es realmente necesario, la claridad del código es el objetivo más importante.
Editar FabioTurati señala correctamente que este es el sentido lógico opuesto al original (¡mi error!), Y que esto ilustra un impacto adicional de la cita de Knuth, donde corremos el riesgo de romper el código mientras intentamos optimizarlo.
fuente
a+b
enif
y hacerlo dos veces. Lo entiendes mal "Estaba contento y dijo que esto reduciría el uso de memoria por este método" - fue amable contigo, escondiendo su decepción con esta explicación sin sentido sobre la memoria. No deberías tomarte en serio hacer preguntas aquí. ¿Conseguiste un trabajo? Supongo que no :-(abs()
, y también tiene una solareturn
, en lugar de tener una cuando la condición es verdadera ("if branch") y otra cuando es falsa ( "rama más"). Cuando cambie un código como este, tenga cuidado: existe el riesgo de escribir inadvertidamente una función que devuelve verdadero cuando debería devolver falso, y viceversa. Que es exactamente lo que sucedió aquí. Sé que te estabas enfocando en otra cosa, y has hecho un buen trabajo al respecto. Aún así, esto podría fácilmente haberle costado el trabajo ...adds
, y ARM ha predicado reverse-sub (rsblt
= reverse-sub si less-tha) pero todo lo demás requiere múltiples instrucciones adicionales para implementarabs(a+b)
oabs(a)
. godbolt.org/z/Ok_Con muestra la salida asm de x86, ARM, AArch64, PowerPC, MIPS y RISC-V. Solo mediante la transformación de la comparación en una comprobación de rango,(unsigned)(a+b+999) <= 1998U
gcc puede optimizarlo como en la respuesta de Phil.IsSumInRange(INT_MIN, 0)
. El código original regresafalse
porqueINT_MIN+0 > 1000 || INT_MIN+0 < -1000
; pero el código "nuevo y mejorado" regresatrue
porqueabs(INT_MIN+0) < 1000
. (O, en algunos idiomas, arrojará una excepción o tendrá un comportamiento indefinido. Revise sus listados locales.)El hardware es barato; Los programadores son caros . Entonces, el costo del tiempo que ustedes dos perdieron en esta pregunta es probablemente mucho peor que cualquiera de las respuestas.
De todos modos, la mayoría de los compiladores modernos encontrarían una manera de optimizar la variable local en un registro (en lugar de asignar espacio de pila), por lo que los métodos son probablemente idénticos en términos de código ejecutable. Por esta razón, la mayoría de los desarrolladores elegirían la opción que comunique la intención más claramente (ver Escritura de código realmente obvio (ROC) ). En mi opinión, ese sería el Método A.
Por otro lado, si esto es puramente un ejercicio académico, puedes tener lo mejor de ambos mundos con el Método C:
fuente
a+=b
es un buen truco, pero tengo que mencionar (en caso de que no esté implícito en el resto de la respuesta), según mi experiencia, los métodos que alteran los parámetros pueden ser muy difíciles de depurar y mantener.if
cheque, pero olvidó invertir el resultado de la comparación; su función ahora regresatrue
cuando noa + b
está en el rango. O bien agregar una a la parte externa de la condición ( ), o distribuir las pruebas, invirtiendo, para obtener o para hacer que el flujo de prueba de alcance muy bien,!
return !(a > 1000 || a < -1000)
!
return a <= 1000 && a >= -1000;
return -1000 <= a && a <= 1000;
<=
/>=
, no<
/>
(con<
/>
, 1000 y -1000 se tratan como fuera de rango, el código original los trata como dentro del rango).Me gustaría optimizar la legibilidad. Método X:
Pequeños métodos que hacen solo 1 cosa pero son fáciles de razonar.
(Esta es una preferencia personal, me gustan las pruebas positivas en lugar de negativas, su código original realmente está probando si el valor NO está fuera del rango).
fuente
a
yb
denumber1
ynumber2
ayudas para la legibilidad de ninguna manera. Además, su denominación de las funciones es inconsistente: ¿por quéIsSumInRange
codifica el rango de forma rígida si loIsValueInRange
acepta como argumentos?En resumen, no creo que la pregunta tenga mucha relevancia en la informática actual, pero desde una perspectiva histórica es un ejercicio de pensamiento interesante.
Es probable que su entrevistador sea fanático del Mes del Hombre Mítico. En el libro, Fred Brooks expone que los programadores generalmente necesitarán dos versiones de funciones clave en su caja de herramientas: una versión optimizada para la memoria y una versión optimizada para la CPU. Fred basó esto en su experiencia al liderar el desarrollo del sistema operativo IBM System / 360 donde las máquinas pueden tener tan solo 8 kilobytes de RAM. En tales máquinas, la memoria requerida para las variables locales en las funciones podría ser importante, especialmente si el compilador no las optimizó de manera efectiva (o si el código se escribió directamente en lenguaje ensamblador).
En la era actual, creo que sería difícil encontrar un sistema donde la presencia o ausencia de una variable local en un método marcara una diferencia notable. Para que una variable importe, el método debería ser recursivo con una recursión profunda esperada. Incluso entonces, es probable que se exceda la profundidad de la pila causando excepciones de desbordamiento de pila antes de que la variable en sí misma cause un problema. El único escenario real en el que puede ser un problema es con matrices muy grandes asignadas en la pila en un método recursivo. Pero eso también es poco probable, ya que creo que la mayoría de los desarrolladores pensarían dos veces antes de las copias innecesarias de matrices grandes.
fuente
Después de la asignación s = a + b; las variables ayb ya no se usan. Por lo tanto, no se utiliza memoria para s si no está utilizando un compilador con daño cerebral completo; la memoria que se utilizó de todos modos para a y b se reutiliza.
Pero optimizar esta función es completamente absurdo. Si pudiera ahorrar espacio, serían quizás 8 bytes mientras la función se está ejecutando (que se recupera cuando la función regresa), por lo que es absolutamente inútil. Si pudiera ahorrar tiempo, serían números únicos de nanosegundos. Optimizar esto es una pérdida total de tiempo.
fuente
Las variables de tipo de valor local se asignan en la pila o (lo más probable es que para piezas de código tan pequeñas) utilicen registros en el procesador y nunca lleguen a ver RAM. De cualquier manera, son de corta duración y no hay nada de qué preocuparse. Comienza a considerar el uso de la memoria cuando necesita almacenar en búfer o poner en cola elementos de datos en colecciones que son potencialmente grandes y de larga duración.
Entonces, depende de lo que más le interese para su aplicación. ¿Velocidad de procesamiento? ¿Tiempo de respuesta? Huella de memoria? Mantenibilidad? ¿Consistencia en el diseño? Todo depende de ti.
fuente
stackalloc
y ahoraSpan<T>
. Posiblemente útil en un punto caliente, después de perfilar. Además, algunos de los documentos sobre estructuras implican que los tipos de valor pueden estar en la pila, mientras que los tipos de referencia no lo estarán. De todos modos, en el mejor de los casos, puede evitar un poco de GC.Como han dicho otras respuestas, debes pensar para qué estás optimizando.
En este ejemplo, sospecho que cualquier compilador decente generaría un código equivalente para ambos métodos, por lo que la decisión no tendría ningún efecto en el tiempo de ejecución o la memoria.
Lo que sí afecta es la legibilidad del código. (El código es para que los humanos lo lean, no solo las computadoras). No hay demasiada diferencia entre los dos ejemplos; cuando todas las demás cosas son iguales, considero que la brevedad es una virtud, por lo que probablemente elegiría el Método B. Pero todas las demás cosas rara vez son iguales, y en un caso más complejo del mundo real, podría tener un gran efecto.
Cosas para considerar:
fuente
Como muchas de las respuestas han señalado, intentar ajustar esta función con compiladores modernos no hará ninguna diferencia. Es muy probable que un optimizador encuentre la mejor solución (¡vote por la respuesta que mostró el código del ensamblador para probarlo!). Usted dijo que el código en la entrevista no era exactamente el código que le pidieron comparar, por lo que quizás el ejemplo real tenga un poco más de sentido.
Pero echemos otro vistazo a esta pregunta: esta es una pregunta de entrevista. Entonces, el verdadero problema es, ¿cómo debería responderlo asumiendo que desea intentar conseguir el trabajo?
Supongamos también que el entrevistador sabe de lo que está hablando y solo está tratando de ver lo que usted sabe.
Mencionaría que, ignorando el optimizador, el primero puede crear una variable temporal en la pila mientras que el segundo no lo haría, pero realizaría el cálculo dos veces. Por lo tanto, el primero usa más memoria pero es más rápido.
Podría mencionar que, de todos modos, un cálculo puede requerir una variable temporal para almacenar el resultado (para que se pueda comparar), por lo tanto, si nombra esa variable o no, puede no haber ninguna diferencia.
Luego mencionaría que en realidad el código se optimizaría y probablemente se generaría un código de máquina equivalente, ya que todas las variables son locales. Sin embargo, depende del compilador que esté utilizando (no hace mucho tiempo pude obtener una mejora útil del rendimiento al declarar una variable local como "final" en Java).
Podría mencionar que, en cualquier caso, la pila vive en su propia página de memoria, por lo que, a menos que su variable adicional haga que la pila desborde la página, en realidad no asignará más memoria. Sin embargo, si se desborda, querrá una página completamente nueva.
Mencionaría que un ejemplo más realista podría ser la elección de si usar un caché para contener los resultados de muchos cálculos o no, y esto plantearía una cuestión de CPU vs memoria.
Todo esto demuestra que sabes de lo que estás hablando.
Lo dejaría hasta el final para decir que sería mejor centrarse en la legibilidad en su lugar. Aunque es cierto en este caso, en el contexto de la entrevista puede interpretarse como "No sé sobre el rendimiento, pero mi código se lee como una historia de Janet y John ".
Lo que no debe hacer es trotear las declaraciones insípidas habituales sobre cómo no es necesaria la optimización del código, no optimice hasta que haya perfilado el código (esto solo indica que no puede ver el código incorrecto), el hardware cuesta menos que los programadores , y por favor, por favor, no cites a Knuth "prematuro, bla, bla ...".
El rendimiento del código es un problema genuino en muchas organizaciones y muchas organizaciones necesitan programadores que lo entiendan.
En particular, con organizaciones como Amazon, parte del código tiene una gran influencia. Se puede implementar un fragmento de código en miles de servidores o millones de dispositivos y se puede llamar miles de millones de veces al día todos los días del año. Puede haber miles de fragmentos similares. La diferencia entre un algoritmo malo y uno bueno puede ser fácilmente un factor de mil. Haga los números y multiplique todo esto: hace la diferencia. El costo potencial para la organización del código que no funciona puede ser muy significativo o incluso fatal si un sistema se queda sin capacidad.
Además, muchas de estas organizaciones trabajan en un entorno competitivo. Por lo tanto, no puede simplemente decirles a sus clientes que compren una computadora más grande si el software de su competencia ya funciona bien en el hardware que tienen o si el software se ejecuta en un teléfono móvil y no se puede actualizar. Algunas aplicaciones son particularmente críticas para el rendimiento (me vienen a la mente juegos y aplicaciones móviles) y pueden vivir o morir de acuerdo con su capacidad de respuesta o velocidad.
Personalmente, durante más de dos décadas, trabajé en muchos proyectos en los que los sistemas fallaron o no se pudieron usar debido a problemas de rendimiento y me llamaron para optimizar esos sistemas y en todos los casos se debió a un código incorrecto escrito por programadores que no entendieron El impacto de lo que estaban escribiendo. Además, nunca es un código, siempre está en todas partes. Cuando aparezco, es demasiado tarde para comenzar a pensar en el rendimiento: el daño ya está hecho.
Comprender el rendimiento del código es una buena habilidad para tener de la misma manera que comprender la corrección del código y el estilo del código. Sale de la práctica. Las fallas de rendimiento pueden ser tan malas como las fallas funcionales. Si el sistema no funciona, no funciona. No importa por qué. Del mismo modo, el rendimiento y las características que nunca se usan son malos.
Por lo tanto, si el entrevistador le pregunta sobre el desempeño, le recomendaría que pruebe y demuestre el mayor conocimiento posible. Si la pregunta parece mala, explique cortésmente por qué cree que no sería un problema en ese caso. No cites a Knuth.
fuente
Primero debe optimizar la corrección.
Su función falla para los valores de entrada que están cerca de Int.MaxValue:
Esto devuelve verdadero porque la suma se desborda a -400. La función tampoco funciona para a = int.MinValue + 200. (incorrectamente se suma a "400")
No sabremos qué estaba buscando el entrevistador a menos que intervenga, pero "el desbordamiento es real" .
En una situación de entrevista, haga preguntas para aclarar el alcance del problema: ¿Cuáles son los valores de entrada máximos y mínimos permitidos? Una vez que los tenga, puede lanzar una excepción si la persona que llama envía valores fuera del rango. O (en C #), puede usar una sección marcada {}, que generaría una excepción en caso de desbordamiento. Sí, es más trabajo y complicado, pero a veces eso es lo que se necesita.
fuente
Su pregunta debería haber sido: "¿Necesito optimizar esto en absoluto?".
Las versiones A y B difieren en un detalle importante que hace que A sea preferible, pero no está relacionado con la optimización: no repites el código.
La "optimización" real se llama eliminación de subexpresión común, que es lo que hacen prácticamente todos los compiladores. Algunos hacen esta optimización básica incluso cuando las optimizaciones están desactivadas. Por lo tanto, eso no es realmente una optimización (el código generado seguramente será exactamente el mismo en todos los casos).
Pero si no es una optimización, ¿por qué es preferible? Muy bien, no repites el código, ¡a quién le importa!
Bueno, antes que nada, no corre el riesgo de equivocarse accidentalmente con la mitad de la cláusula condicional. Pero lo que es más importante, alguien que lea este código puede entender inmediatamente lo que está tratando de hacer, en lugar de una
if((((wtf||is||this||longexpression))))
experiencia. Lo que el lector puede ver esif(one || theother)
, que es algo bueno. No rara vez, sucede que usted es esa otra persona que lee su propio código tres años después y piensa "¿Qué significa esto?". En ese caso, siempre es útil si su código comunica inmediatamente cuál fue la intención. Con una subexpresión común nombrada correctamente, ese es el caso.Además, si en cualquier momento en el futuro, por ejemplo, decide que necesita cambiar
a+b
aa-b
, usted tiene que cambiar unoubicación, no dos. Y no hay riesgo de que (de nuevo) se equivoque el segundo por accidente.Acerca de su pregunta real, para qué debe optimizar, en primer lugar, su código debe ser correcto . Esto es lo más importante. El código que no es correcto es un código incorrecto, incluso más si a pesar de ser incorrecto "funciona bien", o al menos parece que funciona bien. Después de eso, el código debe ser legible (legible por alguien que no esté familiarizado con él).
En cuanto a la optimización ... uno ciertamente no debería escribir deliberadamente código anti-optimizado, y ciertamente no estoy diciendo que no deba pensar en el diseño antes de comenzar (como elegir el algoritmo correcto para el problema, no el menos eficiente).
Pero para la mayoría de las aplicaciones, la mayoría de las veces, el rendimiento que obtienes después de ejecutar un código correcto y legible utilizando un algoritmo razonable a través de un compilador de optimización está bien, no hay necesidad real de preocuparse.
Si ese no es el caso, es decir, si el rendimiento de la aplicación no cumple con los requisitos, y solo entonces , debe preocuparse por hacer optimizaciones locales como la que intentó. Preferiblemente, sin embargo, reconsideraría el algoritmo de nivel superior. Si llama a una función 500 veces en lugar de 50,000 veces debido a un algoritmo mejor, esto tiene un mayor impacto que guardar tres ciclos de reloj en una microoptimización. Si no se detiene durante varios cientos de ciclos en un acceso aleatorio a la memoria todo el tiempo, esto tiene un impacto mayor que hacer algunos cálculos extra baratos, etc.
La optimización es un asunto difícil (puedes escribir libros completos sobre eso y no tener fin), y pasar tiempo optimizando ciegamente un punto en particular (¡sin siquiera saber si ese es el cuello de botella!) Generalmente es tiempo perdido. Sin perfiles, la optimización es muy difícil de acertar.
Pero como regla general, cuando vuelas ciego y solo necesitas / quieres hacer algo , o como una estrategia general predeterminada, sugeriría optimizar para "memoria".
La optimización para la "memoria" (en particular, la ubicación espacial y los patrones de acceso) generalmente produce un beneficio porque, a diferencia de lo que alguna vez fue "casi igual", hoy en día acceder a la RAM es una de las cosas más caras (¡menos leer desde el disco!) que en principio puedes hacer. Mientras que ALU, por otro lado, es barato y cada vez es más rápido. El ancho de banda y la latencia de la memoria no mejoran tan rápido. La buena localidad y los buenos patrones de acceso pueden hacer fácilmente una diferencia de 5x (20x en ejemplos extremos y elaborados) en tiempo de ejecución en comparación con los malos patrones de acceso en aplicaciones con muchos datos. Sé amable con tus cachés, y serás una persona feliz.
Para poner el párrafo anterior en perspectiva, considere lo que le cuestan las diferentes cosas que puede hacer. Ejecutar algo como
a+b
toma (si no está optimizado) uno o dos ciclos, pero la CPU generalmente puede iniciar varias instrucciones por ciclo y puede canalizar instrucciones no dependientes, de manera más realista, solo le cuesta algo alrededor de medio ciclo o menos. Idealmente, si el compilador es bueno programando, y dependiendo de la situación, puede costar cero.Obtener datos ("memoria") le cuesta entre 4 y 5 ciclos si tiene suerte y está en L1, y alrededor de 15 ciclos si no tiene tanta suerte (golpe L2). Si los datos no están en la memoria caché, se necesitan varios cientos de ciclos. Si su patrón de acceso aleatorio excede las capacidades de TLB (fácil de hacer con solo ~ 50 entradas), agregue otros cientos de ciclos. Si su patrón de acceso aleatorio en realidad causa un error de página, le costará unos diez mil ciclos en el mejor de los casos, y varios millones en el peor.
Ahora piénselo, ¿qué es lo que quiere evitar con más urgencia?
fuente
Después de obtener la funcionalidad correcta primero . Entonces la selectividad se preocupa por las micro optimizaciones.
Como una pregunta de la entrevista con respecto a las optimizaciones, el código provoca la discusión habitual pero pierde el objetivo de nivel superior de ¿Es el código funcionalmente correcto?
Tanto C ++ como C y otros consideran el
int
desbordamiento como un problema dea + b
. No está bien definido y C lo llama comportamiento indefinido . No se especifica para "envolver", aunque ese es el comportamiento común.IsSumInRange()
Se esperaría que dicha función llamada esté bien definida y funcione correctamente para todos losint
valores dea,b
. Lo crudoa + b
no lo es. La solución de CA podría usar:El código de seguridad puede ser optimizado mediante el uso de un tipo entero más ancha que
int
, si está disponible, como a continuación o distribuir elsum > N
,sum < -N
pruebas dentro de laif (a >= 0)
lógica. Sin embargo, tales optimizaciones pueden no conducir realmente a un código emitido "más rápido" dado un compilador inteligente ni valer el mantenimiento adicional de ser inteligente.Incluso el uso
abs(sum)
es propenso a problemas cuandosum == INT_MIN
.fuente
¿De qué tipo de compiladores estamos hablando y de qué tipo de "memoria"? Porque en su ejemplo, suponiendo un optimizador razonable, la expresión
a+b
generalmente debe almacenarse en un registro (una forma de memoria) antes de realizar dicha aritmética.Entonces, si estamos hablando de un compilador tonto que se encuentra
a+b
dos veces, va a asignar más registros (memoria) en su segundo ejemplo, porque su primer ejemplo podría almacenar esa expresión una vez en un solo registro asignado a la variable local, pero nosotros estamos hablando de compiladores muy tontos en este punto ... a menos que esté trabajando con otro tipo de compilador tonto que la pila derrame todas las variables por todas partes, en cuyo caso tal vez la primera causaría más dificultades para optimizar que el segundo*.Todo esto es extremadamente especulativo en formas bastante tontas en ausencia de mediciones / desmontaje e incluso en el peor de los casos, este no es un caso de "memoria versus rendimiento" (porque incluso entre los peores optimizadores que puedo pensar, no estamos hablando sobre cualquier cosa que no sea memoria temporal como pila / registro), es un caso de "rendimiento" en el mejor de los casos, y entre cualquier optimizador razonable, los dos son equivalentes, y si uno no está usando un optimizador razonable, ¿por qué se obsesiona con la optimización de naturaleza tan microscópica y mediciones especialmente ausentes? Es como un enfoque de nivel de ensamblaje de selección de instrucciones / asignación de registros que nunca esperaría que alguien que buscara ser productivo cuando usa, por ejemplo, un intérprete que acumula todo.
En cuanto a esta pregunta, si puedo abordarla de manera más amplia, a menudo no encuentro los dos diametralmente opuestos. Especialmente si sus patrones de acceso son secuenciales, y dada la velocidad de la memoria caché de la CPU, a menudo una reducción en la cantidad de bytes procesados secuencialmente para entradas no triviales se traduce (hasta cierto punto) para explorar esos datos más rápido. Por supuesto, hay puntos de ruptura en los que si los datos son mucho, mucho más pequeños a cambio de muchas más instrucciones, podría ser más rápido procesar secuencialmente en forma más grande a cambio de menos instrucciones.
Pero he descubierto que muchos desarrolladores tienden a subestimar cuánto puede reducir una reducción en el uso de la memoria en este tipo de casos a reducciones proporcionales en el tiempo dedicado al procesamiento. Es muy humano intuitivo traducir los costos de rendimiento en instrucciones en lugar de acceder a la memoria hasta el punto de alcanzar grandes LUT en un intento vano de acelerar algunos cálculos pequeños, solo para encontrar un rendimiento degradado con el acceso adicional a la memoria.
Para los casos de acceso secuencial a través de una gran matriz (sin hablar de variables escalares locales como en su ejemplo), sigo la regla de que menos memoria para pasar secuencialmente se traduce en un mayor rendimiento, especialmente cuando el código resultante es más simple que lo contrario, hasta que no t, hasta que mis mediciones y mi perfilador me digan lo contrario, y es importante, de la misma manera que supongo que leer secuencialmente un archivo binario más pequeño en el disco sería más rápido de leer que uno más grande (incluso si el más pequeño requiere más instrucciones) ), hasta que se demuestre que esa suposición ya no se aplica en mis mediciones.
fuente