Comparta 9 consejos para mejorar la eficiencia del código

Un artículo que leo después de estudiarlo, reimprimirlo, leerlo a menudo, ¡hay tantos beneficios!

El propósito de nuestro programa es hacer que funcione de manera estable bajo cualquier circunstancia. Un programa que se ejecuta rápido pero resulta ser incorrecto no sirve de nada. En el proceso de desarrollo y optimización del programa, debemos considerar la forma en que se usa el código y los factores clave que lo afectan. Por lo general, tenemos que hacer un compromiso entre la simplicidad del programa y su velocidad de ejecución. Hoy hablaremos sobre cómo optimizar el rendimiento del programa.

1. Reducir la cantidad de cálculos del programa

1.1 Código de muestra

for (i = 0; i < n; i++) {
  int ni = n*i;
  for (j = 0; j < n; j++)
    a[ni + j] = b[j];
}

1.2 Código de análisis

  El código se muestra arriba, cada vez que se ejecuta el ciclo externo, necesitamos realizar un cálculo de multiplicación. i = 0, ni = 0; i = 1, ni = n; i = 2, ni = 2n. Por lo tanto, podemos reemplazar la multiplicación con la suma, usando n como tamaño de paso, lo que reduce la cantidad de código en el ciclo externo.

1.3 Mejora el código

int ni = 0;
for (i = 0; i < n; i++) {
  for (j = 0; j < n; j++)
    a[ni + j] = b[j];
  ni += n;         //乘法改加法
}

Las instrucciones de multiplicación en una computadora son mucho más lentas que las instrucciones de suma.

2. Extrae las partes comunes del código.

2.1 Código de muestra

  Imagina que tenemos una imagen, la representamos como una matriz bidimensional y los elementos de la matriz representan píxeles. Queremos obtener la suma de los cuatro vecinos este, sur, oeste y norte para un píxel dado. Y encuentra su promedio o su suma. El código se muestra a continuación.

up =    val[(i-1)*n + j  ];
down =  val[(i+1)*n + j  ];
left =  val[i*n     + j-1];
right = val[i*n     + j+1];
sum = up + down + left + right;

2.2 Código de análisis

  Después de compilar el código anterior, el código ensamblador es como se muestra a continuación. Tenga en cuenta que en las líneas 3, 4 y 5, hay tres operaciones de multiplicación que se multiplican por n. Después de expandir lo anterior hacia arriba y hacia abajo, encontraremos que i * n + j está presente en la expresión de cuatro celdas. Por lo tanto, se puede extraer la parte común y los valores de arriba, abajo, etc. se pueden obtener mediante operaciones de suma y resta.

leaq   1(%rsi), %rax  # i+1
leaq   -1(%rsi), %r8  # i-1
imulq  %rcx, %rsi     # i*n
imulq  %rcx, %rax     # (i+1)*n
imulq  %rcx, %r8      # (i-1)*n
addq   %rdx, %rsi     # i*n+j
addq   %rdx, %rax     # (i+1)*n+j
addq   %rdx, %r8      # (i-1)*n+j

2.3 Mejorar el código

long inj = i*n + j;
up =    val[inj - n];
down =  val[inj + n];
left =  val[inj - 1];
right = val[inj + 1];
sum = up + down + left + right;

  La compilación del código mejorado se muestra a continuación. Solo hay una multiplicación después de la compilación. Reducido en 6 ciclos de reloj (un ciclo de multiplicación equivale aproximadamente a 3 ciclos de reloj).

imulq %rcx, %rsi  # i*n
addq %rdx, %rsi  # i*n+j
movq %rsi, %rax  # i*n+j
subq %rcx, %rax  # i*n+j-n
leaq (%rsi,%rcx), %rcx # i*n+j+n
...

  Para el compilador GCC, el compilador puede tener diferentes métodos de optimización de acuerdo con diferentes niveles de optimización y completará automáticamente las operaciones de optimización anteriores. A continuación presentamos, esos deben optimizarse manualmente.

3. Elimina el código ineficiente en el ciclo

3.1 Código de muestra

  El programa no parece ser un problema, un código de conversión de casos muy común, pero ¿por qué el tiempo de ejecución del código aumenta exponencialmente a medida que la longitud de la cadena de entrada se hace más larga?

void lower1(char *s)
{
  size_t i;
  for (i = 0; i < strlen(s); i++)
    if (s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a');
}

3.2 Código de análisis

  Luego probamos el código e ingresamos una serie de cadenas.

imagen

Prueba de rendimiento de código Lower1

  Cuando la longitud de la cadena de entrada es inferior a 100000, el tiempo de ejecución del programa tiene poca diferencia. Sin embargo, a medida que aumenta la longitud de la cadena, el tiempo de ejecución del programa aumenta exponencialmente.

  Echemos un vistazo al código convertido al formulario goto.

void lower1(char *s)
{
   size_t i = 0;
   if (i >= strlen(s))
     goto done;
 loop:
   if (s[i] >= 'A' && s[i] <= 'Z')
       s[i] -= ('A' - 'a');
   i++;
   if (i < strlen(s))
     goto loop;
 done:
}

  El código anterior se divide en tres partes: inicialización (línea 3), prueba (línea 4) y actualización (línea 9, 10). La inicialización solo se realizará una vez. Pero la prueba y la actualización se ejecutarán siempre. Strlen se llama una vez cada vez que se realiza el ciclo.

  Echemos un vistazo a cómo el código fuente de la función strlen calcula la longitud de una cadena.

size_t strlen(const char *s)
{
    size_t length = 0;
    while (*s != '\0') {
 s++; 
 length++;
    }
    return length;
}

  El principio de la función strlen para calcular la longitud de una cadena es: atravesar la cadena y detenerse hasta que encuentre '\ 0'. Por tanto, la complejidad temporal de la función strlen es O (N). En lower1, para una cadena de longitud N, el número de llamadas a strlen es N, N-1, N-2 ... 1. Para una función de tiempo lineal llamada N veces, la complejidad del tiempo es cercana a O (N2).

3.3 Mejorar el código

  Para este tipo de llamada redundante que aparece en el bucle, podemos moverla fuera del bucle. Utilice el resultado del cálculo en el ciclo. El código mejorado se muestra a continuación.

void lower2(char *s)
{
  size_t i;
  size_t len = strlen(s);
  for (i = 0; i < len; i++)
    if (s[i] >= 'A' && s[i] <= 'Z')
      s[i] -= ('A' - 'a');
}

  Compare las dos funciones, como se muestra en la siguiente figura. El tiempo de ejecución de la función lower2 se ha mejorado significativamente.

imagen

Eficiencia de código Lower1 y lower2

4. Elimina referencias de memoria innecesarias

4.1 Código de muestra

  El siguiente código se utiliza para calcular la suma de todos los elementos en cada fila de la matriz a y almacenarla en b [i].

void sum_rows1(double *a, double *b, long n) {
    long i, j;
    for (i = 0; i < n; i++) {
 b[i] = 0;
 for (j = 0; j < n; j++)
     b[i] += a[i*n + j];
    }
}

4.2 Código de análisis

  El código de ensamblaje se muestra a continuación.

# sum_rows1 inner loop
.L4:
        movsd   (%rsi,%rax,8), %xmm0 # 从内存中读取某个值放到%xmm0
        addsd   (%rdi), %xmm0      # %xmm0 加上某个值
        movsd   %xmm0, (%rsi,%rax,8) # %xmm0 的值写回内存,其实就是b[i]
        addq    $8, %rdi
        cmpq    %rcx, %rdi
        jne     .L4

  Esto significa que cada bucle necesita leer b [i] de la memoria y luego escribir b [i] de nuevo en la memoria. b [i] + = b [i] + a [i * n + j]; De hecho, al comienzo de cada ciclo, b [i] es el último valor. ¿Por qué tienes que leerlo de la memoria y volver a escribirlo cada vez?

4.3 Mejorar el código

/* Sum rows is of n X n matrix a
   and store in vector b  */
void sum_rows2(double *a, double *b, long n) {
    long i, j;
    for (i = 0; i < n; i++) {
 double val = 0;
 for (j = 0; j < n; j++)
     val += a[i*n + j];
         b[i] = val;
    }
}

  El montaje se muestra a continuación.

# sum_rows2 inner loop
.L10:
        addsd   (%rdi), %xmm0 # FP load + add
        addq    $8, %rdi
        cmpq    %rax, %rdi
        jne     .L10

  El código mejorado introduce variables temporales para almacenar resultados intermedios y solo almacena los resultados en una matriz o variable global cuando se calcula el valor final.

5. Reducir las llamadas innecesarias

5.1 Código de muestra

  Por ejemplo, definimos una estructura que contiene la matriz y la longitud de la matriz, principalmente para evitar que el acceso a la matriz esté fuera de los límites, data_t puede ser int, long y otros tipos. Los detalles son los siguientes.

typedef struct{
 size_t len;
 data_t *data;  
} vec;

imagen

diagrama vectorial vec

  La función de get_vec_element es atravesar los elementos de la matriz de datos y almacenarlos en val.

int get_vec_element (*vec v, size_t idx, data_t *val)
{
 if (idx >= v->len)
  return 0;
 *val = v->data[idx];
 return 1;
}

  Usaremos el siguiente código como ejemplo para empezar a optimizar el programa paso a paso.

void combine1(vec_ptr v, data_t *dest)
{
    long int i;
    *dest = NULL;
    for (i = 0; i < vec_length(v); i++) {
 data_t val;
 get_vec_element(v, i, &val);
 *dest = *dest * val;
    }
}

5.2 Código de análisis

  La función de la función get_vec_element es obtener el siguiente elemento En la función get_vec_element, cada ciclo debe compararse con v-> len para evitar cruzar el límite. Es un buen hábito realizar comprobaciones de límites, pero hacerlo siempre resultará en una reducción de la eficiencia.

5.3 Mejorar el código

  Podemos mover el código para calcular la longitud del vector fuera del bucle y agregar una función get_vec_start al tipo de datos abstracto. Esta función devuelve la dirección inicial de la matriz. De esta manera, no hay una llamada a la función en el cuerpo del bucle, sino un acceso directo a la matriz.

data_t *get_vec_start(vec_ptr v)
{
 return v-data;
}

void combine2 (vec_ptr v, data_t *dest)
{
 long i;
 long length  = vec_length(v);
    data_t *data = get_vec_start(v);
 *dest = NULL;
 for (i=0;i < length;i++)
 {
  *dest = *dest * data[i];
 }
}

6. Desenrollado del bucle

6.1 Código de muestra

  Realizamos mejoras en el código de combine2.

6.2 Código de análisis

  Desenrollar aumentando cada iteración del número de elementos , reduciendo las iteraciones de bucle .

6.3 Mejorar el código

void combine3(vec_ptr v, data_t *dest)
{
    long i;
    long length = vec_length(v);
    long limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc = NULL;
    
    /* 一次循环处理两个元素 */
    for (i = 0; i < limit; i+=2) {
    acc = (acc * data[i]) * data[i+1];
    }
    /*     完成剩余数组元素的计算    */
    for (; i < length; i++) {
  acc = acc * data[i];
    }
    *dest = acc;
}

  En el código mejorado, el primer ciclo procesa dos elementos de la matriz a la vez. Es decir, para cada iteración, el índice de bucle i se incrementa en 2, y en una iteración, la operación de fusión se usa en los elementos de la matriz i e i + 1. Generalmente, a esto lo llamamos desenrollado de bucle 2 × 1, y esta transformación puede reducir el impacto de la sobrecarga del bucle.

Preste atención a no cruzar el límite de acceso, establezca el límite correctamente, n elementos, generalmente establezca el límite n-1

7. Acumular variables, multicanal en paralelo

7.1 Código de muestra

  Realizamos mejoras en el código de combine3.

7.2 Código de análisis

  Para una operación de combinación combinable y conmutativa, como la suma o la multiplicación de enteros, podemos mejorar el rendimiento dividiendo un conjunto de operaciones de combinación en dos o más partes y combinando los resultados al final.

Atención especial: no combine fácilmente números de punto flotante. El formato de codificación de los números de punto flotante es diferente de otros números enteros.

7.3 Mejorar el código

void combine4(vec_ptr v, data_t *dest)
{
 long i;
    long length = vec_length(v);
    long limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc0 = 0;
    data_t acc1 = 0;
    
    /* 循环展开,并维护两个累计变量 */
    for (i = 0; i < limit; i+=2) {
    acc0 = acc0 * data[i];
    acc1 = acc1 * data[i+1];
    }
    /*     完成剩余数组元素的计算    */
    for (; i < length; i++) {
        acc0 = acc0 * data[i];
    }
    *dest = acc0 * acc1;
}

  El código anterior usa dos expansiones de bucle para fusionar más elementos en cada iteración. También usa dos rutas paralelas para acumular los elementos con un índice par en la variable acc0, y los elementos con un índice impar se acumulan en la variable. Acc1. Por lo tanto, lo llamamos "desenrollado de bucle 2 × 2". Utilice desenrollado de bucle 2 × 2. Al mantener múltiples variables acumulativas, este método aprovecha las múltiples unidades funcionales y sus capacidades de canalización.

8. Recombinación y transformación

8.1 Código de muestra

  Realizamos mejoras en el código de combine3.

8.2 Código de análisis

  En este punto, el rendimiento del código está básicamente cerca del límite, incluso si realiza más desenrollamientos de bucles, la mejora del rendimiento no es obvia. Necesitamos cambiar nuestra forma de pensar, prestar atención al código en la línea 12 en el código combine3, podemos cambiar el orden de fusión de los elementos del siguiente vector (los números de punto flotante no son aplicables). La ruta clave del código combine3 antes de la recombinación se muestra en la siguiente figura.

imagen

La ruta crítica del código combine3

8.3 Mejorar el código

void combine7(vec_ptr v, data_t *dest)
{
 long i;
    long length = vec_length(v);
    long limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc = IDENT;
    
    /* Combine 2 elements at a time */
    for (i = 0; i < limit; i+=2) {
   acc = acc OP (data[i] OP data[i+1]);
    }
    /* Finish any remaining elements */
    for (; i < length; i++) {
        acc = acc OP data[i];
    }
    *dest = acc;
}

  La recombinación de transformaciones puede reducir el número de operaciones en la ruta crítica en el cálculo.Este método aumenta el número de operaciones que se pueden ejecutar en paralelo y hace un mejor uso de las capacidades de canalización de las unidades funcionales para obtener un mejor rendimiento. La ruta crítica después de la recombinación es la siguiente.

imagen

Ruta crítica después de la recombinación combine3

9 Código de estilo de transferencia condicional

9.1 Código de muestra

void minmax1(long a[],long b[],long n){
 long i;
 for(i = 0;i,n;i++){
        if(a[i]>b[i]){
            long t = a[i];
            a[i] = b[i];
            b[i] = t;
        }
   }
}

9.2 Código de análisis

  El rendimiento de la canalización de los procesadores modernos hace que el trabajo del procesador esté muy por delante de las instrucciones que se están ejecutando actualmente. La predicción de bifurcación en el procesador predice dónde saltar a continuación cuando se encuentra una instrucción de comparación. Si la predicción es incorrecta, es necesario volver al lugar donde saltó la rama. Los errores de predicción de rama afectarán seriamente la eficiencia de ejecución del programa. Por lo tanto, debemos escribir código que permita al procesador mejorar la precisión de la predicción, es decir, usar instrucciones de transferencia condicional. Usamos operaciones condicionales para calcular valores y luego usamos estos valores para actualizar el estado del programa, como se muestra en el código mejorado.

9.3 Mejorar el código

void minmax2(long a[],long b[],long n){
 long i;
 for(i = 0;i,n;i++){
 long min = a[i] < b[i] ? a[i]:b[i];
 long max = a[i] < b[i] ? b[i]:a[i];
 a[i] = min;
 b[i] = max;
 }
}

  En la cuarta línea del código original, es necesario comparar a [i] y b [i], y luego se realiza el siguiente paso. La consecuencia es que se debe hacer una predicción cada vez. El código mejorado implementa esta función para calcular los valores máximo y mínimo de cada posición i, y luego asignar estos valores a a [i] y b [i], en lugar de la predicción de rama.

10. Resumen

  Introdujimos varias técnicas para mejorar la eficiencia del código, algunas de las cuales pueden ser optimizadas automáticamente por el compilador y otras deben ser implementadas por nosotros mismos. Se resume como sigue.

  1. Elimina llamadas de función consecutivas. Cuando sea posible, mueva los cálculos fuera del ciclo. Considere comprometer selectivamente la modularidad del programa para una mayor eficiencia.

  2. Elimina referencias de memoria innecesarias. Introduce variables temporales para guardar resultados intermedios. Solo cuando se calcula el valor final, el resultado se almacena en una matriz o variable global.

  3. Desenrolle el bucle, reduzca los gastos generales y haga posible una mayor optimización.

  4. Mediante el uso de técnicas como múltiples variables de acumulación y recombinación, encuentre formas de mejorar el paralelismo a nivel de instrucción.

  5. Vuelva a escribir la operación condicional en un estilo funcional para que el compilador utilice la transferencia de datos condicional.

Supongo que te gusta

Origin blog.csdn.net/mainmaster/article/details/113695241
Recomendado
Clasificación