ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

商汤校招代码优化岗笔试复盘:从X86 SIMD到ARM NEON

商汤校招代码优化岗笔试复盘:从X86 SIMD到ARM NEON 1. 笔试开场商汤校招代码优化岗到底在考什么2018年那会儿AI公司招代码优化工程师还是个挺新鲜的事。商汤作为计算机视觉头部公司模型要在各种设备上跑出实时性能训练好的神经网络不能只在GPU上炫技还得塞进手机、摄像头、边缘盒子这些资源受限的家伙里。这时候就需要专门的人来做底层优化把算法从“能跑”变成“跑得快”甚至“跑得飞快”。我印象里这一年的校招笔试分了不止一场第二场相比第一场更侧重动手细节和体系结构功底X86和ARM两边的题目都覆盖了纯属那种“懂的人觉得不难不懂的人无从下手”的风格。第二场的定位很明确筛选具备体系结构素养、能看懂汇编级代码、对性能敏感的人。整张卷子不会让你写一个完整项目而是把优化工作里最常见的场景拆成一道道小题目比如“给一段循环换成SIMD指令”“分析cache miss的来源”“手写ARM NEON intrinsic完成一个图像滤波”等等。表面上考的是指令集和优化技巧实际上在验证你有没有真正做过性能调优而不只是背过概念。对准备这类笔试的人我的建议是先搞清楚岗位性质它不是普通的后端开发也不是算法岗而是处在算法和硬件之间的桥梁型角色。你得懂一点算法原理知道每行算子在做什么同时要对CPU的流水线、内存层次、编译器行为有肌肉记忆式的理解。商汤这类AI公司的代码优化岗日常工作里很大一部分就是计算视觉算子的优化比如卷积、池化、归一化、仿射变换这些所以笔试题目也会往这个方向靠不会出太偏的领域题。后面我会拆开来讲每个考点对应的是什么能力以及答题时应该怎么组织思路。2. X86体系下的优化题从SIMD到内存布局2.1 SSE/AVX指令集的核心考法X86这一块的笔试内容基本绕不开SIMD。2018年的时候AVX2已经普及AVX-512在服务器上也出现了但校招题不会一上来就考512位寄存器反而会把SSE、AVX的常用操作作为基础再延伸出一些需要动脑的场景。第二场笔试里就有类似“给定一个float数组计算所有元素之和要求用SIMD优化”的题目。看起来简单但注意点很多。拿AVX2的256位寄存器来说一条指令可以同时处理8个float。常规的写法是把数组分成主力循环和尾数处理两部分主力循环每次加载8个float用_mm256_loadu_ps做非对齐加载然后_mm256_add_ps累加到一个向量累加器里最后在循环结束后做横向归约也就是把向量里的8个数加到一起。这里有个关键细节归约不能直接暴力相加而是利用_mm256_hadd_ps加两次或者用_mm256_permute2f128_ps把高低128位折半归并再提取标量。我见过不少人在笔试里代码写得对但是忽略了对齐问题。_mm256_load_ps要求地址32字节对齐而_mm256_loadu_ps允许任意对齐代价是可能慢一点。笔试阅卷时面试官会看你的代码能不能在真实机器上跑出效果如果出现对齐异常或者段错误这题基本就废了。正确做法是先用posix_memalign或者C11的alignas(32)分配对齐缓冲区然后可以用对齐加载版本。还有配了FMA指令集的机器可以把乘法和加法合并成一条_mm256_fmadd_ps这在卷积算子里特别实用。比如计算y a * b c如果分别用_mm256_mul_ps和_mm256_add_ps那就是两条指令而FMA只需要一条延迟也更低。笔试里如果出现“两帧图像逐像素做alpha混合”这类题目用FMA写出来的代码明显比分开写更漂亮还能体现你对现代CPU指令集的了解。2.2 内存连续性与cache命中一道矩阵题第二场笔试里有一道让我印象深刻的矩阵题目给定一个二维数组按行优先存储要统计每列元素的和问哪种遍历方式更快。很多人第一反应是“差不多”但实际上按列遍历会产生严重的cache miss因为每次访问一个元素都要跨越整行cache line的利用率极低。一个典型的x86 cache line是64字节也就是16个float的大小。如果矩阵是1024x1024的float数组按行遍历时加载一个cache line能连续命中16个元素按列遍历时每次只取到1个元素剩下15个都是无效加载相当于cache的有效利用率只有6.25%。这种差距在数据量大的时候可以达到几十倍笔试题目如果你只回答“快”而不说理由分数会打折扣。要答得完整需要明确解释局部性原理最好还能说清cache line大小和矩阵元素大小之间的关系。这道题让我意识到代码优化笔试其实在考察你是否理解“内存是新的磁盘”这句话。现代CPU算力增长远快于内存带宽增长很多时候瓶颈根本不在计算单元而在数据搬运。答题时如果有余力可以补充分块tiling优化的思路把矩阵分成适合cache大小的块比如8x8或16x16的小块让数据在加载后能被多次复用。这也是后面做卷积极致优化时的基本功。2.3 编译器优化选项与限制X86部分还有一类题不写一行代码而是问“给一段代码用gcc -O2和-O3分别编译性能有什么区别”。这类题考察的是你对编译器优化行为的理解。-O2已经包含了大部分安全的优化比如指令调度、公共子表达式消除、循环不变代码外提-O3会额外开启向量化、函数内联等更激进的优化但副作用是代码体积增大、编译时间变长个别情况下甚至可能因为过度unroll导致指令缓存miss性能反而不升。笔试里如果题目是“写出你认为优化效果最好且可控性最强的编译器选项组合”不要只写-O3。靠谱的答案是先开-O2加上-marchnative让编译器根据本机CPU特性启用对应的指令集再根据瓶颈决定是否开-funroll-loops或-flto。这里面有个很重要的经验不要盲目相信编译器自动向量化它常常因为指针别名问题alias无法自动向量化比如循环里同时操作两个指针编译器不能确定它们是否指向同一块内存于是放弃向量化。解决办法是使用__restrict__关键字告诉编译器“这两个指针不会重叠”或者手动用intrinsic重写。这一块如果笔试时间充裕建议把“为什么编译器不敢自动优化”的原因写出来而不是只给结论。面试官特别看重你是不是真的踩过编译器优化失效的坑。3. ARM与交叉编译嵌入式场景的隐形考点3.1 NEON内联汇编与intrinsic的答题思路商汤当年的业务覆盖手机端、安防摄像头、智能门禁这些设备的CPU大多是ARM架构。所以笔试出现ARM相关的题一点不意外。ARM平台的优化主要靠NEON指令集它和X86的SIMD理念类似但寄存器宽度和指令组织方式不同。NEON有32个128位寄存器可以用vaddq_f32这样的intrinsic函数完成向量加法也可以用内联汇编直接写但笔试一般不要求你手写汇编能正确使用intrinsic、并说明为什么能加速就足够了。我记得第二场笔试有一道题是“用NEON实现一个5x5的高斯滤波”。这个题目有两层意图第一看你知不知道高斯滤波本质是一个可分离卷积可以分解成两次一维卷积大幅减少计算量第二看你能否将卷积操作映射到NEON指令上。先说第一层5x5滤波直接做是每个像素25次乘加分离后是横向5次加纵向5次共10次乘加计算量直接降到原来的40%。第二层横向滤波可以一次处理8个像素每个元素加载相邻的5个像素做点积NEON的vmlaq_f32指令一条就能完成8路浮点乘加效率极高。答题时还需要注明数据类型的对齐要求比如float32x4_t最好放在16字节对齐的地址上否则要用vld1q_f32非对齐加载或提前做内存拷贝。这类细节在笔试卷面上很加分说明你不是只背了intrinsic API而是真正在ARM开发板上跑过。3.2 交叉编译环境笔试中的环境题ARM笔试部分往往还有一个环境题比如“请说明如何为一个ARM嵌入式设备搭建交叉编译环境并编译一个使用NEON的C程序”。这题看似工程化实质上在考察你对交叉编译工具链的熟悉程度。标准流程是先确认目标设备的架构是armv7还是armv8-aAArch64比如树莓派3B及以后的板子是armv8。然后选择对应的交叉编译器常见的是gcc-arm-linux-gnueabihf针对32位ARM硬浮点或aarch64-linux-gnu-gcc针对64位ARM。编译时用-marcharmv8-asimd显式开启NEON扩展或者在更老的工具链上用-mfpuneon。代码里包含#include arm_neon.h头文件然后写intrinsic。编译出的二进制不能直接在本机运行要用file命令检查架构再拷贝到目标设备或模拟器里执行。很多考生会忽略工具链和系统库的匹配问题。如果你的目标系统是精简嵌入式Linux没有标准的glibc运行库那么需要选择-static静态链接把依赖全部打包进二进制里如果是Buildroot/Yocto构建的完整系统就可以动态链接。另外交叉编译的sysroot参数很重要头文件路径和库路径都要指向目标系统对应的目录否则编译期就会报找不到stdio.h这类低级错误。我在实际工作中踩过一两次这种坑都是在写cmake文件时没有配置好CMAKE_SYSROOT导致的笔试里把这层逻辑说清楚分数会明显不一样。3.3 大小端、对齐与ABI细节ARM部分的开放性题目还可能考到大小端问题。X86是典型的小端little-endianARM则两者都支持Android和Linux默认也是小端。但有些嵌入式设备会配置成大端运行比如某些网络处理器。如果笔试问你“将一串颜色数据从大端设备通过网络传输到小端设备如何高效转换”你需要说明字节序转换的必要性同时指出SIMD优化同样适用于字节重排。另一个容易丢分的点是ARM EABI的栈对齐规则。AAPCSARM Architecture Procedure Call Standard要求函数调用时栈指针16字节对齐这在NEON代码里尤其重要因为vst1q_f32这类存指令要求地址16字节对齐。如果你在嵌入式代码里用了NEON存储但没有注意对齐运行时会出现SIGBUS异常。笔试里补一句“用memalign或aligned_alloc分配16/32字节对齐的内存”就能显示出实际开发的素养。ABI层面的知识还包括ARM硬浮点和软浮点ABI的区别。硬浮点用vfpv3或neon寄存器传参软浮点则用通用寄存器传。两者编译出来的二进制不能混用链接时会报“uses VFP register arguments”之类的错误。商汤这种主打端侧AI的公司面试官问这个细节本质上是在试探你是否关心端侧模型部署的真实链路。4. 性能分析与调优工程题不止是算得快4.1 cache miss、分支预测与伪共享第三类高频题是性能分析。笔试给人一种“代码优化就是写快代码”的错觉但真正拉开差距的是你遇到性能问题后能不能定位瓶颈。第二场里有一道题给出一段多线程累加的代码问为什么两个线程各自更新的变量放到同一个cache line里会导致性能骤降。这就是经典的假共享false sharing问题。假共享的原理是多核CPU中每个核有私有的L1/L2 cache但缓存一致性的最小单位是cache line。线程A和线程B各自写一个int变量如果这两个变量刚好落在同一个64字节cache line里那么A的写操作会让B核上的对应cache line失效B再写时又要重新同步两个线程之间互相拖后腿性能可能比单线程还差。解决办法是让两个变量分布在不同的cache line上比如用alignas(64)或者给结构体填充字节分隔开。这道题还有一个延伸方向如何用perf stat这类工具发现它。工作中我经常先跑一遍perf stat ./program看cache-misses和context-switches的数值如果cache miss率异常高就会用perf record -e cache-misses生成火焰图或者call graph定位到具体函数对照源码找出是循环方向不对、结构体布局不合理还是假共享。笔试答题时如果能把这个排查流程写下来会比只干巴巴地回答“加padding”显得有经验得多。4.2 基准测试与profiling工具的使用还有一类题会问“你如何判断一种优化是否有意义”。很多人会说“前后对比时间”但这个答案太粗糙。正确的做法是控制变量、多次测量、统计置信区间。比如用clock_gettime(CLOCK_MONOTONIC)或者std::chrono::high_resolution_clock计时优化前后各跑100次取中位数或者P95而不是平均值因为平均容易受系统噪声影响。更进一步还需要排除外部干扰。优化代码时CPU频率可能会因负载升高而动态调节turbo boost导致测试环境不稳定。所以专业做法是在跑基准前绑定CPU核心、关闭自动调频Linux下可以用taskset -c 2固定在某一个核上再配合cpupower frequency-set --governor performance固定到最高频率。这些细节在真实调优里救了我很多次笔试里如果能答出来面试官会觉得你是干过活的。在工具层面X86下常用的profiling工具是Linux perf、Intel VTuneARM下也可以使用perf只是部分硬件计数器可能不可用。编译时记得加-g保留调试信息-fno-omit-frame-pointer保证调用栈可回溯否则perf记录的符号可能全是问号。这些问题我在第一次用perf时都踩过后来形成了一套固定的编译开关模板笔试相关题目我也建议按这个思路回答。4.3 多线程优化与负载均衡性能分析题还喜欢考多线程。比如给定一个并行for循环问如何分配任务让负载均衡。最朴素的方式是连续均分——把N个任务按索引平均分给M个线程但如果每个任务耗时差别很大可能出现某个线程干很久其他线程空闲等待。更好的方式是动态调度OpenMP的schedule(dynamic, chunk_size)就是典型的解法线程空闲时从任务队列里取下一块任务。另一个常见的并行优化坑是锁竞争。代码里如果每个线程都用#pragma omp critical保护累加操作那么串行化程度会高得可怕性能还不如单线程。正确做法是每个线程维护私有累加器结束后再用原子操作或归约合并。这也是我在笔试中看到的高频考点因为实际问题里并行求和实在太常见了。回答这类题时建议把“先私有化后归约”的范式写清楚。5. 备考路线从这场笔试看代码优化岗的能力模型5.1 知识体系搭建清单回过头来看商汤这场第二场笔试对知识面的考察是体系化的。先把这个岗位需要的东西梳理成一份清单按优先级排一下第一梯队是X86 SIMD指令集SSE/AVX和ARM NEON的常用操作这是笔试的重头戏第二梯队是内存层次结构、cache优化、对齐、大小端等计算机体系结构知识第三梯队是编译器原理特别是优化选项和向量化限制第四梯队是工具使用包括交叉编译、perf、基准测试方法。如果你还在校想往这个方向走我建议优先把计算机组成原理和编译原理这两门课学扎实然后开始用真实的板子或者QEMU模拟器做实验。没有条件买ARM开发板的话可以用QEMU跑一个ARM的系统镜像或者在云平台上申请ARM的实例现在已经很便宜了。X86的SIMD优化相对简单直接在个人电脑上就能验证效果我当年就是拿一台笔记本反复跑矩阵乘法、滤波算子一点点感受指令集带来的差异。5.2 实操训练在真实环境中做实验许多人备考时容易陷入“看资料很多动手很少”的状态。代码优化是一个极度依赖实感的技能你光知道_mm256_fmadd_ps这条指令还不够得亲手写一个卷积算子把它从200毫秒优化到20毫秒再优化到5毫秒在这个过程中体会cache分块、寄存器重排、指令级并行的作用。我建议找几个经典项目练手第一个是实现一个高性能的sgemm矩阵乘法参考OpenBLAS的思路但自己从头写第二个是用NEON实现一个rgb转灰度或caffe风格的数据层第三个是把一个多层for循环的朴素算子逐步优化记录每一步的耗时和瓶颈变化。做完这三个练习你会对“优化”这个词有脱胎换骨的理解。5.3 答题策略与心法最后聊聊笔试现场的答题策略。这类笔试时间往往紧张题量又大需要抓大放小。我的经验是拿到卷子先花一两分钟把题目全看一遍标记出哪些是硬核代码题哪些是原理说明题哪些是开放性设计题。优先做自己最熟的那部分保证拿到确定性的分数再回头啃硬骨头。代码题即使时间不够也要把思路写下来用伪代码说明“这里用SIMD、那里做分块”面试官看的是你的思路是否清晰而不是是否能满分通过。关于要不要写汇编我的建议是如果你不熟别硬写。Neon或AVX的内联汇编出错的概率很大一旦写错代码直接跑不起来反而影响了其他题目的时间。用intrinsic就已经能拿到大部分分数除非题目明确要求手写汇编否则不要冒险。不过你要能读懂简单的汇编——比如看到vmulps知道是向量乘法看到vmlaq.f32知道是向量乘加这种读懂能力比亲写能力更常被考察到。备考期间还有一个容易被忽略的点多关注AI芯片和端侧推理引擎的开源实现。像NCNN、MNN、TNN这些推理框架底层有大量X86和ARM优化代码读这些源码比刷任何笔试题都管用。我当时备考商汤时把NCNN里几个核心算子的实现反复看了好几遍后来笔试遇到类似的卷积优化题几乎就是直接套用了那个思路。这件事让我意识到最好的备考资料不是面经而是真实工业级代码里的优化智慧。回到这场2018年的笔试虽然时间过去了一些但底层优化的核心素养没有变理解硬件、理解编译器、理解数据流然后动手验证。希望这篇复盘能帮到正在准备同类岗位的你少走一些我走过的弯路。
返回列表