php - transporte - paraderos cl
¿Vale la pena el tiempo de la micro-optimización? (10)
¿Vale la pena el tiempo de la micro-optimización?
No, a menos que sea.
En otras palabras, a priori , la respuesta es "no", pero después de saber que una línea específica de código consume un porcentaje saludable del tiempo del reloj, entonces, y solo entonces, vale la pena optimizarlo.
En otras palabras, primero perfil, porque de lo contrario no tienes ese conocimiento. Este es el método en el que confío , independientemente del idioma o sistema operativo.
Agregado: cuando muchos programadores discuten el rendimiento, de expertos en abajo, tienden a hablar sobre "dónde" el programa pasa su tiempo. Hay una ambigüedad furtiva en ese "dónde" que los aleja de las cosas que podrían ahorrar más tiempo, a saber, los sitios de llamadas de función. Después de todo, la "llamada principal" en la parte superior de una aplicación es un "lugar" en el que el programa casi nunca está "en", pero es responsable del 100% del tiempo. Ahora no se va a deshacer de "llamar a Main", pero casi siempre hay otras llamadas de las que puede deshacerse. Mientras el programa abre o cierra un archivo, o formatea algunos datos en una línea de texto, o espera una conexión de socket, o "nuevo" -ing un trozo de memoria, o pasa una notificación a través de una gran estructura de datos, es gastar grandes cantidades de tiempo en llamadas a funciones, pero ¿es ese "dónde"? De todos modos, esas llamadas se encuentran rápidamente con muestras de la pila.
Soy un desarrollador de PHP y siempre he pensado que las micro-optimizaciones no valen la pena. Si realmente necesita ese rendimiento adicional, podría escribir su software para que sea arquitectónicamente más rápido, o escribir una extensión de C ++ para manejar tareas lentas (o mejor aún, compilar el código usando HipHop). Sin embargo, hoy un compañero de trabajo me dijo que hay una gran diferencia en
is_array($array)
y
$array === (array) $array
y yo estaba como "eh, eso es una comparación sin sentido realmente", pero él no estaría de acuerdo conmigo ... y él es el mejor desarrollador de nuestra compañía y se está haciendo cargo de un sitio web que hace alrededor de 50 millones de consultas SQL por día - - por ejemplo. Entonces, me pregunto si es posible que esté equivocado o que la micro-optimización realmente valga la pena y ¿cuándo?
Bueno, hay más cosas que velocidad para tener en cuenta. Cuando lees esa alternativa ''más rápida'', ¿piensas al instante "Oh, esto está comprobando para ver si la variable es una matriz", o piensas "... wtf"?
Porque realmente, cuando se considera este método, ¿con qué frecuencia se lo llama? ¿Cuál es el beneficio de velocidad exacto? ¿Se acumula cuando la matriz es más grande o más pequeña? Uno no puede hacer optimizaciones sin puntos de referencia.
Además, uno no debería hacer optimizaciones si reducen la legibilidad del código. De hecho, reducir esa cantidad de consultas en unos pocos cientos de miles (y esto a menudo es más fácil de lo que uno pensaría) u optimizarlas si corresponde, sería mucho, mucho más beneficioso para el rendimiento que esta micro-optimización.
Además, no te dejes intimidar por la experiencia del chico, como otros han dicho, y piensa por ti mismo.
Bueno, para una matriz trivialmente pequeña, $array === (array) $array
es significativamente más rápido que is_array($array)
. En el orden de más de 7 veces más rápido. Pero cada llamada es solo del orden de 1.0 x 10 ^ -6
segundos ( 0.000001 seconds
). Entonces, a menos que lo llames literalmente miles de veces, no valdrá la pena. Y si lo llamas miles de veces, te sugiero que estás haciendo algo mal ...
La diferencia se produce cuando se trata de una matriz grande. Como $array === (array) $array
requiere una nueva variable para ser copiada requiere que la matriz sea iterada internamente para la comparación, es SIGNIFICATIVAMENTE más lenta para una matriz grande. Por ejemplo, en una matriz con 100 elementos enteros, is_array($array)
encuentra dentro de un margen de error ( < 2%
) de is_array()
con una pequeña matriz (entrando a 0.0909
segundos para 10.000 iteraciones). Pero $array = (array) $array
es extremadamente lento. Por solo 100 elementos, ya es más del doble de lento que is_array()
(entrando a 0.203
segundos). Para 1000 elementos, is_array
mantuvo igual, sin embargo, la comparación de 2.0699
aumentó a 2.0699
segundos ...
La razón por la que es más rápido para arreglos pequeños es que is_array()
tiene la sobrecarga de ser una llamada a función, donde la operación de is_array()
es una construcción de lenguaje simple ... Y iterar sobre una variable pequeña (en código C) será típicamente más barato que el función de llamada de arriba. Pero, para variables más grandes, la diferencia crece ...
Es una compensación. Si la matriz es lo suficientemente pequeña, la iteración será más eficiente. Pero a medida que el tamaño de la matriz crece, será cada vez más lento (y, por lo tanto, la llamada a la función será más rápida).
Otra forma de verlo
Otra forma de verlo sería examinar la complejidad algorítmica de cada lanzamiento.
Echemos un vistazo a is_array()
primero. Su código fuente básicamente muestra que es una operación O(1)
. Lo que significa que es una operación de tiempo constante. Pero también debemos mirar la llamada a la función. En PHP, las llamadas a funciones con un solo parámetro de matriz son O(1)
u O(n)
dependiendo de si es necesario activar la copia en escritura. Si llama a is_array($array)
cuando $array
es una referencia de variable, se activará copy-on-write y se realizará una copia completa de la variable.
Por lo tanto, is_array()
es el mejor caso O(1)
y el peor de los casos O(n)
. Pero mientras no uses referencias, siempre es O(1)
...
La versión de lanzamiento, por otro lado, hace dos operaciones. Hace un yeso, luego hace un control de igualdad. Entonces, veamos cada uno por separado. El handler operador de handler primero fuerza una copia de la variable de entrada. No importa si es una referencia o no. Entonces, simplemente usar el operador de conversión (array)
fuerza una iteración O(n)
sobre la matriz para lanzarla (a través de la llamada copy_ctor).
Luego, convierte la nueva copia en una matriz. Esto es O(1)
para matrices y primitivas, pero O(n)
para objetos.
Entonces, el operador idéntico se ejecuta. El handler es solo un proxy de is_identical_function()
. Ahora, is_identical causará un cortocircuito si $array
no es una matriz. Por lo tanto, tiene un mejor caso de O(1)
. Pero si $array
es una matriz, puede volver a cortocircuitarse si las tablas hash son idénticas (lo que significa que ambas variables son copias de escritura sobre escritura). Entonces ese caso es O(1)
también. Pero recuerda que forzamos una copia arriba, por lo que no podemos hacer eso si se trata de una matriz. Entonces es O(n)
gracias a zend_hash_compare ...
Entonces, el resultado final es esta tabla del peor tiempo de ejecución:
+----------+-------+-----------+-----------+---------------+
| | array | array+ref | non-array | non-array+ref |
+----------+-------+-----------+-----------+---------------+
| is_array | O(1) | O(n) | O(1) | O(n) |
+----------+-------+-----------+-----------+---------------+
| (array) | O(n) | O(n) | O(n) | O(n) |
+----------+-------+-----------+-----------+---------------+
Tenga en cuenta que parece que escalan igual para las referencias. Ellos no. Ambos escalan linealmente para variables referenciadas. Pero el factor constante cambia. Por ejemplo, en una matriz referenciada de tamaño 5, is_array realizará 5 asignaciones de memoria y 5 copias de memoria, seguidas por 1 verificación de tipo. La versión emitida, por otro lado, realizará 5 asignaciones de memoria, 5 copias de memoria, seguidas de 2 comprobaciones de tipo, seguidas de 5 comprobaciones de tipo y 5 verificaciones de igualdad ( memcmp()
o similares). Así que n=5
produce 11 operaciones para is_array
, pero 22 operaciones para ===(array)
...
Ahora, is_array()
tiene la sobrecarga O (1) de un apilamiento de pila (debido a la llamada de función), pero eso solo dominará el tiempo de ejecución para valores extremadamente pequeños de n
(vimos en el punto de referencia anterior que solo 10 elementos de matriz suficiente para eliminar por completo toda diferencia).
La línea de fondo
Sugiero ir por la legibilidad sin embargo. Encuentro que is_array($array)
es mucho más legible que $array === (array) $array
. Así que obtienes lo mejor de ambos mundos.
El script que utilicé para el punto de referencia:
$elements = 1000;
$iterations = 10000;
$array = array();
for ($i = 0; $i < $elements; $i++) $array[] = $i;
$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) is_array($array);
$e = microtime(true);
echo "is_array completed in " . ($e - $s) ." Seconds/n";
$s = microtime(true);
for ($i = 0; $i < $iterations; $i++) $array === (array) $array;
$e = microtime(true);
echo "Cast completed in " . ($e - $s) ." Seconds/n";
Editar: para el registro, estos resultados fueron con 5.3.2 en Linux ...
Edit2: Se corrigió el motivo por el que la matriz es más lenta (se debe a la comparación iterada en lugar de a la memoria). Ver compare_function para el código de iteración ...
Bueno, voy a suponer que is_array($array)
es la forma preferida, y $array === (array) $array
es la forma supuestamente más rápida (lo que plantea la pregunta de por qué is_array
no se implementa utilizando esa comparación , pero yo divago).
Casi nunca regresaré a mi código e insertaré una microoptimización * , pero a menudo los incluiré en el código mientras lo escribo, siempre que:
- no ralentiza mi tipeo.
- la intención del código aún está clara.
Esa optimización particular falla en ambos aspectos.
* De acuerdo, de hecho lo hago, pero eso tiene más que ver con que tengo un toque de TOC en lugar de buenas prácticas de desarrollo.
Como dice el cliché, la microoptimización generalmente vale la pena el tiempo solo en los puntos de acceso más pequeños y más críticos para el rendimiento de tu código, solo después de que hayas demostrado que es ahí donde está el cuello de botella. Sin embargo, me gustaría detallarlo un poco, señalar algunas excepciones y áreas de malentendidos.
Esto no significa que el rendimiento no deba considerarse en absoluto por adelantado. Defino la micro-optimización como optimizaciones basadas en detalles de bajo nivel del compilador / intérprete, el hardware, etc. Por definición, una micro-optimización no afecta la complejidad del big-O. Las macrooptimizaciones deben considerarse por adelantado, especialmente cuando tienen un gran impacto en el diseño de alto nivel. Por ejemplo, es bastante seguro decir que si tiene una estructura de datos grande y de acceso frecuente, una búsqueda lineal O (N) no va a cortarla. Incluso cosas que son solo términos constantes pero que tienen una sobrecarga grande y obvia podrían valer la pena considerar por adelantado. Dos ejemplos importantes son la asignación excesiva de memoria / copia de datos y el cálculo de la misma cosa dos veces cuando podría estar informándola una vez y almacenando / reutilizando el resultado.
Si está haciendo algo que se ha hecho antes en un contexto ligeramente diferente, puede haber algunos cuellos de botella que son tan conocidos que es razonable considerarlos por adelantado. Por ejemplo, recientemente estuve trabajando en una implementación del algoritmo FFT (Transformada rápida de Fourier) para la biblioteca estándar D. Dado que se han escrito tantas FFT en otros idiomas, es bien sabido que el cuello de botella más grande es el rendimiento de la memoria caché, por lo que entré inmediatamente en el proyecto pensando en cómo optimizarlo.
En general, no debe escribir ninguna optimización que haga que su código sea más feo o más difícil de entender; en mi libro esto definitivamente cae en esta categoría.
Es mucho más difícil volver atrás y cambiar el código anterior que escribir un código nuevo, porque debe hacer una prueba de regresión. Entonces, en general, ningún código que ya esté en producción debería cambiarse por razones frívolas.
PHP es un lenguaje tan increíblemente ineficiente que si tiene problemas de rendimiento, probablemente debería refactorizar los puntos conflictivos para que ejecuten menos código PHP de todos modos.
Por lo tanto, diría que en general no, y en este caso no, y en los casos en que absolutamente lo necesita Y ha medido que hace una diferencia comprobable Y es el triunfo más rápido (fruta fácil), sí.
Ciertamente, dispersar micro-optimizaciones como esta a lo largo de su código existente, de trabajo y probado es algo terrible de hacer, definitivamente introducirá regresiones y casi con certeza no hará una diferencia notoria.
La micro-optimización vale la pena cuando tiene evidencia de que está optimizando un cuello de botella .
Normalmente no vale la pena: escriba el código más legible que pueda y use puntos de referencia realistas para verificar el rendimiento. Si encuentra que tiene un cuello de botella, micro-optimice solo ese pedazo de código (midiendo sobre la marcha). A veces, una pequeña cantidad de micro-optimización puede marcar una gran diferencia.
Pero no micro-optimice todo su código ... terminará siendo mucho más difícil de mantener, y es muy probable que descubra que ha perdido el verdadero cuello de botella, o que sus micro-optimizaciones están perjudicando el rendimiento en lugar de ración.
Las microoptimizaciones en mi humilde opinión en realidad son incluso más relevantes que las optimizaciones algorítmicas hoy en día si trabajas en un campo crítico para el rendimiento. Esto podría ser una gran cosa, porque muchas personas no trabajan en áreas críticas para el rendimiento, incluso para el software crítico para el rendimiento, ya que pueden estar haciendo llamadas de alto nivel en una biblioteca de terceros que hace el trabajo real crítico para el rendimiento. Por ejemplo, muchas personas que intentan escribir una imagen o un software de video pueden escribir códigos que no sean de rendimiento crítico que deseen a nivel de imagen, sin tener que pasar manualmente varios millones de píxeles a más de 100 fotogramas por segundo. La biblioteca hace eso por ellos.
Cuando digo que las microoptimizaciones son más relevantes que las algorítmicas actuales, no me refiero a que, por ejemplo, el código SIMD paralelizado que minimice las fallas de caché al aplicar una clase de burbuja superará una clasificación introsort o radix. Lo que quiero decir es que los profesionales no hacen burbujas para clasificar tamaños de entrada grandes.
Si toma un lenguaje razonablemente de alto nivel hoy, del cual incluyo C ++, ya tiene su cuota de estructuras de datos y algoritmos de propósito general razonablemente eficientes al alcance de su mano. No hay excusa a menos que sea un estudiante de CS principiante y se reinventa la rueda más primitiva para aplicar clases de complejidad cuadrática a tamaños de entrada masivos o búsquedas de tiempo lineal que se pueden realizar en tiempo constante con los datos apropiados estructuras.
Entonces, una vez que superas este nivel de principiante, las aplicaciones críticas para el rendimiento aún tienen características de rendimiento muy diferentes. ¿Por qué? ¿Por qué un software de procesamiento de video tiene tres veces la velocidad de cuadro y más vistas previas de video interactivo que el otro cuando los desarrolladores no están haciendo nada extremadamente tonto algorítmicamente? ¿Por qué un servidor que hace algo muy similar puede manejar diez veces las consultas con el mismo hardware? ¿Por qué este software carga una escena en 5 segundos mientras que el otro tarda 5 minutos en cargar la misma información? ¿Por qué este bello juego tiene velocidades de cuadros sedosas y consistentes mientras que el otro es más feo, más primitivo con sus gráficos e iluminación, y tartamudea aquí y allá mientras toma el doble de memoria?
Y eso se reduce a micro optimizaciones, no a diferencias algorítmicas. Además, nuestra jerarquía de memoria hoy en día está tan sesgada en el rendimiento, por lo que los algoritmos previos que se consideraban buenos hace un par de décadas ya no son tan buenos si exhiben una localidad de referencia deficiente.
Entonces, si desea escribir un software competitivo eficientemente, con mucha frecuencia, eso se reducirá a cosas como multihilo, SIMD, GPU, GPGPU, mejorando la localidad de referencia con mejores patrones de acceso a la memoria (bucle de mosaico, SoA, caliente / división de campo frío, etc.), tal vez incluso la optimización para la predicción de bifurcación en casos extremos, y así sucesivamente, no tanto avances algorítmicos a menos que esté abordando un territorio extremadamente inexplorado donde ningún programador se ha aventurado antes.
Todavía hay avances algorítmicos ocasionales que son posibles modificadores de juego, como el rastreo de cono voxel recientemente. Pero esas son excepciones y las personas que inventan esto a menudo invierten sus vidas en I + D (generalmente no son personas que escriben y mantienen bases de código de gran escala), y aún se reduce a micro-optimizaciones si se puede aplicar el trazado de cono voxel. a entornos en tiempo real como juegos o no. Si no eres bueno en micro-optimizaciones, simplemente no obtendrás las tasas de fotogramas adecuadas ni siquiera usando estos avances algorítmicos.
Tuvimos un lugar donde la optimización fue realmente útil.
Aquí algunas comparaciones:
is_array($v)
: 10 sec
$v === (array)$v
: 3,3 sec
($v.'''') === ''Array''
: 2,6 segundos
El último se convierte en una cadena, una matriz siempre se convierte en una cadena con el valor ''Matriz''. Esta verificación será incorrecta, si $ v es una cadena con el valor ''Matriz'' (nunca sucede en nuestro caso).
La micro optimización no vale la pena. La legibilidad del código es mucho más importante que la micro-optimización.
Gran artículo sobre la micro-optimización inútil por Fabien Potencier (creador del framework Symfony ):
imprimir contra eco, ¿cuál es más rápido?
Print usa un código de operación más porque realmente devuelve algo. Podemos concluir que el eco es más rápido que la impresión. Pero un código de operación no cuesta nada, realmente nada. Incluso si un script tiene cientos de llamadas para imprimir. Intenté una nueva instalación de WordPress. La secuencia de comandos se detiene antes de que termine con un "Error de bus" en mi computadora portátil, pero el número de códigos de operación ya era de más de 2,3 millones. Basta de charla.