Estoy buscando un ejemplo en el que un algoritmo aparentemente está cambiando su clase de complejidad debido a las estrategias de optimización del compilador y / o procesador.
int main(void) { exit(0); };
Estoy buscando un ejemplo en el que un algoritmo aparentemente está cambiando su clase de complejidad debido a las estrategias de optimización del compilador y / o procesador.
int main(void) { exit(0); };
Respuestas:
Tomemos un programa simple que imprime el cuadrado de un número ingresado en la línea de comando.
#include <stdio.h>
int main(int argc, char **argv) {
int num = atoi(argv[1]);
printf("%d\n",num);
int i = 0;
int total = 0;
for(i = 0; i < num; i++) {
total += num;
}
printf("%d\n",total);
return 0;
}
Como puede ver, este es un cálculo de O (n), que se repite una y otra vez.
Compilar esto con gcc -S
uno obtiene un segmento que es:
LBB1_1:
movl -36(%rbp), %eax
movl -28(%rbp), %ecx
addl %ecx, %eax
movl %eax, -36(%rbp)
movl -32(%rbp), %eax
addl $1, %eax
movl %eax, -32(%rbp)
LBB1_2:
movl -32(%rbp), %eax
movl -28(%rbp), %ecx
cmpl %ecx, %eax
jl LBB1_1
En esto, puede ver la adición realizada, una comparación y un salto hacia atrás para el ciclo.
Haciendo la compilación gcc -S -O3
para obtener optimizaciones del segmento entre las llamadas a printf:
callq _printf
testl %ebx, %ebx
jg LBB1_2
xorl %ebx, %ebx
jmp LBB1_3
LBB1_2:
imull %ebx, %ebx
LBB1_3:
movl %ebx, %esi
leaq L_.str(%rip), %rdi
xorb %al, %al
callq _printf
Ahora se puede ver que no tiene bucle y, además, no se agrega. En cambio, hay una llamada a la imull
que multiplica el número por sí mismo.
El compilador ha reconocido un bucle y el operador matemático dentro y lo ha reemplazado por el cálculo adecuado.
Tenga en cuenta que esto incluyó una llamada a atoi
para obtener el número. Cuando el número ya existe en el código, el cumplidor calculará previamente el valor en lugar de realizar llamadas reales como se demuestra en una comparación entre el rendimiento de sqrt en C # y C donde sqrt(2)
(una constante) se sumaba en un bucle 1,000,000 de veces.
Tail Call Optimization puede reducir la complejidad del espacio. Por ejemplo, sin TCO, esta implementación recursiva de un while
bucle tiene una complejidad espacial en el peor de los casos Ο(#iterations)
, mientras que con TCO tiene una complejidad espacial en el peor de los casos Ο(1)
:
// This is Scala, but it works the same way in every other language.
def loop(cond: => Boolean)(body: => Unit): Unit = if (cond) { body; loop(cond)(body) }
var i = 0
loop { i < 3 } { i += 1; println(i) }
// 1
// 2
// 3
// E.g. ECMAScript:
function loop(cond, body) {
if (cond()) { body(); loop(cond, body); };
};
var i = 0;
loop(function { return i < 3; }, function { i++; print(i); });
Esto ni siquiera necesita un TCO general, solo necesita un caso especial muy estrecho, a saber, la eliminación de la recursión directa de la cola.
Sin embargo, lo que sería muy interesante es cuando la optimización del compilador no solo cambia la clase de complejidad sino que también cambia el algoritmo por completo.
El glorioso compilador de Glasgow Haskell a veces hace esto, pero eso no es realmente de lo que estoy hablando, es más como hacer trampa. GHC tiene un lenguaje de coincidencia de patrones simple que permite al desarrollador de la biblioteca detectar algunos patrones de código simples y reemplazarlos con un código diferente. Y la implementación de GHC de la biblioteca estándar de Haskell no contiene algunas de esas anotaciones, para que los usos específicos de las funciones específicas que son conocidos por ser ineficiente se vuelven a escribir en versiones más eficientes.
Sin embargo, estas traducciones están escritas por humanos, y están escritas para casos específicos, por eso considero que hacer trampa.
Un Supercompilador puede cambiar el algoritmo sin intervención humana, pero AFAIK no se ha construido ningún supercompilador a nivel de producción.
Un compilador que es consciente de que el lenguaje está usando la reducción de fuerza de gran número (reemplazando las multiplicaciones por el índice de un ciclo por una suma) cambiaría la complejidad de esa multiplicación de O (n log n) en el mejor de los casos a O (n) .