Bootstrap

C语言编译时有哪些优化项及参考示例

在编译C语言代码时,编译器通常会执行许多优化,以提高生成代码的效率。以下是一些常见的C语言编译时优化选项:

  • 优化等级(Optimization Level): 编译器通常会提供不同的优化等级,从-O0(无优化)到-O3(最大优化等级)。-O2也是一个常用的优化等级,它比-O3少一些激进优化。*
编译命令:gcc -O3 -o output_optimized source.c
  • 常量折叠和传播:编译器可以在编译时计算常量表达式的值,并在代码中传播这些值。例如,如果代码中有 5 + 3 这样的表达式,编译器可以将其替换为 8,从而减少代码的执行时间和存储空间。
int a = 5 + 3; // 常量折叠前的代码
int b = 8; // 常量折叠后的代码
  • 循环展开:循环展开是一种优化技术,它通过减少循环次数和分支判断来提高代码的执行效率。编译器可以通过分析循环的条件表达式,确定循环展开的次数和每次迭代的操作,然后将这些操作合并到一起。这样可以减少循环的开销,提高代码的执行效率。
for (int i = 0; i < N; i++) {
    // Some code here...
}
优化后代码:
for (int i = 0; i < N / 2; i++) {
    // Some code here...
    // Some code here...
}
  • 内联函数:内联函数是一种优化技术,它通过将函数调用直接替换为函数体,从而减少函数调用时的开销。内联函数的实现方式是通过将函数调用处替换为函数体的拷贝,这样可以减少函数调用的开销,提高代码的执行效率。但是需要注意的是,内联函数的引入可能会增加代码的大小,从而影响代码的执行效率。
#include <stdio.h>

static inline int square(int x) {
    return x * x;
}

int main() {
    int x = 5;
    printf("Square of %d is %d\n", x, square(x)); // 内联函数调用,直接展开函数体
    return 0;
}
  • 删除无用代码:编译器可以删除永远不会被执行到的代码。这种优化技术称为“无用代码删除”。例如,如果代码中有 if (0) 这样的语句块,编译器可以将其删除,因为该语句块永远不会被执行到。
int main() {
    int a = 5 + 3; // 无用代码删除前的代码
    return 0; // 无用代码删除后的代码
}
  • 循环合并和交换:循环合并和交换是一种优化技术,它通过将两个循环合并为一个循环或者交换两个循环的顺序来提高代码的执行效率。例如,如果有两个相邻的循环,它们分别对两个不同的数组进行遍历,编译器可以通过分析这两个循环的关系,将它们合并为一个循环,从而减少循环的次数和比较操作。
for (int i = 0; i < 5; i++) { // 循环合并前的代码
    sum1 += i;
}
for (int i = 5; i < 10; i++) { // 循环合并前的代码
    sum2 += i;
}
for (int i = 0; i < 10; i++) { // 循环合并后的代码
    sum1 += i; if (i >= 5) sum2 += i; else sum2 += i - 5; // 循环交换后的代码,将两个循环合并为一个循环,并交换了两个循环的顺序
}
  • *减少函数调用:通过一些手段减少函数调用的次数也是一种常见的优化技术。例如,可以通过将函数参数传递改为全局变量或者静态变量来减少函数调用的次数。
int add(int a, int b) { // 函数定义
    return a + b;
}
int main() {
    int sum = add(5, 3); // 函数调用前的代码
    int sum = 5 + 3; // 减少函数调用后的代码
    return 0;
}

无用变量删除:编译器可以删除永远不会被使用的变量。这种优化技术称为“无用变量删除”。如果代码中存在一些定义但永远不会被使用的变量,编译器可以将其删除以减少存储空间的使用和代码的复杂性。*

int main() {
    int a = 5 + 3; // 无用变量删除前的代码
    return 0; // 无用变量删除后的代码
}
  • 重新排序变量:为了方便处理,编译器会重新排序变量。这种优化技术称为“变量重排”。例如,如果有两个变量 a 和 b,它们在代码中同时被访问,编译器可以将它们交换位置,以便更好地利用缓存和提高内存访问效率。
int a = 5; // 变量重新排序前的代码,变量a先定义
int b = 3; // 变量重新排序前的代码,变量b后定义
int tmp = a + b; // 变量重新排序后的代码,将变量b的赋值提前,以便更好地利用缓存和提高内存访问效率
死代码删除:
  • 死代码删除:编译器可以删除永远不会被执行到的代码,这种优化技术称为“死代码删除”。例如,如果代码中有 if (0) 这样的语句块或者一个永远不会被赋值的变量,编译器可以将其删除。
int main() {
    int a = 5 + 3; // 死代码删除前的代码
    return 0; // 死代码删除后的代码,变量a永远不会被使用到,因此可以删除该变量的定义和赋值操作
}
  • 常量传播:编译器可以在代码中传播常量值。例如,如果代码中有 if (x == 5) 这样的条件判断,并且该条件在整个程序中都成立,编译器可以将 x 的值替换为 5,从而减少代码的大小和提高执行效率。
int main() {
    int a = 5; // 常量传播前的代码,变量a先定义并赋值5
    if (a == 5) { // 常量传播前的代码,判断变量a的值是否等于5
        printf("a is 5\n"); // 常量传播前的代码,如果变量a的值等于5,则输出该字符串
    }
    int b = a; // 常量传播后的代码,将变量a的值赋给变量b,此时变量b的值也是5
    if (b == 5) { // 常量传播后的代码,判断变量b的值是否等于5,由于变量b的值已经是5,因此这个判断是多余的死代码,可以删除
        printf("b is 5\n"); // 常量传播后的代码,如果变量b的值等于5,则输出该字符串,但实际上这个输出语句也是多余的死代码,可以删除
    }
    return 0; // 常量传播后的代码,最终的输出结果为"a is 5""b is 5"这两个字符串,但由于这两个字符串都是多余的死代码,因此最终的输出结果为空字符串。
}
  • 公共子表达式消除:编译器可以消除公共子表达式。例如,如果有两个表达式 a = b * c; 和 d = b * c; 这样的语句块,编译器可以将其合并为 a = b * c; d = b * c; 或者直接消除重复的计算。
int a = 5 * 3; // 公共子表达式消除前的代码,先计算出5 * 3的结果为15,并将结果赋值给变量a
int b = 5 * 3; // 公共子表达式消除前的代码,再次计算出5 * 3的结果为15,并将结果赋值给变量b
int c = a + b; // 公共子表达式消除后的代码,将变量a和变量b的值相加,得到最终结果为30
  • 自动并行化:编译器可以通过自动并行化来提高代码的执行效率。例如,对于一些循环操作,编译器可以自动识别并行的机会,然后将这些并行化的操作分配到不同的处理单元上执行。这样可以利用多核处理器的优势来提高代码的执行效率。
#include <stdio.h>
#include <stdlib.h>
#include <omp.h>

int main() {
    int i, j;
    int n = 100;
    double *a = (double *)malloc(n * sizeof(double));
    double *b = (double *)malloc(n * sizeof(double));
    #pragma omp parallel for private(i) shared(a, b)
    for (i = 0; i < n; i++) {
        a[i] = i + 1;
    }
    #pragma omp parallel for private(i) shared(a, b)
    for (i = 0; i < n; i++) {
        b[i] = a[i] * 2;
    }
    free(a);
    free(b);
    return 0;
}
;