十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

寒武纪后端笔试(二)复盘:C++内存与Linux并发考点全解析

寒武纪后端笔试(二)复盘:C++内存与Linux并发考点全解析 寒武纪2019秋招后端岗的笔试二是我当年校招时印象最深的一套题。它不是那种把 LeetCode 原题堆满的卷子而是把C内存、Linux并发、网络协议和系统设计全部串在一起逼着你在一两个小时内证明自己真的能写后端。后来我帮不少朋友复盘过寒武纪的笔试题发现很多同学不是不会某个知识点而是不知道这套题的考察逻辑。今天我就把当年做题、之后又反复研究同类考点的经验整理出来聊聊这套题到底在考什么以及每一类题目该用什么思路去解。1. 笔试二的选题逻辑寒武纪后端岗和互联网后端不是一回事1.1 这家公司的“后端”到底指什么很多备考同学第一个误区是把寒武纪后端笔试当成普通互联网后端笔试来准备上来就背Java八股、Spring全家桶、Redis缓存。实际上寒武纪这类AI芯片公司的“后端岗”含义要宽得多可能是做训练/推理平台的业务服务可能是给编译器、运行时、MagicMind这类工具链写配套服务也可能要靠近硬件做性能优化。这意味着笔试更看重C/C、内存模型、Linux系统编程、并发调度这些偏底层的功底而不是单纯看你会不会用框架。我当时拿到笔试二的时候第一感觉是“这卷子是给后台开发还是给系统工程师出的”做到后面才明白这两类要求在这里是合并的。你既要懂网络接口怎么设计、数据库索引怎么建也要懂一个对象的生命周期、一段并发代码为什么会有问题。所以复习时不能只刷算法更不能只背框架要把操作系统和C底层当成主线。1.2 题型分布与复习优先级从题型看笔试二大概可以分成四块一是选择题/填空题偏语言细节和系统原理二是简答题偏网络、并发、数据库场景三是编程题包含算法题和一个小型系统设计题四是给一段代码让你找问题这类题最要命因为看起来都对但实际有坑。我给当时的优先级排了个序C/C语言细节和内存管理排最前然后是Linux并发与IO模型再是网络和数据库最后才是纯算法和系统设计。理由很简单寒武纪后端笔试的底层语言是C你连智能指针和内存对齐都答不利索后面网络题答得再好也很难拉回来。算法题反而不用怕因为岗位不是纯算法岗考察难度不会到竞赛水平把二分、TopK、动态规划基础题稳住就够了。2. C/C与内存细节这套卷子里最硬的一块骨头2.1 指针、引用与内存布局的经典陷阱笔试题里很爱考这么一类题给你一个结构体问sizeof是多少或者给你一个指针问加减之后地址变了多少。看起来简单但坑很深。#include iostream struct A { char a; int b; char c; }; int main() { std::cout sizeof(A) std::endl; return 0; }如果编译器默认4字节对齐这个结构体大小不是6而是12。原因是b要从偏移4开始放a占偏移0中间空出3个字节paddingc放在偏移8最后整个结构体大小要对齐到4的整数倍于是变成12。把字段顺序改成char a; char c; int b;之后大小就变成8。这个考点背后的原理是CPU访问对齐数据更快编译器会用填充字节换取性能现代处理器上未对齐访问可能直接报错或者触发多次内存读取。指针运算同样容易中招。一道经典题是int *p nullptr; p;问p变成了什么。有些同学理所当然写“p加1”但nullptr上做算术本来就是未定义行为虽然很多编译器实际结果是(int*)4但你不能依赖它。正确的回答是先说这是未定义行为再说如果编译器按常规处理会加上sizeof(int)。答题时把两个层次都写出来会显得你确实理解指针算术的本质。2.2 虚函数、智能指针与RAII为什么永远是重点为什么AI芯片公司后端笔试那么执着考虚函数因为工具链和平台服务要处理大量继承体系比如不同的硬件后端设备类、不同的任务执行器都依赖虚函数实现运行时多态。题目一般会追问基类析构函数要不要声明为virtual答案是要。否则通过基类指针delete派生类对象时派生类析构函数不会被执行资源就泄漏了。C里这种未定义行为不会马上报错线上可能过很久才暴露出内存涨、句柄增多的现象。智能指针部分我记得一道典型题是让分析一段代码能不能编译、析构会不会执行。涉及shared_ptr循环引用时两个对象互相持有对方引用计数永远降不到0析构不会触发。解决办法是把其中一边改成weak_ptr它不会增加引用计数需要时通过lock()提升为shared_ptr使用。这背后还要理解shared_ptr的引用计数本身是原子的所以拷贝和析构有一定开销如果场景是独占所有权优先用unique_ptr它的开销基本为零。RAII不是简单语法而是C后端的基本功——锁用lock_guard管理、连接池用RAII包装、临时文件用作用域对象负责删除。笔试里如果让你“挑出下面代码的资源泄漏”思路就是看每一处new、open、malloc有没有对应的delete、close、free如果中间抛异常这些释放还不会执行RAII就是为了兜住这种情况。2.3 内存问题排查工具的实战意义笔试后半段经常给一段代码让你模拟线上环境分析它崩溃的原因。这种题光靠背理论不够要知道工具怎么用。用AddressSanitizer最简单g -fsanitizeaddress -g main.cpp -o main ./main一旦越界、use-after-free、内存泄漏ASan会直接打印出错位置和调用栈定位效率远高于肉眼review。Valgrind也行但慢适合小规模复现。另一个常见问题是字节序尤其涉及网络传输和硬件寄存器时总是会考htons、htonl、ntohs这些转换函数。不要觉得只有嵌入式才关注大端小端AI芯片的推理服务跨平台部署时字节序不一致会导致解析出来的数据完全是错的。3. Linux并发与IO模型隐藏的送分题其实都在考调度3.1 进程、线程、协程的本质差异笔试里关于进程线程协程的题绝不能只答“进程开销大、线程开销小、协程更轻”。要能解释清楚为什么。进程有独立地址空间线程共享同一进程地址空间但有自己的栈和寄存器上下文协程则是用户态切换不涉及内核态所以切换成本更低。C20里的协程以及Go里的goroutine能支撑高并发本质上都是因为调度尽可能留在用户态。我见过一道很实际的题一个后端服务大量创建线程为什么性能反而下降因为线程创建要分配栈空间默认8MB虚拟内存线程切换要陷入内核加上锁竞争、缓存失效线程数一旦超过CPU核数太多系统时间占比会明显上升。对CPU密集型任务经验值大概设为核心数加1IO密集型任务可以设为核心数的两倍但最终要压测。回答时主动说出“具体得通过压测调整”比死记公式更让面试官认可。3.2 锁、条件变量与死锁的底层逻辑并发编程题很爱出一段生产者消费者代码让你补齐。核心是用互斥锁保护共享队列并用条件变量通知消费者而不是忙等。std::mutex mtx; std::condition_variable cv; std::queueint q; void produce(int v) { { std::lock_guardstd::mutex lk(mtx); q.push(v); } cv.notify_one(); } int consume() { std::unique_lockstd::mutex lk(mtx); cv.wait(lk, [] { return !q.empty(); }); int v q.front(); q.pop(); return v; }这里有个重要细节cv.wait为什么需要第二个参数因为存在“虚假唤醒”wait返回时队列不一定有数据必须再检查条件。条件变量一定要配合互斥锁使用而且判断条件必须放在锁内。死锁题则固定那套答法互斥、持有并等待、不可抢占、循环等待四个条件同时满足才会死锁。避免方法就是破坏其中一个比如固定加锁顺序、用std::lock一次锁多个、用带超时的锁。3.3 epoll与零拷贝高并发服务的性能底座高并发网络服务一定会问select/poll/epoll的区别。我的答法是select和poll本质是轮询连接数多了效率线性下降epoll是事件驱动内核只通知有事件发生的连接所以能支撑大量连接。还有一个常被忽略的点select有FD_SETSIZE限制poll没有数量限制但仍有性能瓶颈。笔试如果考epoll经常追问LT和ET模式的区别。LT是只要缓冲区有数据就会持续通知不容易漏事件ET是只有状态变化才通知所以必须循环读取直到read返回EAGAIN否则可能丢失剩余数据。写ET模式的读循环是高频题while (true) { ssize_t n read(fd, buf, sizeof(buf)); if (n -1 errno EAGAIN) break; if (n 0) break; // process buf }此外还有零拷贝相关的题比如发送文件时sendfile比readwrite少了哪几次拷贝简单说传统方式要把数据从磁盘拷到内核页缓存再从内核拷到用户态再从用户态拷到socket发送缓冲区最后到网卡sendfile让数据在内核态直接流转省掉两次用户态拷贝。mmap也有类似用途但要注意和页缓存的一致性。这些点如果能在笔试简答题里主动写出来会让你的答案明显和别人不一样。4. 网络、数据库与一致性题目是这样串起来的4.1 TCP状态机背后的高并发问题网络部分笔试一般不会只考握手几次挥手几次而是会把TCP状态和真实服务问题放在一起。比如服务端出现大量CLOSE_WAIT说明对端已经关闭连接但应用层没有调用close通常是代码里连接或文件句柄没有释放。大量TIME_WAIT则多出现在短连接场景原因是主动关闭连接的一方要等待2MSL。如果服务是短连接TIME_WAIT数量大不一定异常但可以通过开启SO_REUSEADDR、让客户端复用连接等方式优化。回答时最好说明TIME_WAIT是TCP可靠性的代价不是单纯可以消灭的东西。4.2 接口幂等与并发扣减后端设计题很爱考“怎么保证接口幂等”以及“怎么防止超卖”。这两个问题其实是同一类并发场景下状态一致性。幂等最简单的方法是用业务唯一键做数据库唯一约束重复请求插入时直接失败也可以利用状态机只有待支付状态才能变成已支付还可以用Redis分布式锁加去重标记。扣减库存时最稳妥的SQL写法是条件更新UPDATE inventory SET stock stock - 1 WHERE id ? AND stock 0;这条语句靠数据库行锁保证同一时刻只有一个事务能扣减成功stock 0这个条件直接防止库存变成负数。答这类题时不要只给方案要说明每个方案的适用场景。比如Redis分布式锁适合高并发但能容忍短暂不一致的场景数据库条件更新适合强一致场景两者可以结合使用。4.3 索引失效场景与执行计划分析数据库索引题除了背B树、聚簇索引、回表这些概念笔试更爱考“这条SQL为什么不走索引”。常见的失效原因有对索引列使用函数、隐式类型转换、like %xx、or连接了非索引列、索引列参与计算、不满足最左前缀。看到题目先不用急着背答案可以想一下为什么索引是B树按值排序的一旦对列套函数或不满足前缀匹配就无法利用有序性。实操中可以用EXPLAIN看执行计划。关注type字段从ALL变成ref或range就是优化生效关注rows字段扫描行数明显下降才算真正命中索引还要看Extra里有没有Using filesort或Using temporary这通常是性能杀手。分页深偏移也是高频考点limit 100000, 10这种写法会导致前面10万行全扫一遍优化方法是记录上一页最大ID下次查询用WHERE id last_id LIMIT 10配合二级索引能大幅减少扫描量。5. 算法题与系统设计题手撕代码之外的抢分策略5.1 算法题稳住哪几类就够了寒武纪后端笔试的算法题整体不是竞赛难度重点落在实现能力和边界条件。我建议优先稳住三类TopK问题、二分变形、区间问题。TopK可以用小根堆复杂度O(nlogk)也可以用快速选择平均O(n)。笔试时我建议写堆因为稳定、不易出错、代码量可控。二分变形的坑在于边界很多同学写while (l r)还是while (l r)全凭感觉最后要么死循环要么越界。我的习惯是先写清楚区间定义再套模板不要临时开脑洞。区间类比如合并区间、区间覆盖考察的是排序加贪心的基本功代码量不大但很能反映一个人写代码是否干净。如果时间有限把这三类题刷透再去碰动态规划基础题比漫无目的刷题效率高得多。5.2 系统设计题答题框架以训练任务调度为例系统设计题不一定要你写出完整代码但会考察你有没有全局思维。比如出一道“训练任务调度平台”我的固定答题顺序是先做需求分析明确功能需求提交任务、查询状态、取消、失败重试和核心非功能需求高可用、可扩展、任务不丢再做容量估算假设单机每秒能处理100次任务状态变更如果业务峰值1000 QPS就需要至少10台无状态服务节点然后设计API和存储结构任务表至少要有任务ID、状态、优先级、创建时间、重试次数状态流转用状态机约束最后考虑消息队列削峰、数据库分片、重试与幂等。这个框架可以套到短链服务、评论系统、文件上传服务等题目上。关键不是方案多酷而是每一层都有理有据。比如为什么用消息队列因为提交任务和实际调度之间需要解耦削峰填谷。为什么状态要记录重试次数因为分布式环境下面临机器宕机、网络超时不记录就无法判断该不该继续重试。5.3 边界条件与代码风格最后10%的分数往往在这里笔试编程题经常出现“代码思路对但用例过不了”的情况绝大多数是边界问题。空数组、空指针、单个元素、元素重复、负数、整数溢出、字符串为空这些都要在写完后主动检查一遍。C里特别注意int溢出求和或乘方先想会不会超过2^31-1必要时用long long。输入输出也别大意有的题目要求多组数据读完不加循环只处理第一个用例会直接挂掉。代码风格方面变量名要能看懂逻辑复杂的地方写一行注释函数别堆太长。面试官看笔试代码时最怕的不是你没写最优解而是逻辑乱得完全没法看。能完整跑通、普通但清晰的解通常比“看起来高级但没写完”的解法得分更高。6. 复盘与避坑如果重回考场我会多注意这几件事6.1 常见失误排行榜我见过太多同学栽在同一个地方选择题花太多时间。笔试二的选择题往往涉及很偏的语言细节比如volatile是否保证原子性、shared_ptr的线程安全性这类题想太久不划算。我的建议是单题不超过两分钟拿不准先标记最后再回来。另一个常见失误是编程题没有先想清楚再写。上来就敲代码写到一半发现样例不对又推翻重写浪费大量时间。至少先花两分钟想清楚输入范围、数据结构和复杂度再动键盘。还有一个隐蔽的坑答题时只写结论不写原因。比如问到“为什么map和unordered_map性能差异大”只写“红黑树和哈希表”是不够的要说出红黑树查询O(logn)、哈希平均O(1)、哈希需要hash函数和解决冲突、数据量小的时候哈希可能因为计算开销反而更慢。原因型答案才是高分的分水岭。6.2 我推荐的复习路线与资料如果备考时间有六到八周我会这样安排前两周主攻C书用C Primer重点看智能指针、内存管理、移动语义第三四周看深入理解计算机系统关注内存布局、异常控制流、虚拟内存、并发第五周看Unix环境高级编程里的进程、线程、IO模型第六周刷网络和数据库重点TCP状态、HTTP、索引优化最后两周用LeetCode分类刷题每天保持两到三题巩固TopK、二分、动态规划基础。这套路线的核心逻辑是先用底层知识把“为什么”讲清楚再用算法题保证手不懒。对于寒武纪这类公司的笔试系统知识的作用大于纯刷题但完全不刷题也不行毕竟编程题占了很大分值。6.3 一个很多人没想透的问题为什么这套笔试要这么出备考到后来我才想明白寒武纪后端笔试二想筛选的不是“会背知识点的人”而是“遇到线上问题能定位根因的人”。内存问题要看得懂地址和调用栈并发问题要知道锁和条件变量在底层怎么工作网络问题要能从TCP状态反推应用层逻辑。这也是为什么它不考框架、不考前端、不考八股背诵。所以大家复习的时候与其背几百道题不如多问自己一层“为什么”为什么shared_ptr引用计数是原子的为什么epoll用红黑树加就绪链表为什么数据库用B树而不是哈希索引想清楚这些不仅是为了过笔试也是为后面面试和真正做后端打底。希望这篇复盘能帮你少走点弯路把时间花在真正重要的地方。
返回列表