秋招季又到了,不少人私信问我寒武纪软件岗的笔试到底考什么。说实话,寒武纪2019年秋招这套题,在当年AI芯片公司里算是相当有代表性的:既考C/C++基本功,又考操作系统和体系结构,最后还要看你对AI芯片软件栈有没有概念。这期内容我重点复盘“试题(二)”里的后半部分,涵盖操作系统、Linux调试、体系结构、算法手写和开放题,题型、考点、踩坑点都拆开讲,给后面想冲AI芯片方向软件岗的同学做个参考。
1. 笔试题第二场的整体结构与答题节奏
1.1 第二场题量分布和考察思路
我拿到的这场笔试,整体结构和第一场类似,但是风格有明显变化。第一场偏C/C++语法细节和基础数据结构,第二场则明显加重了系统层面的考查,题量大概在35道左右,时间120分钟,题型依次是:20道不定项选择、8道填空题、4道简答题、2道手写编程题,外加1道开放设计题。
这里要特别提醒一下“不定项选择”这四个字。寒武纪的软件岗笔试题不会明确告诉你这道题是单选还是多选,选项数量也不固定。也就是说,你不仅要判断某个选项对不对,还要判断这道题到底有几个正确选项。我见过不少同学在这上面吃亏,明明知识点会,结果少选漏选直接丢分。估计出题人就是想用这种方式筛选出真正能举一反三的人,毕竟写代码的时候,边界条件和多种可能情况恰恰是决定程序质量的关卡。
1.2 考察重点:从“语言层面”下沉到“系统层面”
第二场的知识权重非常集中。以我复盘的这份卷子来看,操作系统和Linux的内容占了差不多30%,计算机体系结构约20%,C/C++深度约25%,算法和数据结构约15%,剩下的10%是AI芯片软件栈相关的开放性问题。跟国内互联网大厂软件岗笔试相比,这个比例最大的不同在于:它不考你Java、不考Spring框架、不考分布式设计,而是死磕底层。
原因其实不复杂。寒武纪做的是AI芯片,软件岗的核心工作围绕驱动、编译器、运行时和算子库展开,这些工作极度依赖对内存、并发、指令流水线和缓存行为的理解。与其说这是一份“软件工程师笔试题”,不如说它是一份“系统软件工程师笔试题”。如果平时只写应用层业务代码,遇到这套题大概率会很吃力。
2. 操作系统与Linux真题复盘
2.1 进程与线程的差异:不是背概念,是给场景
真题里有一道印象很深的题,它没有直接问“进程和线程的区别有哪些”,而是给了一个多线程服务器模型,要求分析在Linux下,如果两个线程同时调用fork,这个多线程进程的子进程里到底会有几个线程。
这题的关键点在于fork只复制当前调用线程到子进程,其他线程不会跟着复制。如果在多线程环境下调用fork,子进程里只有调用fork的那个线程存活。如果之后子进程里再调用某个函数,而这个函数依赖其他线程初始化好的锁状态,就极容易死锁。出题人这么问,就是想看你对fork和线程模型的理解是不是基于真实运行机制,而不是背教材。
还有一个选择题直接给了四个选项:进程间通信方式中,哪种方式适合大量数据传输。很多人的第一反应是socket,但socket有协议栈开销;管道则需要多次拷贝;信号量根本不适合传数据;正确答案是共享内存。原因很简单,共享内存是所有IPC方式里唯一不需要内核态数据拷贝的方式,所以AI芯片底层做张量数据传输时,很多实现优先考虑共享内存而不是管道。
2.2 死锁的四个条件:去年考了,今年仍然值得背
第二场有一道填空题专门考死锁,问的是:死锁的四个必要条件是什么。这个名词大家应该都熟——互斥、持有并等待、不可剥夺、循环等待。但笔试光写四句话是拿不全分的,后边还跟了一问:破坏循环等待条件最常用的办法是什么。
答案显然是对资源进行有序分配,也就是所有进程按同一个编号顺序申请资源。为什么会用这个方法?因为循环等待的本质是多个进程各持有一部分资源,然后互相等对方释放。如果所有人按固定顺序申请,就不会出现“A等B、B等A”的环。底层软件里,驱动开发经常需要管理多个硬件资源,用统一的加锁顺序去规避死锁是最务实的做法,比什么银行家算法来得可靠得多。这一点当年考过,放到今天的系统软件面试中依然可能是考点。
2.3 Linux调试与性能分析简答题
简答题里有一道是给了一段崩溃日志,让你说明定位步骤。日志信息是“segfault at 0000000000000000 ip 00007f...”,很多人一看就懵。其实这类问题在Linux C/C++开发里太常见了,它本质就是空指针解引用。我当时拿到题目,第一步写的是用gdb加载core文件,然后输入bt查看调用栈;第二步是查看崩溃点附近的汇编,确认是不是mov指令访问了地址0;第三步是检查指针是否在某个分支里没有被赋值就用了。
第二个简答题考的是性能分析:程序CPU占用率很高,但响应很慢,如何排查。这类问题没有一个固定公式,但是如果按顺序来:先用top或htop看CPU核数和进程负载,然后用perf record/report采样热点函数,再看是不是缓存未命中过高,必要时用strace看系统调用是否频繁。我记得这个题在评分标准里分了两档,能写到perf和strace的得分明显高于只写“优化代码”这种大话。说白了,出题人就是要看到你手上有多少趁手的排查工具。
3. 体系结构与C/C++深度考点解析
3.1 volatile、内存屏障与多线程的关系
这场笔试在C/C++方向出现了一道比较有意思的多选:在嵌入式系统中,以下哪些场景必须使用volatile修饰变量。按我记的大概选项有:A. 中断服务程序和主循环共享的标志位;B. 多线程间用锁保护的共享变量;C. 寄存器映射到内存地址的硬件状态寄存器;D. 局部循环计数器。
正确答案应该是A和C,如果你对编译器行为理解得深,D在某些极端条件下也成立但要具体情况具体分析。volatile告诉编译器这个变量可能在当前执行流之外被改变,所以不要把它优化到寄存器里,每次都从内存重新读取。但这里有个巨坑:volatile并不保证原子性,也不保证内存屏障。在AI芯片驱动里,硬件寄存器必须用volatile去读,但如果是两个CPU核之间通信,光有volatile是不够的,需要配合内存屏障或原子操作。出题人把这道题放在这里,还埋了一个选项陷阱:B选项说“多线程间用锁保护的共享变量”,锁内部已经做了正确的内存序处理,再加volatile反而画蛇添足。
3.2 大小端判断和位域的内存布局
填空题有一道考大小端,看起来简单:在32位小端模式下,存储0x12345678,最低地址字节是多少。答案大家都会,是0x78。真正拉开差距的是后边那道位域题:给定一个struct,里面有若干bit-field,问这个结构体占几个字节,并且问某个字段的bit取值是多少。
位域在嵌入式驱动里非常常用,尤其是硬件寄存器映射。笔试题目大概长这样:
typedef struct { uint16_t a : 3; uint16_t b : 5; uint16_t c : 8; } RegField;关键点在于编译器会按单元分配位域。在多数平台的默认对齐规则下,3 + 5刚好占一个字节,后面8位占第二个字节,所以整体是2字节。但如果你把c改成超过8位,或者调整字段顺序,结果就可能完全不同。处理这类题时,我的经验是先看位域类型占多少字节(这里uint16_t是2字节),再看一个存储单元能不能放得下所有位域——按顺序从低位开始分配,放不下就另起一个单元。笔试时至少一半人会在这类题上算错,本质是对C内存布局的“土办法”不够熟。
3.3 结构体对齐与sizeof的必考题
结构体对齐几乎是寒武纪这类偏底层的软件岗笔试必考项,第二场也不例外。题目给了一个结构体,里面混着char、int、short和一个指针,问在64位系统下sizeof是多少。很多人会背“对齐到最大成员大小”这个口诀,遇到指针就傻眼,因为指针在64位下是8字节,但还要考虑每个成员本身的偏移规则。
正确做法是逐成员算偏移:char占偏移0,然后要跳过3个填充字节让int对齐到4,short放在偏移8,最后指针要从偏移16开始,总大小要按最大对齐数8对齐,所以最终是24。这道题最大的坑是编译器默认对齐选项,如果用了#pragma pack(1)结果直接变成15。做题时要先看题面有没有pack相关提示,没有就按默认规则来计算。这块内容看着基础,实际上特别能筛人。
4. 算法与手写代码题复盘
4.1 手写题一:两个有序数组的中位数
第二场最后有两道手写编程题,第一道是“给定两个有序数组,求合并后的中位数,要求时间复杂度O(log(m+n))”。很多人一看中位数,先合并再找,时间复杂度直接O(m+n),虽然能跑,但是复杂度不达标。
O(log(m+n))的关键是二分法。思路不复杂:要在两个数组里找到第k小的数,每次比较两个数组的第k/2个元素,排除掉较小那一侧的前k/2个元素,然后k减半,循环下去。中位数则是两种总长度的奇偶情况。如果m+n是奇数,直接找第(m+n)/2+1个;如果是偶数,找第(m+n)/2和第(m+n)/2+1个求平均。这道题考的不只是二分,还考临界条件处理,数组越界、k/2超出长度时需要灵活调整,很多人在手写时没处理好边界,逻辑就乱了。
另外,说一下卷面问题。笔试是纸质的,代码要写清楚变量名和缩进。阅卷老师的评分点里一定会看边界判断是否完整。建议写完以后在末尾补充几行注释,把自己考虑的边界条件列一下,比如“m=0时直接取B数组”“k=1时取min(A[0],B[0])”,这样做其实是在帮阅卷人快速看到你的思考颗粒度。
4.2 手写题二:LRU缓存
第二道手写题是LRU缓存,要求设计一个数据结构,支持get和put操作,get和put的时间复杂度都是O(1),缓存满时淘汰最近最久未使用的项。这道题在互联网公司也是高频题,寒武纪软件岗考它,大概率是因为驱动和运行时里有很多类似的资源管理场景。
标准方案是哈希表加双向链表。哈希表负责O(1)查找,双向链表负责O(1)淘汰。关键点在于:每次get命中,要把节点从链表当前位置摘下来,放到链表头部;每次put新节点,先判断缓存是否已满,满了就删除链表尾部节点,同时删除哈希表中对应的映射,然后插入新节点到头部。
手写时有个细节容易出错:链表的摘除和插入操作涉及的指针太多了,如果不画图,写错一个next或prev指向,整个链表就废了。我当时的做法是先画出插入、删除、移动三个操作的指针变化图,再动手写代码。不要在代码里使用std::list偷懒,面试官想看的是你能不能独立实现双向链表,而不是依赖STL。就算允许用STL,也要能解释清楚list的迭代器在erase之后为什么会失效,这个追问频率很高。
4.3 开放设计题:一个AI推理引擎的任务调度
最后一道开放设计题,题干大致是:在AI推理场景中,一个请求会拆成多个算子任务,部分算子可以并行执行,部分算子存在前后依赖,请设计一个任务调度方案。这道题不要求写完整代码,但要求画出依赖图、说明数据结构、讲清调度流程。
这道题其实考察的是DAG拓扑排序和线程池的结合使用。我当时的答题思路是:先把算子依赖关系建成DAG,每个节点是一个任务,边表示依赖;然后用入度表做拓扑排序,入度为零的节点可以进入就绪队列;就绪队列由线程池消费,每个算子执行完,更新依赖它的下游节点的入度,如果下游入度归零,就把它加入就绪队列。再进一步扩展,可以提到优先级调度和资源感知调度,比如某些算子占用大块显存,可以不立即执行,而是等显存充足再调度。
开放题没有标准答案,阅卷看的是系统设计意识。建议在回答时先明确抽象层次,再讲数据结构和算法,然后说明多线程同步方式,比如互斥锁保护就绪队列还是用无锁队列。能写到无锁队列,会在印象分上有明显优势。这个问题放到今天看,其实就是寒武纪MagicMind这类编译运行时工具链中常见的技术场景,只是当年它还是一道概念题,现在已经变成实打实的日常工作了。
5. 寒武纪软件栈背景题与备考建议
5.1 AI芯片软件岗为什么总要问“你对寒武纪了解多少”
第二场笔试的简答题最后一道是:简述你对AI芯片软件栈的理解。这道题让很多人犯难,因为它不是纯粹的技术题,还要看你有没有提前了解目标公司。
2019年的时候,寒武纪的软件栈叫NeuWare,面向思元系列芯片,包含算子库、编译器、运行时等层次。到了近两年,MagicMind这个工具链慢慢成为寒武纪开发者社区里的主要关键词,不少做AI推理部署的工程师应该都在开发者社区下载过MagicMind。它的核心价值在于把训练好的模型自动编译成能在寒武纪芯片上高效运行的代码,把算子融合、内存复用、指令调度这些脏活累活交给编译器去优化。
笔试如果遇到“谈AI芯片软件栈的理解”,你应该形成这样一条思路:应用层框架(TensorFlow/PyTorch)在上层,往下是模型转换与图优化层,再往下是算子编译器与运行时,最下面是驱动和硬件。如果能再点出“AI编译器要在正确性和性能之间做很多权衡,比如layout选择、算子融合策略、内存分配策略”,就已经是很高分的回答了。2019年那会儿大家对AI编译器还没有今天这么熟悉,能回答到这个深度的人确实不多。
5.2 给下一届秋招同学的备考清单
既然已经复盘到第二场,我顺手把这场笔试的备考清单整理一下,重点科目就是C++内存模型、Linux基础操作、体系结构、算法四类,外加对AI编译工具链的认知。
第一,C++方向,《Effective C++》前几章要反复看,重点掌握构造/析构/赋值、智能指针、虚函数实现机制、const和static的多层用法。第二,操作系统方向,进程线程、同步互斥、死锁、虚拟内存、缺页中断、文件系统这几个板块都属高频,建议结合《深入理解计算机系统》第8、9章一起学。第三,算法方向,hot 100题至少要刷两遍,特别是链表、二叉树、二分、动态规划、LRU这几种类型。第四,针对寒武纪,建议花一点时间了解AI芯片软件栈相关的概念,不要求会写算子kernel,但至少要知道模型从训练框架到芯片执行要经过哪些阶段。
5.3 做题时的几条实战经验
最后分享几条我在复盘这场笔试时总结出来的经验,都是实打实踩坑换来的。
第一,看到不定项选择时先把它当成多选来做,每个选项单独判断真假,再选出所有正确项,这样可以降低漏选概率。第二,所有涉及内存布局的题目,先确认系统位数、编译器默认对齐、有没有pack指令,这三个变量不确定,答案都免谈。第三,手写代码时,先写边界条件,再写主体逻辑,最后写测试用例,如果时间不够,边界条件注释也能让阅卷人知道你有这个意识。
最重要的一条,不要只看标准答案,要研究答案背后的运行机制。寒武纪软件岗的笔试题目风格,跟写业务代码那套完全不同,它更看重你是否理解程序在硬件上是怎么跑起来的。如果只会调函数、不会分析内存和并发问题,即使笔试过了,后面面试追问“为什么”“如果换成这种场景会怎样”时还是会露馅。
我事后复盘有个很深的感受,这套卷子里没有一道题是为了难而难,每道题都能在寒武纪实际的驱动、编译器或者运行时工作中找到对应的影子。准备这种偏底层的软件岗笔试,最有效的方式不是刷海量的面经,而是老老实实把C/C++、操作系统、体系结构这三块基础打扎实,再认真研究一下AI芯片软件栈的发展脉络——从NeuWare到MagicMind,理解它们解决的问题,基本上就抓住了这家公司软件岗笔试的命脉了。