AI Infra 面经 (1)


Q: static关键字的作用是什么?修饰成员变量和成员函数分别有什么效果?static全局变量和普通全局变量有什么区别?

  • 修饰局部变量:生命周期延长至程序结束,但作用域不变。
  • 修饰全局变量:限制链接属性为内部链接(internal linkage),仅当前编译单元可见,普通全局变量默认外部链接。
  • 修饰成员变量:属于类而非对象,所有实例共享,需在类外定义。
  • 修饰成员函数:无this指针,只能访问静态成员。

Q: 如何让一个类只能在堆上创建对象?

将析构函数设为私有或protected,这样栈上对象无法自动调用析构函数,编译器会报错。提供一个公有的静态工厂方法通过new创建对象,并提供destroy()方法手动释放。


Q: C++右值引用是什么?有什么作用?

右值引用(T&&)绑定到临时对象(右值),核心作用是实现移动语义和完美转发。移动语义通过”窃取”临时对象的资源避免深拷贝,显著提升性能。完美转发通过std::forward保持参数的值类别,避免不必要的拷贝。


Q: C++中有哪些锁?各自的使用场景?

  • std::mutex:最基本的互斥锁,保护临界区。
  • std::recursive_mutex:允许同一线程多次加锁。
  • std::shared_mutex(C++17):支持读写锁,多读单写。
  • std::lock_guard:RAII封装,作用域自动加解锁,不可手动控制。
  • std::unique_lock:支持延迟加锁、手动加解锁、配合条件变量使用。

Q: C++ lambda表达式的语法和捕获方式?

语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }。捕获方式:[=]值捕获全部、[&]引用捕获全部、[x]值捕获x、[&x]引用捕获x、[this]捕获当前对象指针。lambda本质是编译器生成的匿名函数对象(仿函数)。


Q: std::move和std::forward的作用和区别?

  • std::move:无条件将参数转换为右值引用,表示资源可以被移动。本身不移动任何东西,只是类型转换。
  • std::forward:完美转发,保持参数原始的左值/右值属性。用于模板函数中将参数按原始类型转发给下一层函数,避免右值被退化为左值。

Q: C++程序的编译过程是什么?动态库和静态库有什么区别?

编译过程:预处理(宏展开、头文件展开)→ 编译(生成汇编)→ 汇编(生成.o目标文件)→ 链接(合并目标文件生成可执行文件)。

区别

  • 静态库(.a/.lib):链接时代码拷贝到可执行文件中,运行时无依赖,但体积大。
  • 动态库(.so/.dll):运行时加载,多进程共享一份,体积小,但存在运行时依赖。

Q: new和malloc的区别?new的底层实现?

  • new是运算符,malloc是函数;new返回具体类型指针,malloc返回void*。
  • new失败抛异常,malloc返回NULL。
  • new调用构造函数,malloc不调用。
  • new的底层:调用operator new(内部调malloc)分配内存 → 调用构造函数初始化对象。

Q: 拷贝构造函数的参数为什么必须是引用?

如果是传值,调用拷贝构造函数时需要先拷贝实参,而拷贝实参又需要调用拷贝构造函数,形成无限递归。所以必须传const引用。


Q: 如何实现线程安全的单例模式?

C++11推荐Meyers’ Singleton:利用局部静态变量的线程安全初始化保证。static Singleton& getInstance() { static Singleton instance; return instance; }。C++11标准保证局部static变量初始化是线程安全的。同时需要将构造函数私有化,删除拷贝构造和赋值运算符。


Q: 智能指针有哪些?shared_ptr是线程安全的吗?

三种智能指针:unique_ptr(独占所有权)、shared_ptr(共享所有权,引用计数)、weak_ptr(不增加引用计数,解决循环引用)。

shared_ptr的引用计数操作是原子的(线程安全),但对其管理的对象的读写不是线程安全的。多线程同时读同一个shared_ptr安全,但同时读写同一个shared_ptr实例或操作其指向的对象需要额外同步。


Q: vector的扩容机制是什么?为什么选择2倍扩容?

当size==capacity时触发扩容:分配新内存(通常2倍)→ 移动/拷贝元素 → 释放旧内存。选择2倍是在时间和空间之间的权衡:倍数太小导致频繁扩容(时间开销大),倍数太大浪费空间。2倍扩容使得n次push_back的均摊时间复杂度为O(1)。部分实现(如MSVC)使用1.5倍,可更好复用之前释放的内存。


Q: C++的内存模型分为哪几个区?

  • 栈区:局部变量、函数参数,自动管理,向低地址生长。
  • 堆区:动态分配(new/malloc),手动管理。
  • 全局/静态区:全局变量和static变量,程序生命周期。
  • 常量区:字符串字面量和const全局变量。
  • 代码区:存放机器指令。

Q: 指针和引用的区别?

  • 引用是别名,必须初始化且不可重新绑定;指针可以为空、可重新指向。
  • 引用没有独立内存地址(概念上);指针有自己的地址空间。
  • sizeof(引用)是被引用对象的大小;sizeof(指针)是指针本身大小(4/8字节)。
  • 不存在引用的引用、引用数组;但存在指针的指针、指针数组。

Q: 为什么构造函数不能是虚函数?为什么析构函数应该是虚函数?

构造函数不能是虚函数:虚函数通过vptr调用,而vptr在构造函数中才被初始化。构造时对象类型尚未确定,无法进行多态调用。

析构函数应该是虚函数(基类有多态用途时):通过基类指针delete派生类对象时,若析构函数不是虚函数,只会调用基类析构,导致派生类资源泄漏。


Q: 线程间共享内存时,什么时候用条件变量?什么时候用锁?

  • 锁(mutex):保护临界区,防止多线程同时访问共享数据导致数据竞争。
  • 条件变量(condition_variable):用于线程间的等待/通知机制,一个线程等待某个条件满足再继续执行。条件变量必须配合锁使用。

典型场景:生产者-消费者模型中,消费者在队列为空时通过条件变量等待,生产者放入数据后notify唤醒。


Q: 死锁的四个必要条件是什么?如何避免?lock_guard和unique_lock的区别?

四个必要条件:互斥、持有并等待、不可剥夺、循环等待。

避免方法:按固定顺序加锁(破坏循环等待)、使用std::lock同时锁多个mutex、使用try_lock、设置超时。

区别:lock_guard构造时加锁析构时解锁,不可手动操作;unique_lock支持延迟加锁、手动lock/unlock、可移动、配合条件变量。unique_lock开销略大。


Q: C++如何调用C语言封装的函数?

使用extern "C"包裹C函数声明,告诉C++编译器按C语言的命名规则(不做name mangling)进行链接:extern "C" { void c_function(int x); }


Q: C++多态的实现原理?虚函数表存在哪里?模板多态和模板偏特化是什么?

运行时多态:通过虚函数实现。每个含虚函数的类有一个虚函数表(vtable),每个对象持有vptr指向vtable。调用虚函数时通过vptr查表间接调用。vtable存放在只读数据段(.rodata)。

编译时多态:通过模板实现(也称静态多态),如CRTP模式。编译器在编译期实例化具体类型,无运行时开销。

模板偏特化:对模板参数的部分约束提供特殊实现,如template<typename T> class Foo<T*>是对指针类型的偏特化。


Q: 进程和线程的区别?

  • 进程:资源分配的基本单位,有独立的地址空间、文件描述符等。进程间通信开销大(IPC)。
  • 线程:CPU调度的基本单位,同一进程的线程共享地址空间和资源,切换开销小。线程间通信可直接通过共享内存。

Q: 什么是大端小端?如何判断当前系统的字节序?

  • 大端(Big-Endian):高位字节存放在低地址。
  • 小端(Little-Endian):低位字节存放在低地址。

判断方法:联合体法 - union { int i; char c; }; u.i = 1; if(u.c == 1) 则为小端。指针法 - int x = 1; if(*(char*)&x == 1) 则为小端。


Q: OpenCL的运行流程是什么?

获取平台和设备 → 创建上下文 → 创建命令队列 → 编译程序并创建内核 → 创建缓冲区并传输数据到设备 → 设置Kernel参数并入队执行 → 读取结果回主机 → 释放资源。


Q: GPU的架构层次是怎样的?

以NVIDIA GPU为例:GPU包含多个GPC(图形处理集群)→ 每个GPC含多个SM(流多处理器)→ 每个SM含多个CUDA Core(SP)、Tensor Core、共享内存、L1 Cache、Warp Scheduler。线程以Warp(32线程)为单位执行,SM是基本的调度和执行单元。


Q: GPU全局内存和共享内存有什么区别?如何更好利用共享内存?

全局内存容量大(GB级)、延迟高(数百周期)、所有线程可见;共享内存容量小(每SM几十KB)、延迟低(几个周期)、仅Block内线程可见。

利用共享内存的方法:将频繁访问的全局内存数据加载到共享内存中复用(tiling策略);利用共享内存实现线程间通信和数据交换;注意避免bank conflict。


Q: Cache的工作原理是什么?

Cache基于时间局部性和空间局部性原理,将CPU近期可能访问的数据缓存在更快的存储层。组织方式为Cache Line(通常64字节),采用组相联映射。访问时通过tag匹配判断命中,命中则直接返回,未命中则从下级存储加载整条Cache Line。替换策略常用LRU。


Q: 如何提高Cache命中率?

  • 数据结构紧凑,减少padding(AoS→SoA)。
  • 顺序访问内存,利用空间局部性。
  • 循环分块(Loop Tiling),使工作集适配Cache大小。
  • 避免大步长访问和随机访问。
  • 数据对齐,避免跨Cache Line访问。
  • 预取(prefetch)指令提前加载数据。

Q: 性能优化通常有哪些思路?

  1. 访存优化:合并访存、利用共享内存/寄存器、数据对齐、提高Cache命中。
  2. 计算优化:减少冗余计算、使用快速数学函数、向量化。
  3. 并行优化:提高占用率、均衡负载、减少同步。
  4. 延迟隐藏:计算与访存重叠、流水线。
  5. 算法优化:算子融合、减少kernel launch次数。

Q: 为什么在GPU编程中要减少分支?什么是分支掩码?

GPU以Warp(32线程)为单位执行指令(SIMT),同一Warp内的线程必须执行相同指令。若Warp内线程走不同分支,会发生Warp Divergence:两个分支都要串行执行,未激活的线程通过掩码(mask)屏蔽其写操作。这导致执行效率最高下降50%。


Q: OpenCL写kernel的主要参数有哪些?

  • global_work_size:全局工作项数量(类似CUDA的grid*block)。
  • local_work_size:工作组大小(类似CUDA的block size)。
  • kernel参数:通过clSetKernelArg设置,包括global/local/private的buffer和标量参数。
  • work_dim(工作维度,1/2/3D)和global_work_offset。

Q: 计算密集型和访存密集型算子有什么区别?

  • 计算密集型:算术运算量远大于内存访问量,性能受计算能力(FLOPS)限制,如GEMM。
  • 访存密集型:内存访问量大而计算少,性能受内存带宽限制,如elementwise、BN、激活函数。

判断依据是算术强度(FLOPs / Bytes),通过Roofline模型与硬件的计算/带宽比值对比。


Q: 可分离卷积在GPU上为什么慢?为什么是访存密集型?

可分离卷积将标准卷积分解为depthwise + pointwise两步。Depthwise卷积每个通道独立计算,计算量极小但需要从全局内存加载feature map,算术强度很低,属于典型的访存密集型。在GPU上无法充分利用计算单元,大量时间花在内存访问上。


Q: Conv+BN融合的原理是什么?为什么可以融合?

BN:y = γ * (x - μ) / σ + β,Conv:x = W * input + b。融合后新权重W' = (γ/σ) * W,新偏置b' = (γ/σ) * (b - μ) + β。可以融合是因为推理时BN的μ和σ是固定值,两个线性变换合并为一个,减少一次kernel调用和中间结果的访存。


Q: 推理框架中卷积有哪些实现方式?

  1. Im2Col + GEMM:将卷积转化为矩阵乘法,利用高度优化的GEMM库。
  2. Winograd:减少乘法次数,适合小卷积核(3x3)。
  3. FFT:频域卷积,适合大卷积核。
  4. 直接卷积:直接计算,适合特殊shape。
  5. 隐式GEMM:不显式做im2col变换,节省内存。

Q: 什么是时间局部性和空间局部性?

  • 时间局部性:最近被访问的数据在不久的将来很可能再次被访问(如循环变量)。
  • 空间局部性:被访问数据附近的数据在不久的将来很可能被访问(如数组顺序遍历)。

Q: Bank Conflict产生的原因和解决方法?

原因:共享内存被划分为32个bank,同一Warp内多个线程同时访问同一bank的不同地址时产生冲突,访问被串行化。

解决方法:Padding(在共享内存数组维度上加1使列访问错开bank);重新设计访问模式使同一Warp内线程访问不同bank;利用broadcast机制(同一bank同一地址的多线程访问不冲突)。


Q: TVM是什么?有什么特点?

TVM是端到端深度学习编译框架,将高层模型编译为各种硬件的高效代码。核心特点:计算与调度分离(Schedule描述优化策略);AutoTVM/Ansor自动搜索最优参数;多后端支持(CPU/GPU/TPU/FPGA);Relay IR图级别优化(算子融合、常量折叠)。


Q: Batch Normalization的计算过程、作用,以及训练与推理的区别?

计算y = γ * (x - μ) / √(σ² + ε) + β作用:加速收敛、缓解梯度消失/爆炸、正则化。训练vs推理:训练时用当前batch统计量并维护running统计量;推理时使用训练阶段累积的running_mean和running_var。


Q: Depthwise卷积和Pointwise卷积是什么?

  • Depthwise:每个输入通道独立应用一个卷积核,不做通道间信息交互,大幅减少参数量。
  • Pointwise:1x1卷积,负责通道间信息融合。

两者组合形成Depthwise Separable Convolution,计算量约为标准卷积的1/k² + 1/C_out。


Q: MobileNet v1/v2/v3的核心演进?

  • v1:用Depthwise Separable Conv替代标准卷积。
  • v2:引入Inverted Residual Block(先升维再降维)+ Linear Bottleneck(最后不加ReLU)。
  • v3:SE注意力模块 + NAS搜索结构 + h-swish激活。
  • GhostNet:用廉价线性变换生成”幽灵”特征图。

Q: YOLO v1/v2/v3和SSD的核心思想?

  • YOLO v1:图像划分为SxS网格,每格直接回归bbox和类别。
  • YOLO v2:加入Anchor、BN、多尺度训练。
  • YOLO v3:多尺度预测(FPN)、Darknet-53骨干。
  • SSD:多尺度特征图上密集放置default box,各层负责不同尺度目标。

Q: 全局池化一般用在哪里?

通常用在分类网络的最后卷积层之后、全连接层之前,将任意空间尺寸的特征图压缩为1x1,替代全连接层以减少参数量并保持平移不变性。


Q: 什么是模型蒸馏?

用大教师模型指导训练小学生模型。学生同时学习hard label和soft label(教师的软化输出)。soft label通过temperature控制,包含类间相似度等”暗知识”,使小模型获得接近大模型的性能。


Q: 量化的分类和Int8量化细节?对称/非对称量化区别?

按时机分PTQ和QAT;按粒度分per-tensor/per-channel/per-group。对称量化:q = round(x / scale),零点为0,适合权重。非对称量化:q = round(x / scale) + zero_point,适合激活值。QAT在训练中模拟量化噪声,精度优于PTQ。


Q: Dropout在训练和推理时的区别?

训练时以概率p随机置零神经元输出,保留部分除以(1-p)保持期望不变(inverted dropout)。推理时不进行dropout,使用全部神经元。


Q: 反卷积和空洞卷积是什么?

  • 反卷积(转置卷积):用于上采样,在输入间插入零再做正常卷积。
  • 空洞卷积:在卷积核元素间插入空洞,扩大感受野而不增加参数量,常用于语义分割。

Q: 什么是GIoU?

GIoU = IoU - |C \ (A∪B)| / |C|,C是包含A和B的最小闭合区域。解决IoU在两框不重叠时梯度为0的问题,取值范围[-1, 1]。


Q: Softmax为什么能用于分类?

softmax(x_i) = exp(x_i) / Σ exp(x_j),将任意实数映射到(0,1)且和为1,构成有效概率分布;保持相对大小关系;可微,配合交叉熵损失进行梯度反传。


Q: NMS有哪些改进方法?

  • Soft-NMS:降低高IoU框的置信度而非直接删除。
  • DIoU-NMS:用DIoU替代IoU,考虑中心点距离。
  • Weighted NMS:融合多个框坐标(加权平均)。
  • Matrix NMS:并行化NMS计算,显著加速。

Q: 手撕:实现卷积操作?

(编程题)


Q: 手撕:实现计算图?

(编程题)


Q: 手撕:实现Pooling操作?

(编程题)


Q: 手撕:实现NMS?

(编程题)


Q: 手撕:用OpenCL实现矩阵乘法和向量求和?

(编程题)