如何记录和教导其他人“无法识别的优化”计算密集型代码?


11

有时候,有1%的代码需要足够的计算强度,因此需要最重的底层优化。通常,示例是视频处理,图像处理和各种信号处理。

我们的目标是记录文档并教授优化技术,以使代码不会变得难以维护,并易于被新的开发人员删除。(*)

(*)尽管在某些不可预见的未来CPU中特定优化可能完全没有用,所以无论如何都会删除代码。

考虑到软件产品(商业或开放源代码)通过拥有最快的代码并利用最新的CPU架构来保持竞争优势,软件作者通常需要调整其代码以使其运行得更快,同时为特定产品获得相同的输出。任务,允许少量舍入错误。

通常,软件编写者可以保留函数的多个版本,作为每次进行的优化/算法重写的文档。一个人如何使这些版本可供其他人学习其优化技术?

有关:


1
您可以将不同版本保留在代码中,注释掉,并带有大量注释来告诉读者发生了什么。
Mike Dunlavey

1
并且不仅要告诉他们代码在做什么,还要告诉他们为什么这样更快。如果需要,包括自己的,Wiki之类的文档的链接或互联网上可用的文档(在这种情况下,请注意链接腐烂,将其复制到自己的文档系统中并附上原始链接可能是明智的选择) 。)
Marjan Venema

1
@MikeDunlavey:哦,不要评论它。只需具有相同功能的几种实现,然后调用最快的一种即可。这样,您可以轻松地切换到不同版本的代码,并对所有代码进行基准测试。
sleske 2011年

2
@sleske有时仅仅拥有更多的二进制代码可能会使它变慢。
quant_dev

@quant_dev:是的,有可能发生。我只是认为定期(理想地)构建和运行代码以使其保持最新非常重要。也许仅在调试模式下构建它。
sleske 2011年

Answers:


10

简短答案

将优化保留在本地,使其清晰可见,很好地进行文档记录,并可以轻松地就源代码和运行时性能而言,将优化版本彼此之间以及未优化版本之间进行比较。

完整答案

如果这样的优化真的那么重要你的产品,那么你需要知道,不仅为什么之前的优化是有用的,但也能帮助开发者提供足够的信息,知道他们是否会在将来有用。

理想情况下,您需要将性能测试纳入构建过程中,以便找出新技术何时使旧的优化无效。

记得:

程序优化的第一条规则:不要这样做。

程序优化的第二条规则(仅适用于专家!):还不要这样做。”

-迈克尔·杰克逊

为了知道现在是否是时候,需要进行基准测试。

正如您所提到的,高度优化的代码的最大问题是难以维护,因此,您需要尽可能地将优化部分与未优化部分分开。无论是通过编译时链接,运行时虚拟函数调用还是两者之间的操作都无关紧要。重要的是,在运行测试时,您希望能够针对当前感兴趣的所有版本进行测试。

我倾向于以这样一种方式构建系统,即可以始终使用生产代码的基本未优化版本来理解代码的意图,然后与之一起构建不同的优化模块,其中包含一个或多个优化版本,无论在何处都明确记录优化版本与基准不同。运行测试(单元和集成)时,可以在未优化的版本所有当前优化的模块上运行它。

例如,假设您具有快速傅立叶变换功能。也许您在中有一个基本的算法实现fft.c并在中进行了测试fft_tests.c

然后是奔腾,您决定fft_mmx.c使用MMX指令实现定点版本。后来奔腾3来临时,你决定添加它使用一个版本的SIMD流指令扩展fft_sse.c

现在,您想添加CUDA,所以添加fft_cuda.c,但是发现您使用了多年的测试数据集,CUDA版本比SSE版本慢!您进行了一些分析,最终添加了一个100倍大的数据集,并且可以达到预期的速度,但是现在您知道使用CUDA版本的设置时间非常重要,对于较小的数据集,您应该使用算法,无需支付设置费用。

在每种情况下,您都实现相同的算法,所有算法都应以相同的方式运行,但在不同的体系结构上运行效率和速度将有所不同(如果它们将完全运行)。但是,从代码角度来看,您可以比较任何一对源文件,以找出为什么以不同的方式实现同​​一接口的原因,通常,最简单的方法是引用未优化的原始版本。

对于OOP实现,所有这些都是相同的,其中实现未优化算法的基类和派生类实现不同的优化。

最重要的是保持了相同的东西都是一样的,这样的差异是显而易见的


7

具体来说,由于您以视频和图像处理为例,因此可以将代码保留为同一版本的一部分,但根据上下文可以激活或不激活。

虽然您没有提到,但我在C这里假设。

C代码中最简单的方法就是进行优化(并且在尝试使事物具有可移植性时也适用)是保持

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

当您#define OPTIMIZATION_XYZ_ENABLE在Makefile中进行编译时启用时,一切都会正常进行。

通常,当优化的函数过多时,在函数中间切几行代码可能会变得混乱。因此,在这种情况下,可以定义不同的函数指针来执行特定功能。

主代码总是通过函数指针执行,例如


   codec->computed_idct(blocks); 

但是函数指针是根据示例类型定义的(例如,此处的idct函数针对不同的CPU体系结构进行了优化。



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

您应该看到libjpeg代码和libmpeg2代码,并且对于此类技术可能是ffmpeg


6

作为一名研究人员,我最终编写了很多“瓶颈”代码。但是,一旦将其投入生产,将其集成到产品中并提供后续支持的责任就落在了开发人员身上。您可以想象,最清楚地传达程序应该运行的方式和方式至关重要。

我发现成功完成此步骤需要三个基本要素

  1. 使用的算法必须绝对清楚。
  2. 每行实施的目的必须明确。
  3. 必须尽快找出与预期结果的偏差。

第一步,我总是写一个简短的白皮书来记录算法。此处的目的是实际编写它,以便其他人仅使用白皮书即可从头实现它。如果它是众所周知的已发布算法,则足以提供参考并重复关键方程式。如果是原创作品,则需要更加明确。这会告诉你什么样的代码是应该做的

交付给开发人员的实际实现必须以这样的方式记录在案:使所有细微之处都变得清晰明了。如果您以特定顺序获取锁以避免死锁,请添加注释。如果由于缓存一致性问题而在矩阵的列上而不是矩阵的行上进行迭代,请添加注释。如果您做了任何稍微聪明的事情,请对其进行评论。如果可以保证白皮书和代码永远不会分开(通过VCS或类似系统),则可以参考该白皮书。结果很容易超过50%。没关系。这将告诉您代码为何执行其操作。

最后,您需要能够在面对变化时保证正确性。幸运的是,我们是自动化测试持续集成平台中的便捷工具。这些会告诉你什么代码实际上做

我最衷心的建议是不要跳过任何步骤。您稍后将需要它们;)


感谢您的全面答复。我同意你的所有观点。在自动测试方面,我发现要充分覆盖定点算术和SIMD代码的数值范围是困难的,我已经被烧了两次。仅在注释中陈述的前提条件(没有要加强的代码)并不总是得到满足。
rwong 2011年

我尚未接受您的回答的原因是因为我需要更多有关“简短白皮书”的含义以及在制作该白皮书时应付出的努力的指导。对于某些行业,这是主营业务的一部分,但是在其他行业中,必须考虑成本,并且应该采用合法可用的快捷方式。
rwong

首先,我对自动测试,浮点算术和并行代码感到很痛苦。恐怕没有一种适用于所有情况的解决方案。通常,我会在比较宽容的范围内工作,但在您所在的行业中这可能是不可能的。
drxzcl 2011年

2
在实践中,白皮书通常看起来像是科学论文的初稿,没有“蓬松的”部分(没有有意义的介绍,没有摘要,很少的结论/讨论,只有理解它的必要参考)。我认为撰写本文是算法开发和/或算法选择的报告,也是其不可或缺的一部分。您选择实施此算法(例如频谱FFT)。究竟是什么?您为什么选择了这个?它的并行化特性是什么?工作量应与选择/开发工作成比例。
drxzcl 2011年

5

我认为,最好通过对代码进行全面注释来最好地解决此问题,直到每个重要的代码块都事先具有解释性注释。

评论应包括对规范或硬件参考资料的引用。

在适当的地方使用行业范围内的术语和算法名称-例如“架构X为未对齐的读取生成CPU陷阱,因此此Duff的设备将填充到下一个对齐边界”。

我将使用脸部变量命名来确保不会发生任何误解。不是匈牙利语,而是诸如“跨步”之类的东西来描述两个垂直像素之间的距离(以字节为单位)。

我还将用一个简短的,易于阅读的文档作为补充,该文档具有高级图表和块设计。


1
在单个项目中使用一种一致的术语(例如,对类似含义的术语使用“跨度”,例如“步骤”,“对齐”)会有所帮助。将多个项目的代码库集成到一个项目中时,这有些困难。
rwong
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.