一、编译器选项:让机器为你打工

说到性能优化,很多人第一反应就是改代码,但其实编译器才是你的第一个帮手。GCC和Clang都提供了丰富的优化选项,比如-O2-O3这种常见的优化级别。但你知道吗?-O3虽然激进,有时候反而会让程序变慢,尤其是在涉及大量分支预测的代码中。

举个例子,我们来看一个简单的循环求和(技术栈:C++17,编译器:GCC 11):

// 原始版本:未优化
int sum = 0;
for (int i = 0; i < 1000000; ++i) {
    sum += i;
}

// 使用-O2优化后,编译器可能会自动展开循环
// 甚至直接计算闭合公式:sum = n*(n-1)/2

注意事项

  • -O2适合大多数场景,平衡了性能和编译时间。
  • -O3可能增加二进制大小,适合计算密集型任务。
  • -Os优化代码大小,适合嵌入式设备。

二、内存访问优化:别让CPU等你

现代CPU的速度远超内存,所以减少缓存未命中(Cache Miss)是关键。比如,二维数组的行优先访问比列优先快得多,因为内存是连续读取的。

来看一个矩阵乘法的例子(技术栈:C++17):

// 低效版本:列优先访问
void multiply_matrices(const int a[][100], const int b[][100], int result[][100]) {
    for (int i = 0; i < 100; ++i) {
        for (int k = 0; k < 100; ++k) {
            for (int j = 0; j < 100; ++j) {
                result[i][j] += a[i][k] * b[k][j];  // b[k][j]是列访问,缓存不友好!
            }
        }
    }
}

// 高效版本:行优先访问
void multiply_matrices_optimized(const int a[][100], const int b[][100], int result[][100]) {
    for (int i = 0; i < 100; ++i) {
        for (int j = 0; j < 100; ++j) {
            for (int k = 0; k < 100; ++k) {
                result[i][j] += a[i][k] * b[k][j];  // 现在b[k][j]会被缓存命中
            }
        }
    }
}

技术优缺点

  • 优点:行优先访问可提升数倍性能。
  • 缺点:代码可能需要重构,尤其是涉及第三方库时。

三、算法与数据结构:选择比努力更重要

有时候,换一个数据结构或算法,性能就能提升几个数量级。比如,在频繁查找的场景下,std::unordered_map(哈希表)比std::map(红黑树)快得多,但代价是内存占用更高。

示例:统计单词频率(技术栈:C++17):

#include <unordered_map>
#include <string>
#include <vector>

// 使用unordered_map:O(1)平均时间复杂度
void count_words_fast(const std::vector<std::string>& words) {
    std::unordered_map<std::string, int> word_count;
    for (const auto& word : words) {
        word_count[word]++;  // 哈希表查找,极快
    }
}

// 使用map:O(log n)时间复杂度
#include <map>
void count_words_slow(const std::vector<std::string>& words) {
    std::map<std::string, int> word_count;
    for (const auto& word : words) {
        word_count[word]++;  // 红黑树查找,较慢
    }
}

应用场景

  • unordered_map适合需要快速查找且不要求顺序的场景。
  • map适合需要有序遍历的场景。

四、代码重构:魔鬼在细节中

有时候,简单的代码调整就能带来显著的性能提升。比如,避免不必要的拷贝、使用移动语义、减少虚函数调用等。

来看一个字符串处理的例子(技术栈:C++17):

#include <string>
#include <vector>

// 低效版本:频繁拷贝字符串
std::vector<std::string> filter_strings(const std::vector<std::string>& input) {
    std::vector<std::string> result;
    for (const auto& s : input) {
        if (s.size() > 5) {
            result.push_back(s);  // 拷贝发生!
        }
    }
    return result;
}

// 高效版本:使用移动语义
std::vector<std::string> filter_strings_optimized(std::vector<std::string>& input) {
    std::vector<std::string> result;
    for (auto& s : input) {
        if (s.size() > 5) {
            result.push_back(std::move(s));  // 移动而非拷贝
        }
    }
    return result;
}

总结

  1. 编译器选项是免费的午餐,先用好-O2
  2. 内存访问模式影响巨大,尽量让数据连续。
  3. 算法和数据结构的选择决定性能上限。
  4. 代码重构的细节往往能带来意外收获。