40、C语言代码优化与性能调优

厨子大约 5 分钟C语言基础程序程序厨

代码优化的基本原则

代码优化是指在保证程序正确性的前提下,通过改进代码的结构、算法或数据表示,提高程序的执行效率、减少资源消耗或改善用户体验的过程。

下面整理了一些C语言代码优化的基本原则:

  • 明确优化目标:在开始优化之前,首先要明确优化的目标,比如提高运行速度、减少内存占用或降低功耗等。
  • 避免过早优化:过早优化可能会引入额外的复杂性和错误,而且有时优化后的代码在后续的开发中可能会变得不再有效。因此,应该在程序的关键部分或瓶颈处进行优化。
  • 保持代码可读性:优化后的代码应该仍然保持清晰、可读和易于维护。如果优化导致代码变得难以理解,那么这种优化可能是不值得的。
  • 利用编译器优化:现代编译器具有强大的优化能力,能够自动执行许多常见的优化操作。因此,在编写代码时应该尽量利用编译器的优化功能。
  • 2-8原则:80%的性能消耗会集中在那20%的代码上,性能优化的原则就是找到那20%的代码,分析&优化。

编译器优化选项与技巧

我们可以直接利用编译器的优化选项,并掌握一些优化技巧。

  • CLANG编译器

    • -O0:不进行任何优化,这是默认选项。
    • -O1:执行基本的优化操作,包括循环展开、函数内联等。
    • -O2:在-O1的基础上执行更多的优化操作,包括更复杂的循环优化和全局优化等。
    • -O3:在-O2的基础上执行更多的高级优化操作,但可能会增加编译时间和生成的代码大小。
    • -Os:优化代码大小,减少生成的二进制文件的大小。
    • -Ofast:类似于-O3,但允许一些可能违反IEEE标准的浮点运算优化。
    • -funroll-loops:手动开启循环展开优化。
    • -finline-functions:手动开启函数内联优化。
  • 优化技巧

    • 避免不必要的函数调用:函数调用的开销包括参数传递、栈操作、返回地址保存等。如果某个函数被频繁调用且其实现比较简单,可以考虑将其内联到调用点处
    • 减少内存分配和释放:频繁的内存分配和释放会导致内存碎片和性能下降。如果可能的话,尽量使用静态或栈内存来存储数据。
    • 使用合适的数据类型:选择合适的数据类型可以显著影响程序的性能。例如,对于计数变量通常使用unsigned int而不是int,因为unsigned int可以表示更大的正数范围且没有符号位的影响。
    • 避免不必要的锁:在多线程编程中,锁的开销是很大的。如果可能的话,尽量使用无锁数据结构或算法来避免锁的使用,或者减小临界区。

性能分析工具与使用方法

性能分析工具可以帮助我们分析和优化程序性能。它们可以测量程序的运行时间、内存使用情况、CPU利用率等关键指标,还能提供详细的性能报告和可视化图表来帮助我们定位性能瓶颈。

  • gprof:可以生成调用图(call graph)和每个函数的执行时间等信息。
  • perf:可以监控和分析系统的性能问题,包括CPU使用情况、内存分配、磁盘I/O等。
  • Valgrind:一个用于内存调试、内存泄漏检测和性能分析的工具集。其中的callgrind工具可以生成详细的函数调用图和执行时间信息。
  • Intel VTune Profiler:Intel提供的高级性能分析工具,支持多种编程语言和平台,可以提供详细的性能分析和优化建议。

实战中的性能调优案例

以下是一个简单的C语言程序性能调优案例:

原始代码

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void compute(int* array, int size) {
    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            array[i] += array[j];
        }
    }
}

int main() {
    int size = 1000;
    int* array = (int*)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++) {
        array[i] = i;
    }

    clock_t start = clock();
    compute(array, size);
    clock_t end = clock();

    printf("Time taken: %lf seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    free(array);
    return 0;
}

性能分析

使用gprofperf等工具对原始代码进行分析,可以发现compute函数中的双重循环是性能瓶颈。

优化后的代码

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void compute_optimized(int* array, int size) {
    int sum = 0;
    for (int j = 0; j < size; j++) {
        sum += array[j];
    }
    for (int i = 0; i < size; i++) {
        array[i] = sum;
    }
}

int main() {
    int size = 1000;
    int* array = (int*)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++) {
        array[i] = i;
    }

    clock_t start = clock();
    compute_optimized(array, size);
    clock_t end = clock();

    printf("Time taken: %lf seconds\n", (double)(end - start) / CLOCKS_PER_SEC);

    free(array);
    return 0;
}

优化说明

  • 将原始代码中的双重循环优化为两个单独的循环,避免了不必要的重复计算。
  • 优化后的代码运行时间显著减少,因为每个元素只被访问和计算了一次。

扩展

  • 算法优化:选择合适的算法和数据结构可以显著提高程序的性能。例如,对于排序操作可以选择快速排序、归并排序等高效的排序算法;对于查找操作可以选择哈希表、二分查找等高效的查找算法。
  • 并行化:利用多核处理器的并行计算能力可以进一步提高程序的性能。可以使用POSIX线程(pthread)、OpenMP等库来实现并行化。
  • 硬件加速:利用GPU、FPGA等硬件加速器可以加速某些计算密集型任务。例如,可以使用CUDA或OpenCL等框架来编写在GPU上运行的并行程序。
  • 代码重构:定期重构代码以保持其清晰、可读和易于维护的状态。重构不仅可以提高代码质量,还可以为未来的优化提供更好的基础条件。