有读者反馈介绍的很不清晰。这里把翻译完整发出来。大家先看个大概,所有翻译都发一遍之后会做总结。预计这个内容起码发一个月吧
一种在程序中插入额外代码以收集特定运行时信息的技术。
比如
int foo(int x) {
+ printf("foo被调用\n");
// 函数体...
}
展示了在函数开头插入printf
语句的最简单示例,以指示该函数何时被调用。然后,运行程序并计算输出中看到“foo被调用”的次数。也许,世界上每个程序员在其职业生涯中至少有一次这样做过。
行首的加号表示此行是添加的,不在原始代码中。通常,插桩化代码并不意味着将其推送到代码库中,而是用于收集所需的数据,然后可以丢弃。
稍微有趣一些的代码插桩化
+ struct histogram {
+ std::map<uint32_t, std::map<uint32_t, uint64_t>> hist;
+ ~histogram() {
+ for (auto& tripCount : hist)
+ for (auto& zoomCount : tripCount.second)
+ std::cout << "[" << tripCount.first << "]["
+ << zoomCount.first << "] : "
+ << zoomCount.second << "\n";
+ }
+ };
+ histogram h;
+ struct incrementor {
+ uint32_t tripCount = 0;
+ uint32_t zoomCount = 0;
+ ~incrementor() {
+ h.hist[tripCount][zoomCount]++;
+ }
+ };
Coords findObject(const ObjParams& p, Coords c, float searchRadius) {
+ incrementor inc;
while (true) {
+ inc.tripCount++;
float match = findObj(c, p);
if (exactMatch(match))
return c;
if (match > threshold) {
searchRadius = zoomIn(c, searchRadius);
+ inc.zoomCount++;
}
c = getNewCoords(searchRadius);
}
return c;
}
在这个虚构的代码示例中,函数findObject
在地图上搜索具有某些属性p
的对象的坐标。函数findObj
返回使用当前坐标c
定位正确对象的置信度级别。如果是完全匹配,我们停止搜索循环并返回坐标。如果置信度高于threshold
,我们选择zoomIn
以找到对象更精确的位置。否则,我们在searchRadius
范围内获取新的坐标以便下次尝试搜索。
插桩化代码由两个类组成:histogram
和incrementor
。前者跟踪我们感兴趣的变量值及其出现频率,然后在程序完成后打印直方图。后者只是一个辅助类,用于将值推送到histogram
对象中。它非常简单,可以快速调整以满足您的特定需求。我有一个稍微更高级的版本,通常会将其复制粘贴到我正在工作的任何项目中,然后将其删除。
在这个假设情景中,我们添加了插桩化代码以了解在找到对象之前我们多频繁地zoomIn
。变量inc.tripCount
计算循环退出之前循环运行的次数,而变量inc.zoomCount
计算我们减少搜索半径的次数。我们总是期望inc.zoomCount
小于或等于inc.tripCount
。下面是运行插桩化程序后可能观察到的输出:
[7][6]: 2
[7][5]: 6
[7][4]: 20
[7][3]: 156
[7][2]: 967
[7][1]: 3685
[7][0]: 251004
[6][5]: 2
[6][4]: 7
[6][3]: 39
[6][2]: 300
[6][1]: 1235
[6][0]: 91731
[5][4]: 9
[5][3]: 32
[5][2]: 160
[5][1]: 764
[5][0]: 34142
[4][4]: 5
[4][3]: 31
[4][2]: 103
[4][1]: 195
[4][0]: 14575
...
在方括号中的第一个数字是循环的次数,第二个数字是在同一个循环中进行的zoomIn
次数。冒号后面的数字是该特定组合的出现次数。例如,我们观察到7次循环迭代和6次zoomIn
发生了两次,循环运行了7次迭代且没有zoomIn
的情况发生了251004次,依此类推。然后,您可以绘制数据以进行更好的可视化,采用一些其他统计方法,但我们可以得出的主要观点是zoomIn
并不频繁。在调用了400k次findObject
的情况下,总共有10k次zoomIn
调用。
后续章节包含许多示例,说明了这类信息如何用于基于数据的优化。在我们的情况下,我们得出结论:findObj
经常无法找到对象。这意味着循环的下一次迭代将尝试使用新坐标来找到对象,但搜索半径仍然相同。有了这个信息,我们可以尝试一些优化:1)并行运行多个搜索,并在其中任何一个成功时同步;2)为当前搜索区域预先计算某些内容,从而消除findObj
内的重复工作;3)编写一个软件管道,调用getNewCoords
以生成下一组所需坐标,并从内存中预取相应的地图位置。本书的第二部分将更深入地探讨一些这样的技术。
代码插桩化在需要关于程序执行的特定知识时提供了非常详细的信息。它允许我们跟踪程序中每个变量的任何信息。在优化大型代码块时,使用这种方法通常会产生最好的见解,因为您可以使用自上而下的方法(插桩化主函数,然后逐步深入到其被调用的函数)来定位性能问题。虽然代码插桩化在小程序的情况下并不是很有帮助,但通过让开发人员观察应用程序的架构和流程,它提供了最大的价值和见解。对于与不熟悉的代码库一起工作的人来说,这种技术尤其有帮助。
值得一提的是,代码插桩化在具有许多不同组件的复杂系统中表现突出,这些组件根据输入或时间的不同而产生不同的反应。例如,在游戏中,通常有一个渲染线程、一个物理线程、一个动画线程等。对这样的大型模块进行插桩化有助于相对快速地理解哪个模块是问题的源头。因为有时,优化不仅仅是优化代码,还包括数据。例如,渲染可能太慢是因为网格未压缩,或者物理可能太慢是因为场景中的对象太多。
插桩化技术在实时场景的性能分析中被广泛使用,例如视频游戏和嵌入式开发。一些性能分析器将插桩化与其他技术(如跟踪和采样)混合在一起。比如Tracy。
虽然在许多情况下代码插桩化是强大的,但它并不提供有关代码如何从操作系统或CPU的角度执行的任何信息。例如,它无法告诉您进程被调度到执行中和退出执行的频率(由操作系统知道),或者分支错误预测发生的次数(由CPU知道)。被插桩化的代码是应用程序的一部分,并具有与应用程序本身相同的特权。它在用户空间中运行,无法访问内核。
但更重要的是,这种技术的缺点是每次需要插桩化新内容,例如另一个变量时,都需要重新编译。这可能会给工程师带来负担,并增加分析时间。不幸的是,还有其他一些缺点。由于通常您关心的是应用程序中的热点路径,因此您正在为位于代码性能关键部分的内容进行插桩化。在热点路径中注入插桩化代码可能很容易导致整体基准测试减慢2倍。请记住不要对被插桩化的程序进行基准测试,即不要在同一运行中进行评分和分析。请记住,通过对代码进行插桩化,您会改变程序的行为,因此您可能看不到之前看到的相同效果。
上述所有内容增加了实验之间的时间,消耗了更多的开发时间,这就是为什么工程师如今很少手动插桩化他们的代码的原因。然而,自动化代码插桩化仍然被编译器广泛使用。编译器能够自动对整个程序进行插桩化,并收集有关执行的有趣统计信息。自动插桩化最广泛的用例是代码覆盖分析和基于性能指导的优化比如PGO。
在谈到插桩化时,重要的是要提到二进制插桩化技术。二进制插桩化的思想类似,但它是在已构建的可执行文件上完成的,而不是在源代码级别上。有两种类型的二进制插桩化:静态(在构建之前完成)和动态(在程序执行时根据需要插入插桩化代码)。动态二进制插桩化的主要优势在于它不需要重新编译和重新链接程序。此外,通过动态插桩化,可以将插桩化的量限制为仅限于感兴趣的代码区域,而不是整个程序。
二进制插桩化在性能分析和调试中非常有用。二进制插桩化最流行的工具之一是Intel Pin工具。
https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool
Pin拦截程序在发生有趣事件时的执行,并生成从程序中的这一点开始的新插桩化代码。它允许收集各种运行时信息,例如:
与代码插桩化类似,二进制插桩化只允许对用户级代码进行插桩化,而且可能非常慢