百度 AI Infra 实习 (2)
Q: C++中static关键字的作用?
static 在 C++ 中有四种不同的语义,取决于使用的上下文:
静态局部变量:函数内
static int x = 0;。生命周期延续到程序结束(存储在数据段而非栈上),但作用域仍限于函数内。只初始化一次,后续调用保持上次的值。C++11 保证线程安全初始化(Magic Static)。静态全局变量/函数:文件级
static。限制符号的链接属性为内部链接(internal linkage),只在当前翻译单元可见。防止命名冲突,类似匿名命名空间的效果。静态成员变量:
class Foo { static int count; };。属于类而非对象实例,所有对象共享同一份。需要在类外定义(int Foo::count = 0;)。存储在数据段。静态成员函数:无
this指针,只能访问静态成员。可通过类名直接调用(Foo::staticFunc())。常用于工厂方法、单例获取等。
Q: C++中const关键字的作用?
const 表达”不可修改”的语义约束,在不同位置有不同含义:
修饰变量:变量初始化后不可修改。编译期常量优先用
constexpr。const变量可在运行时初始化但之后不可变。修饰指针(关键区分点):
const int* p(或int const* p):指向常量的指针,不能通过 p 修改指向的值int* const p:常量指针,p 本身不能重新指向其他地址const int* const p:两者都不可修改
修饰成员函数:
void foo() const;承诺不修改对象状态(不修改非 mutable 成员变量)。const 对象只能调用 const 成员函数。这是接口设计中区分读/写操作的重要手段。修饰引用参数:
void func(const string& s)防止函数内修改实参。同时允许传入临时对象(右值绑定到 const 引用)。
const 正确性原则:尽可能将不修改的参数声明为 const,让编译器帮助检查错误,也为优化提供更多信息。
Q: C++内存结构(内存布局)?
程序在虚拟内存中的布局(从高地址到低地址):
- 栈区(Stack):局部变量、函数参数、返回地址。向低地址增长,默认 8MB(可通过 ulimit 调整)。分配释放极快(移动栈指针即可)。
- 堆区(Heap):
malloc/new动态分配。向高地址增长,GB 级容量。分配较慢(需要在空闲链表中查找)。 - BSS 段:未初始化(或初始化为 0)的全局/静态变量。程序加载时 OS 清零,不占用可执行文件空间。
- 数据段(Data):已初始化的全局/静态变量。占用可执行文件空间。
- 代码段(Text):编译后的机器指令。只读 + 可执行,多个进程可共享同一份代码段。
额外区域:mmap 区域(在栈和堆之间,用于动态库和大块 mmap 分配)。ASLR 随机化各区域基址。
Q: C++智能指针有哪些?
C++11 提供三种智能指针(<memory> 头文件),通过 RAII 自动管理堆内存:
shared_ptr:共享所有权,内部引用计数(原子操作保证计数线程安全)。注意 shared_ptr 本身的拷贝/析构线程安全,但指向对象的并发读写不安全。
make_shared<T>(args...)比shared_ptr<T>(new T(args...))高效(一次内存分配 vs 两次)。unique_ptr:独占所有权,delete 了拷贝构造/赋值。只可
std::move转移所有权。大小与裸指针相同(零额外开销)。支持自定义删除器(如unique_ptr<FILE, decltype(&fclose)>)。是默认首选。weak_ptr:弱引用,不增加引用计数。通过
lock()尝试提升为 shared_ptr(对象已释放则返回 nullptr)。主要用途:打破 shared_ptr 循环引用;缓存中检查对象是否存活。
Q: C++四种强制类型转换?
C++ 提供四种命名的类型转换运算符,替代 C 风格的 (type)expr,增加类型安全性和可读性:
static_cast:编译期检查的”安全”转换。用途:基本类型转换(int->float)、void* 转具体类型指针、向上转型(派生->基类)。不做运行时检查。
dynamic_cast:运行时类型检查的多态转换。基类指针/引用转派生类(downcast),需要基类有虚函数。失败时返回 nullptr(指针)或抛 bad_cast(引用)。有 RTTI 开销。
const_cast:唯一能去除/添加 const/volatile 属性的转换。典型场景:调用不保证 const 正确的第三方库接口。注意:对真正的 const 对象去 const 后写入是 UB。
reinterpret_cast:最底层的位模式重新解释。指针类型间转换、整数与指针间转换。编译器不做任何检查,是最危险的转换。用于底层序列化/硬件寄存器映射等场景。
Q: 析构函数能否传参?能否有返回值?
都不能。析构函数的签名是固定的:~ClassName(),无参数、无返回值(连 void 都不写)。
原因:
- 无参数:析构是自动触发的(对象离开作用域、delete、容器清除),调用方无法也不需要传递参数
- 无返回值:析构的语义是”清理资源”,不产生结果
- 每个类有且仅有一个析构函数(不能重载)
- 调用时机:栈对象离开作用域、delete 堆对象、容器 clear/resize、全局对象在 main 结束后由 CRT 调用
最佳实践:析构函数不应抛出异常(若抛出且有另一个异常正在传播则 terminate)。
Q: C++运行时多态的具体实现?
通过虚函数表(vtable)+ 虚函数指针(vptr)机制实现:
编译期:
- 编译器为每个含虚函数的类生成一个 vtable(常量数组,存在只读数据段)
- vtable 中按虚函数声明顺序存放各虚函数的实际地址
- 派生类的 vtable 复制基类表并覆盖被 override 的函数地址
运行期:
- 每个对象的开头(通常前 8 字节)存放 vptr,指向其所属类的 vtable
- vptr 在构造函数中被设置(基类构造设为基类 vtable,派生类构造覆盖为派生类 vtable)
- 通过基类指针调用虚函数时:读 vptr -> 加偏移找到 vtable 槽位 -> 读取函数指针 -> 间接调用
开销:每个对象增加 8 字节(vptr);每次虚函数调用增加一次间接跳转(通常 L1 命中无显著延迟);虚函数不能内联(编译器无法确定目标)。
Q: 析构函数和构造函数能否是虚函数?
构造函数不能是虚函数:
- 逻辑上:构造时对象类型尚未确定(正在创建中),vptr 还未被正确设置,无法进行动态派发
- 实际上:构造函数负责设置 vptr,在基类构造函数执行时 vptr 指向基类 vtable,派生类构造函数会覆盖为派生类 vtable
- 替代方案:使用工厂模式(Factory Pattern)实现”虚构造”效果
析构函数可以且应该是虚函数(当类被用作基类时):
- 通过基类指针
delete对象时,如果析构函数非虚,只调用基类析构,派生类资源泄漏(UB) - 声明为
virtual ~Base() = default;确保正确调用整个继承链的析构 - 经验法则:如果类中有任何虚函数,析构函数就应该是虚的
Q: 什么是RAII?
Resource Acquisition Is Initialization(资源获取即初始化)——C++ 最重要的编程范式之一。
核心思想:将资源的生命周期绑定到对象的生命周期。构造函数中获取资源(打开文件/分配内存/获取锁),析构函数中释放资源。利用 C++ 的确定性析构(对象离开作用域时必然析构)保证资源不泄漏,即使发生异常也是如此。
典型应用:
std::unique_ptr/std::shared_ptr:管理堆内存std::lock_guard/std::unique_lock:管理互斥锁std::fstream:管理文件句柄cudaStream的 RAII 封装:管理 CUDA 资源
对比其他语言:Java/Python 用 try-finally 或 GC 管理资源,不保证释放时机。C++ RAII 保证确定性即时释放,无需 GC。这使得 C++ 特别适合需要精确资源控制的系统编程和高性能计算。
Q: std::move、左值和右值?
值类别(C++11 核心概念变更):
- 左值(lvalue):有名字、可取地址、持久存在的表达式。如变量名
x、*ptr、arr[0] - 右值(rvalue):临时对象、字面量,即将销毁的值。如
42、x + y、std::string("hello") - 亡值(xvalue):
std::move(x)的结果,标记为”可被移动”
std::move 的本质:将左值无条件转换为右值引用(static_cast<T&&>(x)),不做任何实际移动。它是一个”许可标记”——告诉编译器”这个对象可以被窃取资源”。
移动语义的价值:
1 | vector<string> v; |
对于 string/vector 等含堆内存的类型,移动只需转移内部指针(O(1)),避免深拷贝(O(n))。
Q: Python的内存管理机制?
Python 使用混合内存管理策略,兼顾效率和安全:
引用计数(主机制):每个 PyObject 维护
ob_refcnt。引用增加(赋值/传参/容器添加)时 +1,减少时 -1。归零立即调用__del__并回收内存。优点是确定性释放(无 GC 暂停),缺点是无法处理循环引用且原子操作有开销(GIL 部分缓解)。分代垃圾回收(处理循环引用):将对象分为 0/1/2 三代。新对象在第 0 代,GC 频繁扫描年轻代。存活越久的对象越少被扫描。使用标记-清除算法检测引用环。触发条件:第 N 代对象分配数超阈值。
内存池(pymalloc):小对象(<=512B)使用专用分配器,按大小级别(8B 对齐)维护空闲链表。减少 malloc 系统调用次数和碎片。大对象直接走系统 malloc。
intern 机制:小整数(-5~256)和短字符串在解释器启动时预创建并缓存复用,避免重复分配。
Q: Python装饰器的原理?
装饰器是高阶函数 + 闭包的语法糖应用:
1 |
|
原理:decorator 接收函数对象作为参数,返回一个新函数(通常是包裹了原函数的闭包)。闭包保持对原函数的引用,在调用前后插入额外逻辑。
常见模式:
- 带参数的装饰器:三层嵌套
decorator(args)(func)(call_args) - 类装饰器:实现
__call__方法 functools.wraps:保留原函数的__name__和__doc__
典型应用:日志记录、性能计时、权限检查、缓存(@lru_cache)、重试逻辑、注册路由(Flask @app.route)。
Q: Python import时解释器做了什么?
Python import 是一个完整的模块加载流程:
- 检查缓存:查看
sys.modules字典,已导入则直接返回缓存的模块对象(避免重复执行) - 搜索模块:按
sys.path列表顺序搜索(当前目录 -> PYTHONPATH -> 标准库 -> site-packages) - 创建模块对象:实例化
types.ModuleType - 编译字节码:将 .py 源文件编译为字节码。若存在 .pyc 且未过期(根据时间戳或哈希判断)则直接加载
- 执行顶层代码:在模块的命名空间中执行所有顶层语句(赋值、函数定义、类定义、print 等)
- 注册到缓存:将模块对象存入
sys.modules
常见坑:循环 import(A 导入 B 同时 B 导入 A)会导致看到不完整的模块。解决:延迟 import 或重构依赖。
Q: git merge和git rebase的区别?
两者都是整合分支修改的方式,但保留历史的策略不同:
merge:
- 创建一个新的”合并提交”(merge commit),有两个父提交
- 保留完整的分支创建和合并历史(非线性)
- 不改写任何已有提交的哈希
- 适合:公共分支/多人协作(保持历史真实性)
rebase:
- 将当前分支的提交”变基”到目标分支之上(逐个重新应用 patch)
- 历史变为线性(看起来像一直在最新代码上开发)
- 改写了提交哈希(所有被 rebase 的提交都是新创建的)
- 适合:个人 feature 分支、提交整理(保持历史整洁)
黄金规则:永远不要对已推送到公共仓库的提交做 rebase(会导致其他人的历史与你冲突)。
Q: git撤回提交的命令?
| 命令 | 效果 | 适用场景 |
|---|---|---|
git reset --soft HEAD~1 |
撤回提交,改动保留在暂存区 | 想修改 commit message |
git reset --mixed HEAD~1 |
撤回提交+暂存,改动保留在工作区 | 想重新组织提交 |
git reset --hard HEAD~1 |
彻底撤回,丢弃所有改动 | 完全放弃这次提交 |
git revert HEAD |
创建新提交来反转,不改写历史 | 撤销已推送的提交 |
选择原则:未推送用 reset(简单直接),已推送用 revert(安全不影响他人)。reset --hard 需谨慎,丢失的内容只能通过 git reflog 在 30 天内恢复。
Q: 介绍Transformer结构?
Transformer(Vaswani 2017)是当代深度学习的基础架构,原始设计为 Encoder-Decoder 结构:
核心组件:Multi-Head Self-Attention + FFN + LayerNorm + 残差连接
Encoder:处理输入序列,双向 attention(每个位置可看到所有其他位置)。堆叠 N 层。
Decoder:自回归生成,包含 Masked Self-Attention(causal mask 确保只看左侧)+ Cross-Attention(关注 encoder 输出)+ FFN。
现代演进:LLM 主流使用 Decoder-only 结构(GPT 系列),因为统一的自回归模式适合通用生成。优化包括:Pre-Norm 替代 Post-Norm、RoPE 替代 sinusoidal、SwiGLU 替代 ReLU FFN、GQA 替代 MHA。
Q: 其他大模型结构与Transformer的对比?
| 架构 | 复杂度 | 训练 | 推理 | 长序列能力 |
|---|---|---|---|---|
| Transformer | O(n^2) | 高度并行 | KV Cache | 上下文有限但建模强 |
| Mamba/SSM | O(n) | 并行(scan) | 恒定状态 | 线性推理但全局建模弱 |
| RWKV | O(n) | 并行(WKV) | RNN式递推 | 线性推理,质量接近Transformer |
| RetNet | O(n) | 并行模式可用 | 递推O(1)/步 | 多尺度衰减 |
核心 trade-off:Attention 的 O(n^2) 提供了全局信息交互能力(任何两个位置都能直接通信),这是其强大表达力的来源。线性模型通过压缩历史到固定大小的状态来实现 O(n),但压缩必然损失信息。目前在万亿 token 规模预训练上,Transformer 仍然质量最优。
Q: 谈谈ResNet?
ResNet(He et al., 2015)引入残差连接(skip connection)解决深层网络的退化问题。
核心洞察:深层网络理论上不应该比浅层差(至少可以学恒等映射),但实际训练中深层网络精度反而下降。原因不是过拟合而是优化困难——梯度消失使得深层参数难以更新。
解决方案:让网络学习残差映射 F(x) = H(x) - x,即 H(x) = F(x) + x。如果最优映射接近恒等,学习 F(x)≈0 比学习 H(x)≈x 容易得多(参数初始化在零附近)。
结构:
- BasicBlock:两层 3x3 conv + BN + ReLU,加 shortcut
- Bottleneck:1x1(降维) + 3x3 + 1x1(升维),减少计算量用于深层网络(ResNet-50/101/152)
对 Transformer 的影响:Transformer 的残差连接(x + Attention(x)、x + FFN(x))直接继承了 ResNet 的思想,是训练深层 Transformer 的关键。
Q: 手撕:用栈实现队列?
(编程题)
Q: 谈谈Docker?
Docker 容器本质是受限的 Linux 进程,通过三大内核机制实现轻量级虚拟化:
- Namespace:6+ 种隔离(PID/Network/Mount/UTS/IPC/User),让容器拥有独立的进程树、网络栈和文件系统视图
- Cgroup:资源限制与计量(CPU quota/Memory limit/IO bandwidth),防止单容器耗尽宿主资源
- UnionFS(OverlayFS):分层只读镜像 + 可写容器层(Copy-on-Write),共享基础层节省空间
与 VM 对比:VM 有独立内核(Guest OS),启动需分钟、占用 GB 级内存;容器共享宿主内核,启动秒级、占用 MB 级。代价是隔离性弱于 VM(内核漏洞可能逃逸)。
Q: TVM的基本原理和作用?
TVM 是深度学习编译器框架,将高层模型表示编译为特定硬件的高效执行代码。
编译流程:
- 前端导入:从 PyTorch/TF/ONNX 导入模型到 Relay IR(高层图 IR)
- 图级优化(Relay):算子融合(减少内存读写)、常量折叠、布局转换(NCHW->NHWC 适配硬件)、死代码消除
- 算子级优化(TE/TIR):循环变换(tiling/split/reorder/vectorize/unroll)、内存层次管理(cache_read/cache_write)
- 自动调优:AutoTVM(模板+搜索)或 Ansor/Meta Schedule(无模板自动生成 schedule),在参数空间中搜索最优配置
- 后端代码生成:输出 CUDA/OpenCL/LLVM IR/Metal 等目标代码
价值:一次编写模型,自动适配多种硬件(GPU/CPU/NPU),性能接近手写算子。尤其适合新硬件的快速算子开发。
Q: 谈谈网络量化?
量化将模型权重/激活从高精度(FP32/FP16)转换为低精度(INT8/INT4),是推理加速的核心技术之一。
方法分类:
- PTQ(Post-Training Quantization):训练后量化,无需重新训练。用少量校准数据(100-1000 条)确定量化参数(scale/zero_point)。快速但可能有精度损失。
- QAT(Quantization-Aware Training):训练中插入伪量化节点模拟量化误差,通过 STE(Straight-Through Estimator)传梯度。精度更好但需要训练流程。
量化粒度(从粗到细):
- per-tensor:整个张量共享一组参数,最简单但精度最差
- per-channel:每个输出通道独立参数,适应通道间值域差异
- per-group:通道内分组(如 128 个元素一组),精度最好但元数据开销大
收益:模型大小缩减 2-4x,推理速度提升 2-4x(INT8 计算单元吞吐是 FP16 的 2 倍),显存占用减半以上。代价是部分精度损失(通常 <1% 任务指标下降)。