Cómo reducir el uso de CPU de programas escritos en C
La optimización es un tema muy importante. Este artículo no profundizará en la teoría del análisis del rendimiento y la eficiencia del algoritmo. Solo quiero resumir aquí algunas técnicas de optimización que se pueden aplicar fácilmente a su código C, de modo que cuando encuentre varias estrategias de programación diferentes, pueda obtener una estimación aproximada del rendimiento de cada estrategia. Este es también el propósito de este artículo.
1. Antes de la optimización
Antes de optimizar, lo primero que debemos hacer es encontrar dónde está el cuello de botella (cuello de botella) de nuestro código. Sin embargo, cuando haga esto, tenga cuidado de no extrapolar desde una versión de depuración porque la versión de depuración contiene una gran cantidad de código adicional. Un ejecutable de versión de depuración es 40 veces más grande que el ejecutable de versión de lanzamiento. Esos códigos adicionales se utilizan para admitir la depuración, como la búsqueda de símbolos. La mayoría de las implementaciones proporcionan diferentes funciones de operador nuevas y de biblioteca para la versión de depuración y la versión de lanzamiento. Además, un ejecutable de versión de lanzamiento puede haberse optimizado de diversas formas, incluida la eliminación de objetos temporales innecesarios, el desarrollo de bucles, el movimiento de objetos en registros, la inserción en línea, etc.
Además, debemos distinguir entre depuración y optimización. Están completando diferentes tareas. La versión de depuración se utiliza para buscar errores y comprobar si hay problemas lógicos en el programa. La versión de lanzamiento se utiliza para realizar algunos ajustes y optimizaciones de rendimiento.
Veamos qué técnicas de optimización de código existen:
2. Colocación de declaraciones
Dónde colocar las declaraciones de variables y objetos en el programa La ubicación tendrá un impacto significativo en el rendimiento. Asimismo, la elección de operadores de prefijo y sufijo también afecta el rendimiento. En esta sección, nos centramos en cuatro cuestiones: inicialización frente a asignación, colocación de declaraciones donde el programa realmente quiere usarlas, la lista de inicialización del constructor y operador de prefijo frente a postfijo.
(1) Utilice inicialización en lugar de asignación.
En lenguaje C, las declaraciones de variables solo se permiten al comienzo del cuerpo de una función. Sin embargo, en C, las declaraciones pueden aparecer al final. final del programa. El propósito de esto es retrasar la declaración del objeto hasta que realmente se utilice. Hacer esto tiene dos beneficios: 1. Garantiza que el objeto no será modificado maliciosamente por otras partes del programa antes de su uso. Si el objeto se declara al principio pero se utiliza después de 20 líneas, no se puede ofrecer tal garantía. 2. Nos brinda la oportunidad de mejorar el rendimiento reemplazando la asignación con la inicialización. En el pasado, las declaraciones solo se podían colocar al principio. Sin embargo, a menudo no obtuvimos el valor que queríamos al principio, por lo que los beneficios de la inicialización sí. no se utilizará. Pero ahora podemos inicializar directamente cuando obtengamos el valor deseado, ahorrando así un paso. Tenga en cuenta que puede que no haya ninguna diferencia entre la inicialización y la asignación para los tipos básicos, pero para los tipos definidos por el usuario, las dos traerán una diferencia significativa, porque la asignación requerirá una llamada de función más: --operator=. Entonces, cuando elegimos entre asignación e inicialización, la inicialización debería ser nuestra primera opción.
(2) Coloque la declaración en la ubicación adecuada
En algunos casos, la mejora del rendimiento obtenida al mover la declaración a la ubicación adecuada debería atraer nuestra atención suficiente.
Por ejemplo:
bool is_C_Needed();
uso nulo()
{
C
if (is_C_Needed() == false)
{
return; //c1 no era necesario
}
// use c1 aquí
return;
}
En el código anterior, el objeto c1 se creará incluso si no es posible usarlo, por lo que Pagará gastos innecesarios por ello. Tal vez diga cuánto tiempo puede perder un objeto c1, pero ¿y si este es el caso? C c1[1000] Creo que no es un desperdicio decir que es un desperdicio. Pero podemos cambiar esta situación moviendo la posición donde se declara c1:
void use()
{
if (is_C_Needed() == false)
{
return; //c1 no era necesario
}
C c1; //se movió desde el principio del bloque
p>
//use c1 aquí
return;
}
¿Qué tal si el rendimiento del programa ha mejorado mucho? Por lo tanto, analice su código detenidamente y coloque la declaración en la posición adecuada. Los beneficios que traerá son inimaginables.
(3) Lista de inicialización
Todos sabemos que la lista de inicialización se usa generalmente para inicializar miembros de datos constantes o de referencia. Pero debido a su propia naturaleza, podemos mejorar el rendimiento utilizando una lista de inicialización. Primero veamos un programa:
clase Persona
{
privado:
C c_1;
C c_2;
público:
Persona(campo const; c1, campo const; c2): c_1(c1), c_2(c2) {}
};
Por supuesto, también podemos escribir el constructor así:
Persona::Persona(const Camp; c1, const Camp; c2)
{ p>
c_1 = c1;
c_2 = c2;
}
Entonces, ¿qué tipo de diferencia de rendimiento aportarán los dos? Para comprender este problema, primero debemos comprender cómo se ejecutan los dos. Primero veamos la lista de inicialización: las operaciones de declaración de los miembros de datos se completan antes de la ejecución del constructor. En el constructor, a menudo solo se completa la operación de asignación. , sin embargo, la lista de inicialización se inicializa directamente cuando se declara el miembro de datos, por lo que solo ejecuta el constructor de copia una vez. Veamos la situación de asignar valores en el constructor: primero, los miembros de datos se crearán a través del constructor predeterminado antes de que se ejecute el constructor, y luego se asignarán a través del operador = en el constructor. Por lo tanto, realiza una llamada de función más que la lista de inicialización. La diferencia de rendimiento sale a la luz. Pero tenga en cuenta que si sus miembros de datos son todos tipos básicos, entonces, por razones de legibilidad del programa, no utilice la lista de inicialización, porque el código ensamblador generado por el compilador es el mismo para ambos.
(4) operador de prefijo VS postfix
El operador de prefijo y - son más eficientes que su versión postfix, porque cuando se usa el operador postfix, se crea un objeto temporal para guardar el valor anterior . Para los tipos básicos, el compilador eliminará esta copia adicional, pero para los tipos definidos por el usuario, esto parece imposible. Por lo tanto, utilice el operador de prefijo tanto como sea posible
3 Funciones en línea
Las funciones en línea no solo pueden eliminar la carga de eficiencia causada por las llamadas a funciones, sino que también conservan las ventajas de las funciones generales. . Sin embargo, las funciones en línea no son una panacea y, en algunos casos, pueden incluso reducir el rendimiento del programa. Por tanto se debe utilizar con precaución.
1. Primero veamos los beneficios que nos brindan las funciones en línea: desde la perspectiva del usuario, las funciones en línea parecen funciones ordinarias. Pueden tener parámetros y valores de retorno, y también pueden tener su propio alcance. llamadas a funciones ordinarias. Además, puede ser más seguro y fácil de depurar que las macros.
Por supuesto, hay que tener en cuenta una cosa: el especificador en línea es solo una sugerencia para el compilador, y el compilador tiene derecho a ignorar esta sugerencia. Entonces, ¿cómo decide el compilador si una función está integrada o no? En términos generales, los factores clave incluyen el tamaño del cuerpo de la función, si hay objetos locales declarados, la complejidad de la función, etc.
2. Entonces, ¿qué sucede si una función se declara en línea pero no está en línea? En teoría, cuando el compilador se niega a incorporar una función, esa función se tratará como una función normal, pero surgirán algunos otros problemas. Por ejemplo, el siguiente código:
// nombre de archivo Hora.h
#include
#include
usando el espacio de nombres std;
p>hora de clase
{
público:
inline void Show() { for (int i = 0; ilt; 10 ; i ) coutlt; };
Debido a que la función miembro Time::Show() incluye una variable local y un bucle for, el compilador generalmente la rechaza en línea y la trata como una función miembro ordinaria. Sin embargo, este archivo de encabezado que contiene la declaración de clase se #incluirá individualmente en cada unidad de compilación independiente:
// nombre de archivo f1.cpp
#include "Time.hj"
void f1()
{
Tiempo t1;
t1.Show();
}
// nombre de archivo f2.cpp
#include "Time.h"
void f2()
{
Hora t2;
t2.Show();
}
Como resultado, el compilador generó dos copias de la misma función miembro para este programa:< / p>
void f1();
void f2();
int main()
{
f1 ( );
f2();
return 0;
}
Cuando el programa está vinculado, el vinculador se encontrará con dos. copias idénticas de Time::Show(), se produce un error de conexión con la redefinición de la función. Pero las implementaciones más antiguas de C abordan esta situación tratando una función no integrada como estática.
Por lo tanto, cada copia de función solo es visible en su propia unidad de compilación, por lo que el error de enlace se resuelve, pero quedarán múltiples copias de función en el programa. En este caso, en lugar de mejorar el rendimiento del programa, aumenta el tiempo de compilación y enlace y el tamaño del cuerpo ejecutable final.
Pero afortunadamente, la declaración del nuevo estándar C sobre funciones no integradas ha cambiado. Una implementación de C conforme DEBE hacer solo una copia de la función. Sin embargo, puede pasar mucho tiempo antes de que todos los compiladores admitan esto.
Además, hay dos problemas más relacionados con las funciones en línea. La primera pregunta es cómo mantenerlo. Una función puede aparecer en línea al principio, pero a medida que el sistema se expande, el cuerpo de la función puede requerir la adición de funciones adicionales. Como resultado, las funciones en línea se vuelven imposibles, por lo que es necesario eliminar el especificador en línea y el cuerpo de la función. colocado en un archivo fuente separado. Otro problema surge cuando se utilizan funciones en línea en el código base. Cuando las funciones en línea cambian, los usuarios deben volver a compilar su código para reflejar el cambio. Sin embargo, para una función no en línea, el usuario sólo necesita volver a vincularla.
Lo que quiero decir aquí es que las funciones en línea no son una panacea para mejorar el rendimiento. Solo cuando la función es muy corta puede obtener el efecto que queremos, pero si la función no es muy corta y se llama en muchos lugares, aumentará el tamaño del cuerpo ejecutable. Lo más molesto es cuando el compilador se niega a integrarlo. En la implementación anterior, los resultados fueron muy insatisfactorios. Aunque hay grandes mejoras en la nueva implementación, todavía no es perfecta. Algunos compiladores son lo suficientemente inteligentes como para determinar qué funciones se pueden incluir en línea y cuáles no, pero la mayoría de los compiladores no son tan inteligentes, por lo que todo se reduce a la experiencia. Si una función en línea no mejora el rendimiento, evite usarla.