首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >我在优化代码时学到的那些事

我在优化代码时学到的那些事

原创
作者头像
七条猫
发布2025-07-18 22:02:33
发布2025-07-18 22:02:33
13600
代码可运行
举报
运行总次数:0
代码可运行

大学时我总以为写代码就是告诉电脑一步步该怎么做。等到真正开始做底层开发,才意识到现代编译器简直就是个有魔法的黑盒子。

调试一个性能问题时,我发现编译器把我写的20多行代码优化成了只有5条汇编指令!好家伙,它偷偷替我做了这么多工作:

常量折叠:让编译器替你算数学

还记得我第一次看反汇编结果时的震惊 - 我代码里的width * 800 + height * 600直接变成了一个硬编码的数字。编译器在编译期间自己算好了!

实际工作中,我经常会看到新人写这样的代码:

代码语言:c++
复制
float calculateArea() {
    return 3.14159265358979 * radius * radius;
}

每次调用都要算一遍圆周率?编译器在默默帮你优化,但如果你写成这样会更好:

代码语言:c++
复制
const float PI = 3.14159265358979;
float calculateArea() {
    return PI * radius * radius;
}

公共子表达式消除:不做重复的事

有次我的同事写了这样的代码处理图像变换:

代码语言:c++
复制
for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
        result[i][j] = source[i * width + j] * (width * height / 255);
        alpha[i][j] = source[i * width + j] * (width * height / 127);
    }
}

开启O2优化后,编译器立刻发现width * heighti * width + j在循环中被重复计算,主动提取出来只算一次。但这种优化并不总是可靠,特别是当表达式变得复杂或者跨越多个函数时。

我现在习惯手动提取复杂表达式:

代码语言:c++
复制
for (int i = 0; i < height; i++) {
    int rowOffset = i * width;
    float sizeNorm = width * height; // 只计算一次
    
    for (int j = 0; j < width; j++) {
        int pixelIndex = rowOffset + j;
        float sourceVal = source[pixelIndex];
        
        result[i][j] = sourceVal * (sizeNorm / 255);
        alpha[i][j] = sourceVal * (sizeNorm / 127);
    }
}

中间表示:编译器的神秘语言

记得有次我在调试LLVM优化问题,不得不深入研究它的中间表示(IR)。当时我对同事说:"这就像研究外星人的语言一样"。

不同的编译器有不同的中间表示:

抽象语法树(AST):编译器的草图

我曾经为了实现一个自定义语言,手写了一个AST解析器。简单来说,AST就像是把你的代码按语法结构画成一棵树。

比如这段简单的代码:

代码语言:c++
复制
if (x > 5) {
  return x * 2;
} else {
  return x + 1;
}

在编译器内部大概变成这样的树结构:

代码语言:c++
复制
       IfStatement
       /     |     \
  Condition  Then   Else
    (x>5)    |       |
           Return   Return
              |       |
         Multiply    Add
           /  \      / \
          x    2    x   1

你写的每一个表达式、每一个语句,都会变成这样的树节点。编译器后续的所有优化都是在这棵"树"上进行手术。

静态单赋值(SSA):变量的"一夫一妻制"

第一次接触SSA形式时,我的脑袋都大了。它的核心理念是:每个变量只被赋值一次

这什么意思呢?正常人写的代码是这样的:

代码语言:c++
复制
int x = 1;
x = x + 2;
x = x * 3;

转换成SSA形式后变成这样:

代码语言:c
代码运行次数:0
运行
复制
x1 = 1
x2 = x1 + 2
x3 = x2 * 3

看起来很蠢对吧?但这对编译器来说简直是天赐福音 - 因为它可以很容易地追踪每个变量的来源和去向,进行更激进的优化。

指令调度:CPU流水线的交通指挥官

这部分是我最痴迷的领域,因为它直接和硬件打交道。现代CPU都是流水线设计,可以同时执行多条指令的不同阶段。

举个例子,我曾经手动重排过这样的代码:

优化前:

代码语言:c++
复制
result = a * b;
temp = c + d;
output = result / temp;
flag = (output > threshold);

优化后:

代码语言:c++
复制
// 启动乘法(耗时长)
result = a * b;
// 同时进行加法(不依赖乘法结果)
temp = c + d;
// 现在乘法可能完成了,进行除法
output = result / temp;
// 最后进行比较
flag = (output > threshold);

编译器通常会自动做这种优化,但在关键循环中,我有时会手动调整指令顺序,特别是当我知道某些操作特别耗时(比如除法和取模)时。

我的实战技巧

经过这么多年和编译器打交道,我总结了几个实用技巧:

  1. 别太聪明,写清晰的代码

有次我写了超复杂的嵌套三元表达式,想着"这样编译器能优化得更好"。结果呢?代码难读,bug一堆,而且优化效果还不如写清晰的if-else。

  1. 了解你的编译器标志

我的项目通常会使用:

-O3 -march=native -ffast-math

但要小心,-ffast-math会牺牲一些IEEE浮点精度换取速度。出过几次bug后,我学会了在科学计算代码里避免使用它。

真实故事:我是如何修复渲染引擎的

回到开头说的那次性能优化。我们的渲染引擎有个着色器编译环节特别慢。通过分析中间代码,我发现问题出在大量的纹理采样上。

原代码大概是这样:

代码语言:c++
复制
for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        // 每个像素采样16次周围的纹素进行模糊
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                result += sampleTexture(x + i - 2, y + j - 2);
            }
        }
    }
}

经过研究CPU流水线和缓存机制,我做了这些修改:

  1. 重排嵌套循环顺序,提高缓存命中率
  2. 手动展开内层循环,减少分支预测失败
  3. 预计算采样坐标,避免重复计算
  4. 使用SIMD指令同时处理多个数据点

最终代码变得更长了,但性能提升了30%。有趣的是,如果我只是简单地开启-O3优化,只能获得约10%的提升。这说明有时候了解底层原理,确实能比编译器做得更好。

总结:和编译器做朋友

编译器是个聪明但不全知全能的工具。它能做的优化比我们想象的多,但也有盲点。理解它的工作原理,能帮助你:

  1. 写出对编译器友好的代码
  2. 知道何时依赖自动优化,何时手动介入
  3. 更容易找出性能瓶颈

如果有一天你发现自己在研究汇编代码,不要害怕 - 欢迎来到优化的兔子洞!这里有时令人沮丧,但更多时候充满乐趣。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 常量折叠:让编译器替你算数学
  • 公共子表达式消除:不做重复的事
  • 中间表示:编译器的神秘语言
    • 抽象语法树(AST):编译器的草图
    • 静态单赋值(SSA):变量的"一夫一妻制"
  • 指令调度:CPU流水线的交通指挥官
    • 我的实战技巧
  • 真实故事:我是如何修复渲染引擎的
  • 总结:和编译器做朋友
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档