拖更了两个月,不知道是最近无事发生还是之前太能写了,这期间发生了很多事,现实与虚幻并存,度过目前的难关以后再慢慢吐槽吧。决定更新一篇工程开发经验的文章,无技术细节。
亿点点项目总结。大概是第一次接这么正规的项目,从代码要求、提交规范、开发流程、开发需求、测试流程到文档撰写,虽然其中有不尽人意的地方,但也算正规。按时间流程说吧。
明确需求
首先是明确需求,也就是说知道自己从开始到结束要干什么。开始很简单,配置环境,登陆远程服务器,clone 源代码;中间的开发过程由甲方提供,对我而言就是实现 sspaddmm
算子,并且通过单元测试和性能测试;在开发完成后,需要提交程序、撰写文档和完成适配,这也就是知道自己要干什么。
不尽人意的地方是:甲方说哪里不明白直接去 github new issue,我也照做了,可惜直到半个月后才回复我。反而直接微信找他聊天反馈更快,这样问题、分析和解答都是私有的,得不到积累和分享,不利于后来者的查阅以及项目的进展。
开发流程
这个对于乙方是重点,那么重点来谈一下。
关于读文档
配置环境、clone 源代码就不用说了,都是基础操作。重点是开发,按着文档一步一步来,先写哪个文件,在写哪个文件,不得不说,甲方文档写的好丑。
如果期间遇到不懂的疑问,可以翻阅文档,也可以查阅相关代码,但是建议查阅代码的 API,为什么呢?文档不是更好吗?我遇到的情况并不是的,众所周知,代码的更新速度是要比文档更新速度快的,代码写完就可以提交,文档要等代码写完后才能写,甚至懒得写,懒得写占大多数情况。程序员最讨厌的四件事:写文档、写注释、别人不写文档、别人不写注释。
这就导致了一个问题,文档滞后于程序,我甚至发现文档的描述和程序的功能不符,这会造成一定的时间浪费,具体表现在:用户按照文档写程序,结果写到一半发现错了,需要重写;用户不读文档,直接读代码 API 的难度系数又可想而知。
尽管难度系数大,但也要能读代码,为什么呢?还是回到懒这一话题,工程中很多 API 并没有记录在文档中,换句话说,文档中没有描述的操作,工程含有的 API 也许支持。因此遇到问题时,我们需要找到相关类的定义,可以粗糙的通过见名知意来了解函数的用途。
这一流程的确帮了我一个大忙。简而言之:我在程序实现的时候必须新开辟空间,修改传入的指针指向的地址,程序结束后,指针还在那里,但不能指向实现中开辟的空间,因为变量是被封装的,不是修改指针指向那么简单。困惑的时候,发现对应的累有 API 可以实现获取修改数据的地址,万事大吉。
关于实现
关于参考
而对于刚接手工程而言,以飞快的速度吃透原理、架构和各个类的各个功能是不现实的,毕竟代码过于庞大。那么这个时候就建议先看工程中有没有类似的实现,做一个参考。这会节省很多的时间,包括 API 阅读、实现逻辑和类设计这三个角度。
除此之外,还可以参考已有程序,避免造轮子。举个例子,sspaddmm
是要对标 torch.sspaddmm
的,那么就先可以参考 torch.sspaddmm
是如何实现的。借鉴前人千锤百炼的代码,我们能更好的出发。
关于开发
无论如何,都会回到具体的程序开发中。这个时候,我推荐的是迭代式开发,不要一次性做到尽善尽美,这样后面 debug 的压力会很大,没有任何一个程序员可以保证一次性写千行左右的程序而不报错,我自己说的。
这个时候写一个最小的程序版,感觉对即可。由于大型工程项目的编译和运行并不像平时点按钮就能运行那么简单。因此,在调试困难的情况下,写完最小程序版通过编译即可,不必关心程序运行结果的正确与否。与此同时,做好 info
的输出,也就是说,在执行各个子模块时,在前面加一句 std::cout << "run module X" << std::endl
,这样更方便定位到程序哪里出了问题。
关于测试
关于单元测试
在开发中,由于我们没有保证程序的逻辑正确性,这一点可以在测试时完成。因为程序上线之前必须经过严格的测试,测试样例尽可能广泛、极端,保证程序的行覆盖率。生成测试样例和期望输出后,就可以对自己写的程序正确与否进行校对了。
如果不对,确切而言,99.99% 的情况都是不对的。我是通过缩小单元测试样例的数量和数据量,比如只有一个测试样例,这个测试样例的数据很小,毕竟应该通过常见用例后再去测其他广泛的用例。在凭借 std::cout
一步一步的 debug 后,程序基本没问题了,其中的技术细节不在这里详谈,技术内容会单独写到其他文章中。这里需要注意的是,每次编译运行都比较耗时间,因此多加几个 std::cout
,之加一个每次 debug 一小段,太浪费时间。
单元测试通过后,就可以进行编译部署,将自己的程序部署到工程中。这个时候,我不建议提交代码,因为性能测试还会发现程序的问题,程序还会面临二次修改,这是其一;其二是:有些人自以为是贪功冒进,写一点代码就提交,也不管正确与否,这样别人在 pull 时会拉取到错误的代码,编译时不会通过,这种行为令人做呕。
关于性能测试
性能测试就不多说了,批量生成不同大小的数据,记录执行时间,与甲方要求的性能进行对比即可。我当时在这里遇到了很多问题,具体来说一下:
首先是甲方工程的报错信息不够人性化,说不支持 int 类型,不支持 int 类型你咋不上天呢?后来发现,是性能测试配置文件中有的内容由脚本生成、有的内容直接写到配置文件中导致的。由于数据量大,数据字段由脚本生成,类型字段我直接写到配置文件中了,也就是 int。所以报一个「不支持 int 类型的错误」,我不理解。别问我是怎么发现这个错误的,呵呵。
之后发现性能测试报错,其报错信息提示「不支持输入类型」,已经有了前车之鉴,我知道真正的错误不会是不支持数据类型,后来发现是:数据格式排列错误,比如 [2] 应该写成 [1, 2],这些是小错误,可以通过阅读代码来解决。那么大错误呢?比如直接告诉我运行超时,但不可能是运行超时,如何解决?
一个万能的方案是,看日志。其实写到这里,虽然吐槽甲方程序的缺点,但是能把自定义类型、类的设计、架构设计、各种极端情况的应对 API、单元测试模块、性能测试模块、整合并调用第三方库、报错信息提示和日志收集做的如此系统,虽然有瑕疵,但也可圈可点,这其中使用了多少设计模式,细思极恐。也不知道什么时候我才能有这么强的工程能力,成为一个工程的总设计师得多厉害。
回到看日志这一话题,我发现导致超时的原因是:core dumped,一个喜闻乐见的错误。再次回到实现部分的代码加上 std::cout
进行 debug。结果发现是开辟内存空间出错了,怀疑单元测试和性能测试用的链接库不是同一个。换成更加安全的内存开辟方法,bug 解决了。
但是发现性能比 pytorch
弱了不少,这个时候继续迭代开发。给程序增加多线程功能,读到这里,也许你能更好的明白:「为什么不要急着提交程序」,你以为的程序正确,但也只是你以为的,后期还有很大概率要完善和修改。修改完毕后,记得回头去执行单元测试,因为修改代码后很可能导致单元测试无法通过。
增加多线程的时候,回顾操作系统,多线程访问变量的弊端,因此很轻松能确定什么时候加多线程,什么时候不加。因为只能使用被封装的多线程库,不能自己手写,所以有些地方不方便加锁。至此,程序开发的东西告以结束,性能比 pytorch
快了两倍。前前后后花费了大约 20 天的时间,也收获了不少东西,做此记录。