Creo que sería más útil para el interlocutor tener una respuesta más diferenciada, porque veo varias suposiciones no examinadas en las preguntas y en algunas de las respuestas o comentarios.
El tiempo de ejecución relativo resultante de desplazamiento y multiplicación no tiene nada que ver con C. Cuando digo C, no me refiero a la instancia de una implementación específica, como esa o esa versión de GCC, sino el lenguaje. No pretendo tomar este anuncio absurdo, sino utilizar un ejemplo extremo para ilustrarlo: podría implementar un compilador de C completamente compatible con los estándares y hacer que la multiplicación tome una hora, mientras que el cambio toma milisegundos, o al revés. No conozco ninguna restricción de rendimiento en C o C ++.
Puede que no le importe este tecnicismo en la argumentación. Probablemente, su intención era simplemente probar el rendimiento relativo de hacer turnos versus multiplicaciones y eligió C, porque generalmente se percibe como un lenguaje de programación de bajo nivel, por lo que uno puede esperar que su código fuente se traduzca en instrucciones correspondientes más directamente. Estas preguntas son muy comunes y creo que una buena respuesta debería señalar que incluso en C su código fuente no se traduce en instrucciones tan directamente como podría pensar en una instancia determinada. Te he dado algunos posibles resultados de compilación a continuación.
Aquí es donde entran los comentarios que cuestionan la utilidad de sustituir esta equivalencia en el software del mundo real. Puede ver algunos en los comentarios a su pregunta, como el de Eric Lippert. Está en línea con la reacción que generalmente obtendrá de ingenieros más experimentados en respuesta a tales optimizaciones. Si usa cambios binarios en el código de producción como un medio general de multiplicar y dividir, la gente probablemente se encogerá ante su código y tendrá algún grado de reacción emocional ("He escuchado esta afirmación sin sentido sobre JavaScript por el amor de Dios") a eso puede no tener sentido para los programadores novatos, a menos que comprendan mejor las razones de esas reacciones.
Esas razones son principalmente una combinación de la disminución de la legibilidad y la inutilidad de dicha optimización, como ya habrá descubierto al comparar su rendimiento relativo. Sin embargo, no creo que la gente tenga una reacción tan fuerte si la sustitución del turno por la multiplicación fuera el único ejemplo de tales optimizaciones. Preguntas como la suya frecuentemente surgen en varias formas y en diversos contextos. Creo que a lo que los ingenieros más experimentados realmente reaccionan con tanta fuerza, al menos a veces lo hago, es que existe la posibilidad de un rango mucho más amplio de daños cuando las personas emplean tales micro-optimizaciones de manera liberal en toda la base del código. Si trabaja en una empresa como Microsoft en una base de código grande, pasará mucho tiempo leyendo el código fuente de otros ingenieros o intentará localizar cierto código en él. Incluso puede ser su propio código el que intentará tener sentido dentro de unos años, particularmente en algunos de los momentos más inoportunos, como cuando tiene que arreglar un corte de producción después de una llamada que recibió en el buscapersonas deber el viernes por la noche, a punto de salir para una noche de diversión con amigos ... Si pasa tanto tiempo leyendo el código, apreciará que sea lo más legible posible. Imagínese leer su novela favorita, pero el editor ha decidido lanzar una nueva edición donde usan abbrv. todo lo anterior gracias a tu agradecimiento svs spc. Eso es similar a las reacciones que otros ingenieros pueden tener a su código, si los rocía con tales optimizaciones. Como han señalado otras respuestas, es mejor indicar claramente lo que quiere decir,
Sin embargo, incluso en esos entornos, es posible que se encuentre resolviendo una pregunta de entrevista en la que se espera que sepa esta u otra equivalencia. Conocerlos no es malo y un buen ingeniero sería consciente del efecto aritmético del desplazamiento binario. Tenga en cuenta que no dije que esto sea un buen ingeniero, sino que un buen ingeniero lo sabría, en mi opinión. En particular, aún puede encontrar algún gerente, generalmente hacia el final de su ciclo de entrevistas, que le sonreirá ampliamente anticipando la delicia de revelarle este "truco" de ingeniería inteligente en una pregunta de codificación y demostrar que él / ella , también, solía ser o es uno de los ingenieros expertos y no "solo" un gerente. En esas situaciones, solo trate de parecer impresionado y agradézcale por la entrevista esclarecedora.
¿Por qué no viste una diferencia de velocidad en C? La respuesta más probable es que ambos resultaron en el mismo código de ensamblaje:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
Ambos pueden compilarse en
shift(int):
lea eax, [0+rdi*4]
ret
En GCC sin optimizaciones, es decir, utilizando el indicador "-O0", puede obtener esto:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
Como puede ver, pasar "-O0" a GCC no significa que no sea algo inteligente sobre qué tipo de código produce. En particular, observe que incluso en este caso el compilador evitó el uso de una instrucción de multiplicación. Puede repetir el mismo experimento con cambios por otros números e incluso multiplicaciones por números que no son potencias de dos. Lo más probable es que en su plataforma verá una combinación de cambios y adiciones, pero no multiplicaciones. Parece una coincidencia que el compilador aparentemente evite usar multiplicaciones en todos esos casos si las multiplicaciones y los turnos realmente tienen el mismo costo, ¿no es así? Pero no pretendo proporcionar suposiciones como prueba, así que sigamos adelante.
Puede volver a ejecutar su prueba con el código anterior y ver si nota una diferencia de velocidad ahora. Aun así, no está probando cambio versus multiplicación, como puede ver por la ausencia de una multiplicación, sin embargo, pero el código que fue generado con un cierto conjunto de banderas por GCC para las operaciones C de cambio y multiplicación en una instancia particular . Por lo tanto, en otra prueba, puede editar el código de ensamblaje a mano y, en su lugar, usar una instrucción "imul" en el código para el método "multiplicar".
Si quisieras derrotar algunos de esos conocimientos del compilador, podrías definir un método de multiplicación y cambio más general y terminarás con algo como esto:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
Lo que puede generar el siguiente código de ensamblaje:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
Aquí finalmente tenemos, incluso en el nivel de optimización más alto de GCC 4.9, la expresión en las instrucciones de ensamblaje que podría haber esperado cuando inició su prueba inicialmente. Creo que en sí mismo puede ser una lección importante en la optimización del rendimiento. Podemos ver la diferencia que hizo al sustituir variables por constantes concretas en nuestro código, en términos de la inteligencia que el compilador puede aplicar. Las microoptimizaciones como la sustitución shift-multiplicación son algunas optimizaciones de muy bajo nivel que un compilador generalmente puede hacer fácilmente por sí mismo. Otras optimizaciones que son mucho más impactantes en el rendimiento requieren una comprensión de la intención del códigoa menudo el compilador no puede acceder a él o solo puede ser adivinado por alguna heurística. Ahí es donde usted, como ingeniero de software, entra y ciertamente no implica sustituir las multiplicaciones por turnos. Involucra factores como evitar una llamada redundante a un servicio que produce E / S y puede bloquear un proceso. Si va a su disco duro o, Dios no lo quiera, a una base de datos remota para obtener algunos datos adicionales que podría haber derivado de lo que ya tiene en la memoria, el tiempo que pasa esperando supera la ejecución de un millón de instrucciones. Ahora, creo que nos hemos alejado un poco de su pregunta original, pero creo que señalar esto a un interlocutor, especialmente si suponemos que alguien que está empezando a comprender la traducción y la ejecución del código,
Entonces, ¿cuál será más rápido? Creo que es un buen enfoque que elegiste para probar la diferencia de rendimiento. En general, es fácil sorprenderse con el rendimiento en tiempo de ejecución de algunos cambios de código. Hay muchas técnicas que emplean los procesadores modernos y la interacción entre el software también puede ser compleja. Incluso si debe obtener resultados de rendimiento beneficiosos para un cierto cambio en una situación, creo que es peligroso concluir que este tipo de cambio siempre generará beneficios de rendimiento. Creo que es peligroso ejecutar tales pruebas una vez, diga "Bien, ¡ahora sé cuál es más rápido!" y luego aplique indiscriminadamente esa misma optimización al código de producción sin repetir sus mediciones.
Entonces, ¿qué pasa si el cambio es más rápido que la multiplicación? Ciertamente hay indicios de por qué eso sería cierto. GCC, como puede ver arriba, parece pensar (incluso sin optimización) que evitar una multiplicación directa en favor de otras instrucciones es una buena idea. El Manual de referencia de optimización de arquitecturas Intel 64 e IA-32 le dará una idea del costo relativo de las instrucciones de la CPU. Otro recurso, más centrado en la latencia y el rendimiento de la instrucción, es http://www.agner.org/optimize/instruction_tables.pdf. Tenga en cuenta que no son un buen predicador del tiempo de ejecución absoluto, sino del desempeño de las instrucciones entre sí. En un ciclo cerrado, ya que su prueba está simulando, la métrica de "rendimiento" debería ser más relevante. Es el número de ciclos que una unidad de ejecución estará típicamente atada al ejecutar una instrucción dada.
Entonces, ¿qué pasa si el cambio NO es más rápido que la multiplicación? Como dije anteriormente, las arquitecturas modernas pueden ser bastante complejas y cosas como la predicción de ramificaciones, el almacenamiento en caché, la canalización y las unidades de ejecución paralelas pueden dificultar la predicción del rendimiento relativo de dos piezas de código lógicamente equivalentes a veces. Realmente quiero enfatizar esto, porque aquí es donde no estoy contento con la mayoría de las respuestas a preguntas como estas y con el grupo de personas que dicen directamente que simplemente no es cierto (ya) que el cambio es más rápido que la multiplicación.
No, por lo que sé, no inventamos una salsa secreta de ingeniería en la década de 1970 o cuando sea para anular de repente la diferencia de costo de una unidad de multiplicación y una palanca de cambios. Una multiplicación general, en términos de puertas lógicas, y ciertamente en términos de operaciones lógicas, es aún más compleja que un cambio con una palanca de cambios en muchos escenarios, en muchas arquitecturas. Cómo esto se traduce en tiempo de ejecución general en una computadora de escritorio puede ser un poco opaco. No estoy seguro de cómo se implementan en procesadores específicos, pero aquí hay una explicación de una multiplicación: ¿la multiplicación entera es realmente la misma velocidad que la adición en la CPU moderna?
Si bien aquí hay una explicación de un Barrel Shifter . Los documentos a los que me he referido en el párrafo anterior dan otra visión sobre el costo relativo de las operaciones, por medio de instrucciones de CPU. Los ingenieros de Intel con frecuencia parecen tener preguntas similares: los foros de la zona de desarrolladores de Intel registran ciclos para la multiplicación y adición de enteros en el procesador core 2 duo
Sí, en la mayoría de los escenarios de la vida real, y casi seguramente en JavaScript, intentar explotar esta equivalencia por el rendimiento es probablemente una tarea inútil. Sin embargo, incluso si forzamos el uso de instrucciones de multiplicación y luego no vimos ninguna diferencia en el tiempo de ejecución, eso es más debido a la naturaleza de la métrica de costo que usamos, para ser precisos, y no porque no haya diferencia de costo. El tiempo de ejecución de extremo a extremo es una métrica y si es la única que nos importa, todo está bien. Pero eso no significa que todas las diferencias de costos entre multiplicación y desplazamiento simplemente hayan desaparecido. Y creo que ciertamente no es una buena idea transmitir esa idea a un interlocutor, por implicación o de otra manera, que obviamente está comenzando a tener una idea de los factores involucrados en el tiempo de ejecución y el costo del código moderno. La ingeniería siempre se trata de compensaciones. La investigación y la explicación de las compensaciones que los procesadores modernos han hecho para exhibir el tiempo de ejecución que nosotros, como usuarios, terminamos viendo, puede dar una respuesta más diferenciada. Y creo que se justifica una respuesta más diferenciada que "esto simplemente ya no es cierto" si queremos ver a menos ingenieros revisar el código micro-optimizado que borra la legibilidad, porque requiere una comprensión más general de la naturaleza de tales "optimizaciones" para detectar sus diversas y diversas encarnaciones que simplemente referirse a algunas instancias específicas como desactualizadas.