Optimización elemental
Cuando se trata de optimización, mucha gente se muestra desdeñosa: "Ahora que las computadoras son tan rápidas, ¿qué sentido tiene ser un porcentaje más rápido?" Esto tiene cierto sentido. Los resultados compilados por los compiladores actuales se han optimizado por completo. Excepto para el desarrollo de software específico como gráficos, imágenes y multimedia, la optimización deliberada no es necesaria en la mayoría de los casos, pero si los desarrolladores escriben código. En ese momento, ya tenía conocimiento de la optimización. Mientras completa la optimización, puede garantizar o incluso mejorar la eficiencia del desarrollo.
Por supuesto, el diseño del algoritmo es el núcleo de la optimización. En la mayoría de los casos, la eficiencia de ejecución del programa está determinada principalmente por la comprensión general del programa por parte del desarrollador, el diseño del algoritmo, etc. ¡Pero a veces la optimización de los detalles también tiene sentido!
Además, en muchos casos, este tipo de optimización no requiere escribir código directamente a través del ensamblador, pero en este caso también puede reflejar la superioridad de dominar el conocimiento del ensamblador.
Como las siguientes dos funciones:
función GetBit(i: Cardinal; n: Cardinal): booleano;
comenzar
Resultado := Booleano((i shr n) y 1);
fin;
función GetBit(i: Cardinal; n: Cardinal): booleano;
comenzar
Resultado := Booleano((1 shl n) e i);
fin;
Código ensamblador correspondiente:
MOV ECX, EDX
SHR EAX, CL
Y EAX, $01
MOV ECX, EDX
MOVEDX, $01
SHL EDX, CL
Y EAX, EDX
Tienen la misma función, todos toman el valor de un determinado bit de i, devuelven Verdadero si es 1 y Falso si es 0.
En la superficie, puede pensar que la eficiencia de ejecución de las dos funciones es la misma, pero de hecho hay una diferencia. La operación de cambio del primer programa se realiza en i. De acuerdo con la convención de llamada predeterminada en Delphi, regístrese. en este momento, i El valor se almacena en el registro EAX y la operación de cambio se puede completar directamente, pero el segundo programa es diferente. Para completar la operación de cambio en el valor inmediato 1, primero se debe transferir al registro; ¡Entonces debe haber una instrucción más! Por supuesto, no en todos los casos, menos instrucciones serán necesariamente más rápidas que más instrucciones. Durante la ejecución específica, también debemos considerar cuestiones como el ciclo de reloj de ejecución de instrucciones y el emparejamiento de instrucciones (no se explicará más sobre esto más adelante). El problema se puede resolver de forma independiente. Solo se puede realizar cuando las comparaciones solo se pueden realizar en entornos de código específicos.
En circunstancias normales, esta diferencia en eficiencia es demasiado insignificante, ¡pero nunca es malo estar atento a la optimización durante la programación! Si dicho código está ubicado en la capa más interna de un bucle y N ciclos de reloj se acumulan a través de una gran cantidad de bucles, la diferencia en la eficiencia de ejecución puede volverse muy grande.
Lo anterior es solo un pequeño ejemplo. Se puede ver que si puede pensar en algunos problemas desde la perspectiva del ensamblaje durante el desarrollo, puede escribir código detallado más eficiente en lenguajes de alto nivel y al mismo tiempo garantizar la eficiencia del desarrollo. Pero todavía hay muchas ocasiones en las que se debe completar una optimización detallada utilizando código ensamblador incorporado y, a veces, debido a la aplicación de código ensamblador incorporado, la escritura de código también puede volverse más eficiente.
Si necesita invertir el orden de los bytes de un número de 32 dígitos, ¿cómo puede hacerlo completamente en lenguaje de alto nivel en Delphi? Puede usar el cambio, también puede llamar a la función incorporada Intercambiar varias veces, pero si piensa en una instrucción BSWAP, todo se vuelve muy simple.
función SwapLong(Valor: Cardinal): Cardinal;
ENSAMBLE
EAX BSWAP
fin;
Nota: Igual que arriba, el valor de Valor se almacena en el registro EAX y el valor de 32 dígitos también se devuelve a través de EAX, por lo que solo se necesita una oración.
Por supuesto, la mayoría de las optimizaciones de ensamblaje integradas no son tan simples, pero es difícil lograr una optimización más profunda con el poco conocimiento de ensamblaje aprendido en la universidad. ¡La experiencia solo se puede obtener mediante la acumulación y comparación continua de códigos ensambladores compilados! Afortunadamente, en la mayoría de los casos, la optimización detallada no es la parte principal del diseño del programa.
Sin embargo, si el programa desarrollado incluye gráficos, imágenes, multimedia, etc., ¡aún es necesario realizar una optimización más profunda! Afortunadamente, Delphi6 puede brindar un buen soporte ya sea para la optimización de instrucciones de punto flotante o la aplicación de MMX, SSE, 3DNow, etc. Incluso si desea que las versiones anteriores de Delphi admitan estos conjuntos de instrucciones extendidos de CPU o desee admitir nuevos conjuntos de instrucciones de CPU en el futuro, puede utilizar las cuatro instrucciones de ensamblaje DB, DW, DD y DQ admitidas por Delphi en el ensamblaje integrado ( en el manual de idioma oficial de Delphi6 de Borland solo dice que DB, DW y DD son compatibles. La representación numérica de las instrucciones relacionadas insertadas también se puede implementar de manera flexible.
como:
DW $A20F //CPUID
DW $770F //EMMS
DB $0F, $6F, $C1 //MOVQ MM0, MM1
Comprender las instrucciones es solo la base. Después de diseñar el algoritmo en torno a FPU, MMX y SSE, si desea optimizarlo aún más, también debe comprender algunas de las características técnicas de la propia CPU.
Echemos un vistazo a los siguientes dos fragmentos de código:
ENSAMBLE
AÑADIR [a], ECX
AÑADIR [b], EDX
fin
ENSAMBLE
MOV EAX, [a]
MOV EBX, [b]
AÑADIR EAX, ECX
AÑADIR EBX, EDX
MOV [a], EAX
MOV [b], EBX
fin
¿Es el segundo más eficiente? Incorrecto, como se mencionó anteriormente, menos instrucciones no significa una alta eficiencia de ejecución. Según la información relevante, el ciclo de reloj para la ejecución de las dos instrucciones en la primera sección del código es 3 (cada instrucción debe completar tres pasos de lectura). modificar y escribir), los ciclos de reloj ejecutados por las seis instrucciones en la segunda sección del código son todos 1. Entonces, ¿las dos piezas de código son igualmente eficientes? Mal de nuevo, de hecho, el segundo fragmento de código se ejecuta de manera más eficiente que el primero. ¿Por qué? Debido a que las CPU posteriores a la clase Pentium tienen dos canalizaciones para ejecutar instrucciones, cuando se pueden emparejar dos instrucciones adyacentes, ¡se pueden ejecutar al mismo tiempo! Específicamente para los dos fragmentos de código anteriores, ¿cuáles son las razones específicas?
Aunque las dos instrucciones del primer código se pueden emparejar, el ciclo de reloj de ejecución total requerido es 5 en lugar de 3, mientras que las seis instrucciones del segundo código se pueden ejecutar en paralelo, lo que conduce a este resultado.
Hablando de eso, todos estos son ejemplos muy simples, que por sí solos no pueden brindarle mucha ayuda. Si realmente desea optimizar un programa específico, debe buscar algunos artículos especiales sobre optimización de FPU y MMX, o buscar manuales técnicos para estudiar y estudiar tecnologías como "ejecución fuera de orden" y "predicción de rama". Solo espero que los amigos que están en la universidad no solo se concentren en esas herramientas de desarrollo para "hacer dinero" y las nuevas tecnologías de moda, sino que puedan dedicar más tiempo a sentar las bases para que puedan dominar rápidamente nuevos conocimientos. entonces podrás dominar nuevas herramientas y habilidades de desarrollo más rápido... (omita mil palabras).
Pero, de nuevo, el conocimiento aún debe usarse para resolver problemas prácticos. Si solo te concentras en los detalles técnicos todos los días, puedes convertirte en un excelente hacker, pero nunca desarrollarás software de primera clase. Por tanto, el propósito fundamental debe seguir siendo la creación de valor. Entonces... no hablaré más de eso, realmente no parecerá un artículo técnico si hablo más de eso. ^_^
Adjunto: Además de considerar la eficiencia de la ejecución, la optimización del programa también debe considerar cuestiones de tamaño (un tamaño pequeño puede cargar la memoria más rápido y completar la decodificación de instrucciones y otras tareas más rápido. Por ejemplo, para borrar el registro EAX, use SUB EAX, EAX o XOR EAX). , EAX en lugar de MOV EAX, $0 Aunque sus ciclos de reloj de ejecución son ambos 1, la longitud de la instrucción del primero (2 bytes) es obviamente más corta que la del segundo (5 bytes). Pero debido a que lo mencionado anteriormente son todos detalles, la cuestión del volumen no se mencionó. Más problemas de reducción de tamaño deben dejarse en manos del compilador. Solo preste un poco de atención al escribir el código ASM incrustado.