【infra之路】AI 编译器学习笔记(二):重拾 C++——移动语义与 LLVM 风格的类型转换

上一篇讲了对象生命周期和模板。这一篇讲 MLIR 代码里另外两处高频写法:std::move(patterns) 背后的移动语义 ,以及 isa / cast / dyn_cast 这套 LLVM 风格 RTTI。API 以 LLVM 20 为准。


一、移动语义

拷贝太贵

cpp 复制代码
std::vector<int> a = /* 一百万个元素 */;
std::vector<int> b = a;    // 申请新内存,把一百万个元素抄一遍

如果之后还要用 a,拷贝是必须的;如果 a 再也不用了,抄一遍就纯属浪费。

移动:不抄数据,直接过户

vector 对象本身很小,只有三样东西:指向堆上数据的指针、元素个数、容量。

cpp 复制代码
std::vector<int> b = std::move(a);
复制代码
移动前:  a { 指针 ──→ [一百万个元素] }
移动后:  a { 空 }
          b { 指针 ──→ [一百万个元素] }   ← 同一块内存,没有复制

拷贝是照着盖一栋一样的房子,移动是把房产证改个名字,不管房子多大,成本都只是改几个字段。

std::move 本身什么都不移动

它只是一个类型转换,给对象贴上"这个我不要了,可以拿走"的标签。真正干活的是接收方的移动构造函数:

cpp 复制代码
vector(const vector &other);   // 拷贝构造
vector(vector &&other);        // 移动构造,&& 是右值引用

写了 std::move(a),编译器就选第二个。

面试标准答法:std::move 只是把左值转换成右值引用,不产生任何移动;移动发生在移动构造函数或移动赋值运算符里。

三条使用规则

  1. 移动后不要依赖原对象的内容。 被移动后的 a 仍是合法对象,可以重新赋值或 push_back,但不能假设它还有原来的数据。
  2. 返回局部变量不用写 std::move。 编译器会自动移动,甚至直接省略拷贝。所以返回 SmallVector 并不慢。
  3. 没有堆上资源的类型,移动等于拷贝。 int、指针,以及 MLIR 的 Value、Type、Attribute、Op 句柄,内部只是一个指针,std::move 它们毫无意义。哪怕是只有一两个元素的 vector,拷贝也要申请一次堆内存,所以移动仍有明显收益。

只能移动、不能拷贝的类型

std::unique_ptr 表示独占所有权,不允许出现两份:

cpp 复制代码
std::unique_ptr<int> p = std::make_unique<int>(5);
std::vector<std::unique_ptr<int>> vec;
vec.push_back(p);              // 编译错误:试图拷贝
vec.push_back(std::move(p));   // 正确:所有权交给 vec,p 变空
vec.push_back(std::make_unique<int>(5));  // 临时对象,自动移动

对这类类型,std::move 不是优化,而是唯一的传递方式。

回到 MLIR

cpp 复制代码
RewritePatternSet patterns(&getContext());
patterns.add<FuseBiasRelu>(&getContext());
applyPatternsGreedily(getOperation(), std::move(patterns));

RewritePatternSet 里存的是一组 unique_ptr<RewritePattern>,所以整体不可拷贝。driver 需要的是 FrozenRewritePatternSet(冻结后不能再加 pattern,内部做好了查找优化),它只接受被移动过来的 RewritePatternSet。这行代码的含义是:pattern 配置完毕,整个交给 driver,patterns 之后就是空壳。


二、LLVM 风格的类型转换

问题:向下转换

cpp 复制代码
class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };
class Circle : public Shape {
public:
  double area() const override { return 3.14 * r * r; }
  double radius() const { return r; }   // 只有 Circle 有
private:
  double r = 1.0;
};

子类当基类用(向上转换)自动且安全。反方向就不行:手里只有 Shape *s,想调 radius(),得先确认它确实是 Circle。编译器里到处是这种需求:手里一个通用的 Operation *,先确认是不是 ReluOp,才能调它特有的函数。

标准 C++ 用 dynamic_cast,但它依赖编译器生成的 RTTI。LLVM 为了体积和速度关闭了 RTTI 和异常,自己实现了一套。

三个工具

写法 作用 类型不对时
isa<T>(x) 问:x 是不是 T 返回 false
cast<T>(x) 转:我确定它是 T 断言失败崩溃(仅在开启断言的构建中检查,否则是未定义行为)
dyn_cast<T>(x) 试着转 返回空

选择原则:只想知道是不是 → isa;逻辑上一定是、错了就是 bug → cast;不确定、要分支处理 → dyn_cast,并且一定要检查结果。

dyn_cast 的固定搭配是把声明写进 if 条件:

cpp 复制代码
// 不好:同一个检查做了两遍
if (isa<RankedTensorType>(type)) {
  auto t = cast<RankedTensorType>(type);
  use(t.getShape());
}

// 也不对:去掉了保护,type 不是 RankedTensorType 时 t 为空,直接崩
auto t = dyn_cast<RankedTensorType>(type);
use(t.getShape());

// 正确:判断和转换一次完成
if (auto t = dyn_cast<RankedTensorType>(type))
  use(t.getShape());

实现原理(面试常问)

做法很朴素:基类存一个 kind 枚举,每个子类提供静态函数 classof 比对。

cpp 复制代码
class Shape {
public:
  enum Kind { K_Circle, K_Square };
  Kind getKind() const { return kind; }
protected:
  Shape(Kind k) : kind(k) {}
private:
  Kind kind;
};

class Circle : public Shape {
public:
  Circle() : Shape(K_Circle) {}
  static bool classof(const Shape *s) { return s->getKind() == K_Circle; }
};

isa<Circle>(s) 内部调 Circle::classof(s),一次整数比较;dyn_cast 就是先 isa,是则转,否则返回空。

一句话:LLVM 风格 RTTI = kind 标记 + classof,不依赖编译器 RTTI。

在 MLIR 中

cpp 复制代码
if (auto relu = dyn_cast<ReluOp>(op)) {   // 注意没有 *:ReluOp 是句柄类
  Value in = relu.getInput();
}

对句柄类,dyn_cast 返回句柄本身,失败时是空句柄,放进 if 同样会被当作假。老教程里 type.dyn_cast<T>() 这种成员函数写法已经不推荐,统一用自由函数形式。

拆开 pattern 里最常见的一行

cpp 复制代码
auto biasAdd = relu.getInput().getDefiningOp<BiasAddOp>();
// 等价于
Operation *def = relu.getInput().getDefiningOp();  // 可能为空
auto biasAdd = dyn_cast_or_null<BiasAddOp>(def);   // 允许输入本身为空

它在两种情况下返回空:

  1. 输入由某个 op 产生,但那个 op 不是 BiasAddOp;
  2. 输入根本不是 op 产生的,而是块参数 (比如函数参数)。这时 getDefiningOp() 返回 nullptr,这也是内部必须用 _or_null 版本的原因,普通 dyn_cast 传空进去会直接崩。
mlir 复制代码
func.func @f(%x: tensor<4xf32>) -> tensor<4xf32> {
  %y = "mic.relu"(%x) : (tensor<4xf32>) -> tensor<4xf32>   // %x 是函数参数
  return %y : tensor<4xf32>
}

对融合 pattern 来说,两种情况的处理方式相同:匹配失败。


小结

  • std::move 只是贴标签,移动由移动构造完成;句柄类移动没有意义;unique_ptr 只能移动。
  • isa 问、cast 断言着转、dyn_cast 试着转;dyn_cast 永远配合 if (auto x = ...)。
  • getDefiningOp<T>() 返回空有两种含义:定义者类型不符,或值是块参数。

下一篇进入 MLIR 本身:核心类的地图。

相关推荐
paopaokaka_luck1 小时前
滑雪场管理系统(AI智能助手流式对话,ECharts 运营数据分析、教练、雪具与场地一体化预约,钱包支付退款、订单核销与服务评价)
java·人工智能·spring boot·网络协议·数据分析·echarts
陈嘿萌1 小时前
ICML 2026|浙大 1D-Fusion:用 Tokenizer 将图像“压”成一维 Token,重构融合表示
图像处理·人工智能·计算机视觉·图像融合·计算机科学与技术·学术研究与理论基础·顶级期刊与会议
看着博客敲代码1 小时前
WorkBuddy 接上 Kali MCP 做自动化渗透:信息收集到初步验证的落地记录
linux·人工智能·安全·网络安全·自动化
szxinmai主板定制专家1 小时前
储能EMS + 协调控制器实战:RK3568/3576/3588+FPGA+AI 故障录波与诊断
人工智能·深度学习·机器学习·fpga开发·rk3588·rk3576·协调控制器
俊哥V2 小时前
每日 AI 研究简报 · 2026-10-12
人工智能·ai
VibeCoding工程之道2 小时前
第八章 最终格局:不是 AI 淘汰人,是「依赖 AI 的人」淘汰自己
大数据·人工智能·ai焦虑·ai思考·ai反噬
IT_陈寒2 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
努力努力再努力wz2 小时前
【机器学习入门系列】从随机事件到正态分布:一文梳理概率论的核心知识
人工智能·机器学习·概率论
蜗牛互联网2 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python