工具的重要性

背景

最近在思考关于"技术无用论"的说法. 从事程序工作难免会遇到技术无用论的观点. 持有这样观点的人通常的思路如下: 比方说, 在一个公司里往往靠的是商务能力生存下去, 而技术往往是服务于商务. 而商务的变更就会直接影响到一个项目是否要推进下去. 直白点, 技术是商务的下游. 而上游决定下游, 如果上游不存在自然下游也不存在, 并且技术只是实现目的的一种手段, 而不是唯一. 因此技术不重要.

与其相似的是第二个观点是“工具无用论". 举个具体的例子, 很多人会认为学习vim或者emacs这种编辑器是浪费时间, 付出对应的学习成本是没有必要的. 或者折腾arch linux,window manager这些也是花里胡哨, 没什么实际用处. 类似的, 编程语言学习一两种就行了, 语言只是工具, 都差不多, 学会了一种再学其他的语言也很快. 毕竟还是需要面向“薪资”编程.

正文

要给出结论需要一些铺垫. 我相信提出上面观点的人不会怀疑“薪资”或者财富是无用的. 所有的人都想解决“财富”问题. 这里要明确一点就是. 财富不等于金钱,财富是金钱的载体. 如果社会流通金钱远远超过了载体的数量就会导致通货膨胀, 或者在某个具体的行业产生泡沫, 追求金钱是追求财富的副产物, 而财富还有其他名字, 它也叫做产品或者服务. 而通常意义上来讲, 工作就是指创造产品或者提供服务的时间. 提供产品可以不用金钱, 最好的例子某过于开源或者免费服务. 一个组织或者个人所提供的产品的影响力越大, 相应的, 组织也会获得越多的金钱. 组织中的个人也会获得更好的薪资待遇. 因此,金钱直接与影响力 挂钩. The Millionaire Fastlane: Crack the Code to Wealth and Live Rich for a Lifetime中有一个公式很清晰的表达了这个过程:

Wealth = Width * Depth

这里的Wealth我更倾向于解读成金钱. 金钱的数量取决于产品的影响力的广度(受众的个数)乘深度(用户粘性). 在这个过程中, 去提供对应的产品需要对应的工具, 程序员需要编程语言去创造软件, 雕塑家需要对应的工具去创建雕塑. 任何行业的专业人员都需要专业的工具的提供相应的产品.

那技术在这个链路里面指的是什么呢?

在中文语境下, 技术往往有两个指向:

  1. 技术指代的是工具本身, 即技术等于工具
  2. 技术指代的是创造工具的背景或者说基础知识

由于技术这个词同时有两个指代, 我认为有不同观点是第二个指代. 第一种指代是创造产品必需的, 好的技术能更好服务于产品. 从而创造更大的影响力. 而第二种指向对于使用工具的人来说很重要, 但对于创造产品没有直接影响. 从这个角度上讲, 第二种指向的技术确实没什么用. 所以我在这里想反驳的其实是第二种观点, 即“工具无用论”. 更具体的说是在软件开发工程中, 好的工具是如何改变思维的.

论工具的重要性

截止到2024年还无法完全使用AI相关的工具达到全流程闭环的今天, 我认为软件开发可以用一句话总结:"通过某种交互方式进行文本编辑, 按照规则(编程语言), 实现模型的过程称为软件开发". 所以这里面有三个因素:

  • 文本编辑
  • 规则(编程语言)
  • 模型

模型会根据不同行业而有所区分, 因此不具有通用性. 由于99.99%的人都无法创造 有生态以及有社区影响力的规则 , 因此都必须要理解规则, 按照规则进行开发. 而不管进行哪种软件开发, 都需要进行文本编辑. 熟练使用Vim以及Emacs都可以让编辑速度有质的提升, 加速了迭代速度, 从而让更多的开发时间花在模型的构建上. 同时能够熟练掌握Vim以及Emacs能够更好的帮助你理解规则. 但是Vim和Emacs的定位不一样, Vim更强调的是编辑本身, Vim配置也更多的是编辑本身的拓展, 而在Emacs里面任何操作都是一条命令, 通过Elisp去自定义命令达到预期的功能. 这里十分推荐Hacker news上的一篇Post, 然而在工作中真正花时间能够很熟练使用Vim的人已经寥寥无几, 使用Emacs的就更稀有了. 并不是每个学校都像mit会开The Missing Semester of Your CS Education这种课. 这也是国内大部分高校计算机教育缺少的一环, 我认为在大学里面花一个学期把Vim或者Emacs当成主修一点都不为过. 它们能够直接加深对计算机的理解.

Lisp与其他语言

不是专门研究编程语言或者折腾Emacs的话, 大部分人应该都不会接触Lisp相关的语言. 但Lisp实在是太独特了, 学习Lisp可以影响思维. 由于Lisp的dialect(方言)很多, 有lexical scoping, 而有dynamic scoping的. 但是精华部分都有一致性, 在Lisp中任何函数调用最终都是由eval去执行, 任何数据都是Data, 函数是Data. 并且几乎没有语法. 只有一种数据结构就是list, 而list只有两个部分构成, car和cdr. 通过操作car(contents of address part of a register),cdr(contents of the decrement part of a register). Lisp就像GEB里面说的strange loop的完备符号形式, 一旦了解了lisp的基本结构, 会发现没有任何语言能像它一样简洁. 如果让我重新学习编程, 我肯定会先选择这本1990年出版的Common Lisp书籍作为学习编程的第一本教材.

2023年难忘的书

新年快乐

2024到了, 瑞兔辞旧去,龙腾新年来, 祝新的一年事事顺心. 这里分享几本在2023年读过难忘的书.

书单

  1. The Inner Game Of Tennis: The Classic Guide To The Mental Side Of Peak Performance: 关于调整心态能让自己有最佳发挥的书. 作者以网球比赛举例子但实际上任何事都会有心态的参与. 首先, Relaxed Concentration是形容心态的最合适的水平. 注意力完全集中但是身体却是很放松的. 要达到这种水平, 有两个影响因素Self 1(后天训练得到的心态)以及Self 2(先天的心态). 不要让Self 2与Self 1分割, 并且不要盲目的依赖于Self 1. 培养nonjudgmental awareness. 停止评判Self 2. 客观的看待事物而不加修饰心态. 用身体和脑一起去学习, 让Self 1描绘出最好的结果, 然后配合Self 2一起去实现.

  2. 9 Out of 10 Climbers Make the Same Mistakes: Navigation Through the Maze of Advice for the Self-coached Climber : 关于学习技巧的书. 如果不是为了追求如何提高登山技巧的话, 读完第一章即可. 书中有句话说的太好不得不引用原文:

climbers are stuck on the basics, but lost in the details. - p7

climbers可以是要完成/精进任何一项事的我们大概率会犯的错 - 在信息爆炸的今天, 明明对于基础以及大框架的忽视, 但是缺扎在了细节海洋里面. 反思与总结才能理清楚什么是真正基础并且重要的. 此外, 减少百分之四的努力不会减少百分之四的产出, 减少百分之四的努力会减少百分之九十的产出.

  1. The 12 Week Year: Get More Done in 12 Weeks than Others Do in 12 Months: 如标题所示, 一本关于方法论的书. 要达到这本书所描述的效果, 前提是相信only actions lead results. 接下来这本书会指导如何action去达到results:
  1. 持续的action在重要的task上面.
  2. 减短反馈周期 (把一年定义为12 weeks而不是12 months)
  3. 制定明确并且可行动的目标 (首先是制定12 weeks的目标,之后划分到以周为行动单位)
  4. 衡量完成度并且打分 大体上就是缩短反馈周期, 主旨就是这样但是书中有很多操作细节.
  1. The Almanack of Naval Ravikant: A Guide to Wealth and Happiness: 第二次读了,绝对是一本每年值得读一本的书. 今年计划再读一次. 每一次读都有不一样的收获. 这本书覆盖面太广了,总而言之,这本书回答的是如何过更幸福快乐的人生. 是视频How to Get Rich内容整理成书.Naval是个值得深挖的人. 他的谈话和见解总是能直接了当的阐明本质, 并且能与实践有很直接的关联. Absolutely pure gold.

  2. 寻觅意义: 王教授在复旦上课的讲话总结. 相比于注重操作的西方哲学,国学往往被认为不具有实践意义或者说只是教化人的工具. 王教授解释了儒释道三家的实践意义. 无心而为,无为而无不为. 这本书肯定能带来一丝心灵的平和与宁静.

记录一次对v4l2相机驱动的系统优化

背景

最近有个需求需要重新优化供应商提供的相机驱动的代码, 来实现更低的cpu占用率以及更好的实时性. 同时要发布的消息里的时间戳是相机曝光时间戳并且是以unix时间戳为基准. 平台为Jetson Orin. 需要做localhost ipc来让下游应用使用. 相机原始默认编码是yuyv, 需要转换成bgr发送.

之前从来没做过linux驱动之类的开发, 也没研究过图像转码, 有因为别的需求看过旧版相机代码. 整个任务自己从调研到实现大概花了两周时间, 其中一周卡在了视频编解码上面. 最后项目上面也没应用上. 因此开源了这个仓库, 算是给近期比较有意思需求的一个方案. 这篇文章将一步步的介绍并解决这个问题.

基于V4l2的相机驱动实现流程

首先, 旧版的相机驱动是基于v4l2的. v4l2就是linux提供的一系列api. 基于v4l2开发的驱动也是有一个比较固定的流程的. 大体如下:

1. 打开相机获得句柄
2. 准备好v4l2_buffer并且配置v4l2_buffer
3. 等待句柄事件触发
4. 事件触发后访问v4l2_buffer存的就是准备好的相机buffer(v4l2_struct) 

此外相机是在硬件buffer里获取, 需要让用户态程序能够访问有三种方式dma, mmap和userptr. 我们希望尽可能低的cpu使用率因此选择dma. 因为其他两种方式相比, 从用户态程序操作buffer是直接对硬件buffer做操作 .而mmap和userptr都是对虚拟地址做操作的, 因此视频转码之后需要多一步I/O拷贝的硬件buffer.

了解了相机驱动大体流程之后, 首先先是尝试了v4l2_open()open(), v4l2_ioctl()ioctl()这些系统api的替换, 观察是否能有优化的效果. 经过一番尝试后发送如果换成v4l2_open()都无法打开用open()能打开的句柄.

图像转码

因为我们的平台是Jetson Orin. 自然会去想使用Nvidia提供的一些硬件视频编解码或者其他工具链路来完成, 比较直观的想法是在gpu上做yuyv到bgr的转换, 或者能直接在硬件buffer里面完成转码肯定是最好的. 所以先尝试了能否在硬件上直接做转码. 就开始了解Nv的工具链, Jetson Pack等等. 当然也会在Nv的论坛上看别人的post, 最开始看回答的时候也有些云里雾里的. 还有很多时候明明也没有解决方案,Nv也会把帖子标为solved. 当然这里不会过多论述踩坑的过程, 比较有帮助是下面这张图, 这张图展示了Nv的生态工具链:

nvtoolkts

ok, 明确了大体上Nv所提供的工具链之后, 接下来解决这一步的问题: 原始相机吐出来的格式是YUYV要转换成BGR24, 在这一过程要达到性能最优.

根据上面的需求尝试了以下几个方案:

  1. 基于硬件的编解码,基于JetPack 5使用nvbuf_utils的NvBufSurface来存储相机原始数据buffer, 再使用NvBufSurfaceTransform
  2. 基于cuda做手动的转码,手写kernel. 根据这个公式, 一组YUYV可以转成成2个BGR像素. 以及jetson_multimedia_api的samples里面的转rgb的例子.
  3. 使用cuda加速版的opencv的cvtColor
  4. 使用Npp来做处理

尝试之后发现: 方案1: 硬件只能转换成BGRA, 因此要从BGRA转换成BGR的话. 不可避免的就需要在用户态开辟buffer做转换. 过程比较繁琐. 方案3: 性能相比于方案2要差, 猜测原因是cuda opencv不能自定义threads和block的比例. 方案4: 有奇怪的bug. 转出来的图片不符合预期,也没有论坛post去讨论这个问题. 因时间原因, 解不了只好放弃.

这一步最终选择了方案二. 使用CUDA Zero Copy Mapped Memory(host mem与device mem映射), 然后使用核函数去做转换.

时间戳对齐

相机驱动输出的结构体是v4l2_struct. 而v4l2_struct带的timestamp是相对时间戳(从系统上电开始计时)并且是Start Of Frame(即buffer里面第一个字节有数据时的时间),而需要转换到unix时间戳。那如何做转换呢? 通过clock_gettime函数里面, 传入两个timespec.一个为realtime另一个为monotonic. 再做差值就是两个尺度原点的差值. 此外, 还需要加RTCPU与CPU时间戳的偏移量(见链接):

t0 = adjusted_sof = TSC - offset_ns

TSC即下图的t1, 其中offset_ns代表latency.

sof-latency-path

再补偿回这个偏移量就可以得到尺度一样的时间戳. 如果是在Nv的板上的话,偏移量会存放在/sys/devices/system/clocksource/clocksource0/offset_ns这个路径下. 加上了这个偏移量那输出的就是monotonic_time.

本机IPC(Inter Process Communication)

在编解码这一步我们使用了cuda开辟了gpu内存, 所以如果能基于cuda_ipc的话应该是最优解(没有多余的拷贝,客户端应用程序可以直接指向gpu buffer). 但是使用cuda-ipc的话就不能使用device和host memory的映射, 必须要cudaMemcpy. 并且我们希望cpu应用端也能直接拿. 接下来就考虑使用shared memory或者传dma_fd. 理论上传dma_fd的性能更高, 但是也依赖Nv的JetPack, 而刚好我们的JetPack版本在2023年12月这个时间点还不支持共享NvBufSurface. 如果以后支持了, 使用NvBufferTransformEx()类似这样的api应该是最优的做法. 还有, 可以用eglstream去做IPC, 比较麻烦的是每多一个订阅者就要改一次代码(开通channel). 因此也排除了基于Nv的生态做IPC.

现在考虑使用shared memory去做这件事. 有一个问题就是在应用端(驱动)去往内存块里写图片的时候, 客户端需要用进程锁. 如果这时候同步机制做的不好的话, 就很容易出问题. 一开始尝试使用boost的interprocess读写锁, 但是这里还是有内核态到用户态之间的切换. 基于纯用户态的话, 使用信号量做触发机制. 这样做是可行的, 但是对客户端接受编写接受图像代码逻辑有要求. 但还是有个问题就是这套机制没办法自定义QOS, 并且只限定于这种场景.

于是最终转向了Iceoryx. 因为这也是现有的基于shared memory(c++)最成熟的中间件. 相比于手写shared memory, 基于Iceoryx的通用性和可复用程序更高, 更有利于后续其他模块满足迁移通信机制的需求.

结果

做完上述处理过后的驱动相比于供应商提供给我们的驱动在cpu使用率在JetSon Orin上降低了10%左右. 如果你也有类似的问题, 参考这个仓库或许对你有所帮助.

总结

整个过程有以下几点总结:

  1. Nv的很多资料散落在论坛里面,需要多个post才能拼成一个完整的信息源.
  2. 问题最快的解决方式是直接让供应商找人去改. 能有时间和精力去研究这些当然好, 写完这篇文章或许能证明自己的一些学习能力. 但是所有问题的最快解决方案始终是找到对应的人. 因为很多问题遇到一次基本上也不会遇到第二次了, 很难说能从获得系统性的思考能力.
  3. 如果在现有的Nv板上在官方文档找不到解决问题的资料, 不妨往以前版本的Nv板的官方资料上面找.

参考资料

以下是在这过程中比较有帮助的参考资料:

  • https://developer.ridgerun.com/wiki/index.php/NVIDIA_Jetson_TX2_-_Video_Input_Timing_Concepts
  • https://forums.developer.nvidia.com/t/interprocess-zero-copy-image-transfer/231623
  • https://on-demand.gputechconf.com/gtc/2016/webinar/getting-started-jetpack-camera-api.pdf