当您调用 API 中的函数时,您期望它们能正确工作;有时这种期望被称为调用者和实现之间的合约。调用者对这些函数也有性能期望,并且软件系统的成功通常取决于 API 是否满足这些期望。因此,除了正确性合约之外,还有一个性能合约。性能合约通常是隐含的、常常是模糊的,并且有时会被(调用者或实现)违反。如何改进 API 设计和文档的这一方面?
如今,任何重要的软件系统都依赖于他人的工作:当然,您编写了一些代码,但您通过 API 调用操作系统和各种软件包中的函数,从而减少了您必须编写的代码量。在某些情况下,您甚至将工作外包给通过不稳定的网络连接到您的远程服务器。您依赖这些函数和服务来保证正确运行,但您也依赖它们表现得足够好,以使整个系统运行良好。在涉及分页、网络延迟、共享磁盘等资源等复杂系统中,必然存在性能变化。然而,即使在简单的设置中,例如所有程序和数据都在内存中的独立计算机,当 API 或操作系统不符合性能期望时,您也可能会感到惊讶。
人们习惯于谈论应用程序和 API 实现之间的合约,以描述调用 API 函数时的正确行为。调用者必须满足某些初始要求,然后函数必须按规定执行。(这些双重责任类似于 Floyd-Hoare 逻辑中用于证明程序正确性的前置条件和后置条件。)虽然今天的 API 规范没有以导致正确性证明的方式明确指出正确性标准,但 API 函数的类型声明和文本文档力求对其逻辑行为保持明确。如果一个函数被描述为“将元素e添加到列表l的末尾”,其中e和l由调用序列中的类型描述,那么调用者就知道期望什么行为。
然而,API 函数不仅仅是正确性。它消耗哪些资源,速度有多快?人们常常根据自己对函数实现应该是什么样子的判断来做出假设。将元素添加到列表的末尾应该是“廉价的”。反转列表可能是“列表长度的线性函数”。对于许多简单函数,这些直觉足以避免麻烦——尽管并非总是如此,如本文稍后所述。然而,对于任何复杂度的函数,例如“在窗口w中以字体f在(x,y)处绘制字符串s”,或“查找远程文件上存储的值的平均值”,如果性能令人惊讶甚至令人沮丧,也是有空间的。此外,API 文档没有提供关于哪些函数是廉价的,哪些函数是昂贵的提示。
更复杂的是,在您根据 API 的性能特征调整了应用程序之后,新版本的 API 实现或新的远程服务器存储了数据文件,并且整体性能没有提高(对新事物的永恒希望),而是下降了。简而言之,在系统组件组合中伴随而来的性能合约值得更多关注。
程序员在其职业生涯早期就开始构建直观的 API 性能模型(见框 1)。为了有用,该模型不必非常准确。这是一个简单的分类
始终廉价(示例toupper, isdigit, java.util.HashMap.get)。前两个函数始终是廉价的,通常是内联表查找。在大小合适的哈希表中查找预计会很快,但哈希冲突可能会减慢偶尔的访问。
通常廉价(示例fgetc, java.util.HashMap.put)。许多函数被设计为在大多数情况下都很快,但偶尔需要调用更复杂的代码;fgetc必须偶尔读取新的字符缓冲区。在哈希表中存储新项可能会使表变得太满,以至于实现会扩大它并重新哈希其所有条目。
关于java.util.HashMap的文档在公开性能合约方面做了一个值得称赞的开始:“假设哈希函数将元素适当地分散到桶中,此实现为基本操作(get 和 put)提供恒定时间性能。遍历集合视图所需的时间与 HashMap 的“容量”成正比……”3
的性能fgetc取决于底层流的属性。如果它是磁盘文件,那么该函数通常会从用户内存缓冲区读取,而无需操作系统调用,但它必须偶尔调用操作系统来读取新的缓冲区。如果是从键盘读取输入,那么实现可能会为读取的每个字符调用操作系统。
在学习编程时,您很早就获得了关于性能的经验法则。下面的伪代码是合理高效处理中等大小字符文件的模式
fs = fopen("~dan/weather-data.txt", "r"); //(1)
for ( i=0; i<10000; i++) {
ch = fgetc(fs); //(2)
// 处理字符 ch
}
...
函数调用 (1) 预计会花费一段时间,但获取字符的调用 (2) 预计是廉价的。这在直觉上是有道理的:要处理文件,只需要打开一次流,但是“获取下一个字符”函数将被频繁调用,可能数千次或数百万次。
这两个流函数由库实现。库的文档2清楚地说明了这些函数的作用——实现和应用程序之间正确性合约的非正式演示。没有提及性能,也没有任何提示程序员这两个函数在性能上存在显着差异。因此,程序员根据经验而不是规范来构建性能模型。7
并非所有函数都具有明显的性能属性。例如
fseek(fs, ptr, SEEK_SET); //(3)
当目标文件数据已在缓冲区中时,此函数可能很廉价。在一般情况下,它将涉及操作系统调用,甚至可能涉及 I/O。在极端情况下,它可能需要卷起数千英尺的磁带。库函数也可能即使在简单情况下也不廉价:实现可能只是存储指针并设置一个标志,该标志将导致在下一个读取或写入数据的流调用上完成繁重的工作,从而将性能不确定性推到原本廉价的函数上。
可预测(示例qsort, regexec)。这些函数的性能随其参数的属性而变化(例如,要排序的数组的大小或要搜索的字符串的长度)。这些函数通常是使用众所周知的算法的数据结构或常用算法实用程序,不需要系统调用。您通常可以根据对底层算法的期望来判断性能(例如,排序将花费 n log n 时间)。当使用复杂的数据结构(例如,B 树)或通用集合(可能难以识别底层具体实现)时,可能更难以估计性能。重要的是要理解,可预测性可能只是概率性的;regexec通常可以根据其输入来预测,但存在会导致指数时间爆炸的病态表达式。
未知(示例fopen, fseek, pthread_create,许多“初始化”函数,以及任何遍历网络的调用)。这些函数并不廉价,并且其性能通常具有高度差异。它们从池中分配资源(线程、内存、磁盘、操作系统对象),通常需要独占访问共享操作系统或 I/O 资源。通常需要大量的初始化。跨网络调用总是很昂贵的(相对于本地访问),但费用的变化可能更大,使得形成合理的性能模型更加困难。
线程库很容易出现性能问题。Posix 标准花费了很多年才稳定下来,并且实现仍然受到问题的困扰。6线程应用程序的可移植性仍然不稳定。线程困难的一些原因是:(1)需要与操作系统紧密集成,几乎所有操作系统(包括 Unix 和 Linux)最初的设计都没有考虑到线程;(2)与其他库的交互,特别是使函数线程安全并处理由此引起的性能问题;以及(3)线程实现的几个不同的设计点,大致分为轻量级和重量级。
SetFontSize(f, 10);
SetDrawPosition(w, 200, 20);
DrawText(w, f, "This is a passage.");
此示例来自一个虚构的窗口系统:它设置字体大小和绘图位置,然后绘制一些文本。您可能期望所有这些函数都非常廉价,因为在屏幕上渲染文本窗口应该很快。事实上,在早期的窗口系统中,例如 Macintosh 上的 QuickDraw,您是对的。
今天窗口系统的功能已经正确地向上蔓延。字符光栅——甚至那些在显示器上渲染的光栅——都是从几何轮廓计算出来的,因此准备给定大小的字体可能需要一段时间。现代窗口系统通过大量使用缓存来加速渲染文本——甚至将准备好的光栅保存在磁盘上,以便它们可供多个应用程序使用,并在系统重启后仍然保留。然而,应用程序程序员不知道他们请求的字体是否已准备好并缓存,不知道SetFontSize是否会为字体的所有字符进行(大量的)计算,或者字符是否会根据传递给DrawText.
的字符串的需要逐个转换。增强的功能还可能允许将字体数据存储在网络连接的文件服务器上,因此字体访问可能需要一段时间,或者如果服务器没有响应,甚至会在长时间超时后失败。
在许多情况下,细节无关紧要,但如果延迟很大或高度可变,则应用程序可能会选择在其初始化期间准备好计划使用的所有字体。大多数窗口系统 API 没有这样的功能。
一些库提供了一种以上执行功能的方式,通常是因为替代方案具有非常不同的性能。
回到框 1,请注意,大多数程序员被告知,使用库函数来获取每个字符不是最快的方法(即使函数代码是内联的以避免函数调用开销)。更注重性能的人会读取一个非常大的字符数组,并使用编程语言中的数组或指针操作提取每个字符。在极端情况下,应用程序可以将文件的页面映射到内存页面中,以避免将数据复制到数组中。为了换取性能,这些函数给调用者带来了更大的负担(例如,要使缓冲区算术正确,并使实现与其他库调用(例如调用fseek需要调整缓冲区指针,甚至可能调整内容)保持一致)。
程序员总是被建议避免程序中过早的优化,从而将极端的修改延迟到更简单的修改被证明不足时才进行。要确定性能的唯一方法是测量它。程序员通常在遇到性能期望(或估计)与实现提供的现实之间的不匹配时才编写整个程序。
SetColor(g, COLOR_RED);
DrawLine(g, 100, 200, 200, 600);
图形库函数通常很快。合理地假设您通过为每条线设置颜色来绘制许多不同颜色的线;如果您担心SetColor函数不廉价,您可能只在颜色更改时才调用它。在某个时候,一家工作站公司的图形硬件产品需要清除(硬件)几何流水线才能更改线的颜色,因此颜色更改是一项昂贵的操作。为了快速,应用程序被迫(例如)按颜色对线条进行排序并将所有红线一起绘制。这不是编写程序的直观方式,它迫使应用程序的结构和编程发生重大变化。
昂贵的SetColor操作被理解为一个错误,并在随后的硬件中得到修复。然而,为克服错误而编写的代码可能会保留下来,对于任何维护代码但不知道硬件性能历史的人来说,这成为一种莫名其妙的复杂性。
“可预测”函数的性能可以根据其参数的属性来估计。“未知”函数的性能也可能因要求它们执行的操作而异。在存储设备上打开流所需的时间肯定取决于底层设备的访问时间和可能的数据传输速率。通过网络协议访问的存储可能特别昂贵;当然,它将是可变的。
许多廉价函数只是大部分时间是廉价的,或者它们具有预期的廉价成本。“获取字符”例程必须偶尔使用操作系统调用来重新填充缓冲区,这总是比从完整缓冲区获取字符花费的时间长得多——并且偶尔可能会花费很长时间(例如,读取负载过重的文件服务器上的文件或来自即将崩溃的磁盘的文件,并且只有在多次读取重试后才会成功)。
具有“未知”性能的函数很可能由于多种原因而表现出广泛的性能变化。原因之一是功能蔓延(见框 2),其中通用函数随着时间的推移变得更加强大。I/O 流就是一个很好的例子:根据要打开的流的类型(本地磁盘文件、网络服务文件、管道、网络流、内存中的字符串等),调用打开流会在库和操作系统中调用非常不同的代码。随着 I/O 设备和文件类型的范围扩大,性能的变化只会增加。大多数 API 的常见生命周期——随着时间的推移逐步添加功能——不可避免地会增加性能变化。
性能变化的一个重要来源是库到不同平台的端口之间的差异。当然,平台——硬件和操作系统——的底层速度会有所不同,但库端口可能导致 API 中函数相对性能的变化或 API 之间性能的变化。快速而粗糙的初始端口通常存在许多性能问题,这些问题会逐渐得到修复。一些库,例如用于处理线程的库,具有众所周知的广泛的移植性能变化。线程异常可能会表现为极端行为——应用程序慢得令人无法接受,甚至死锁。
这些变化是难以构建精确的性能合约的原因之一。通常不需要非常精确地了解性能,但与预期行为的极端偏差可能会导致问题。
如果可以使用“通常廉价”来描述使用malloc()函数进行的动态内存分配,那就太好了,但这将是错误的,因为内存分配——尤其是malloc——是程序员开始寻找性能问题时的首要嫌疑对象之一。作为他们性能直觉教育的一部分,程序员了解到,如果他们调用malloc数万次,尤其是分配小的固定大小的块,他们最好使用malloc分配更大的内存块,将其切成固定大小的块,并管理他们自己的空闲块列表。
多年来,malloc的实现者一直在努力使其在存在广泛的使用模式变化以及运行它的硬件/软件系统属性的情况下通常是廉价的。4提供虚拟内存、线程和非常大内存的系统都对“廉价”和malloc——及其补充,free()——提出了挑战,必须权衡效率和某些使用模式(如内存碎片)的弊端。8
一些软件系统,例如 Lisp 和 Java,使用自动内存分配以及垃圾回收来管理空闲存储。虽然这非常方便,但关注性能的程序员必须意识到成本。例如,应该尽早教导 Java 程序员 String 对象和StringBuffer对象之间的区别,String 对象只能通过在新内存中创建新副本来修改,而 StringBuffer 对象包含容纳字符串延长的空间。随着垃圾回收系统的改进,它们减少了垃圾回收不可预测的暂停;这可能会诱使程序员变得自满,认为自动内存回收永远不会成为性能问题,而事实上它只是不那么频繁地成为性能问题。
API 的规范包括调用失败时的行为细节。返回错误代码和抛出异常是告知调用者函数未成功的常用方法。然而,与正常行为的规范一样,失败的性能未指定。以下是三个重要案例
* 快速失败。 调用快速失败——与正常行为一样快或更快。调用sqrt(-1)会快速失败。即使malloc调用由于没有更多可用内存而失败,该调用也应以与任何malloc必须从操作系统请求更多内存的调用大致相同的速度返回。调用打开不存在的磁盘文件以进行读取可能会以与成功调用大致相同的速度返回。
* 慢速失败。 有时调用失败非常缓慢——非常缓慢以至于应用程序可能想要以其他方式继续进行。例如,打开与另一台计算机的网络连接的请求可能仅在几个长时间超时到期后才会失败。
* 永远失败。 有时调用只是停滞不前,并且不允许应用程序继续进行。例如,其实现在等待永远不会释放的同步锁的调用可能永远不会返回。
关于失败性能的直觉很少像正常性能那样好。原因之一只是编写、调试和调整程序提供的失败事件经验远少于正常事件经验。另一个原因是函数调用可能会以非常非常多的方式失败,其中一些是致命的,并非所有方式都在 API 的规范中描述。即使是旨在更精确地描述错误处理的异常机制,也不会使所有可能的异常都可见。此外,随着库功能的增加,失败的机会也会增加。例如,包装网络服务的 API(ODBC、JDBC、UPnP 等)本质上订阅了大量的网络故障机制。
勤奋的应用程序程序员使用大量火炮来处理不太可能发生的故障。一种常见的技术是用try...catch块包围程序的相当大的部分,这些块可以重试失败的整个部分。交互式程序可以尝试使用巨大的try...catch围绕整个程序来保存用户的工作,其效果是通过在磁盘文件中保存关键日志或数据结构(记录故障前完成的工作的效果)来减轻主程序的故障。
处理停滞或死锁的唯一方法是设置监视线程,该线程期望正常运行的应用程序程序定期与监视程序签到,实际上是说“我仍在正常运行”。如果在签到之间经过的时间过长,监视程序将采取行动——例如,保存状态、中止主线程并重新启动整个应用程序。如果交互式程序响应用户的命令而调用可能缓慢失败的函数,则它可以使用监视程序来中止整个命令并返回到允许用户继续其他命令的已知状态。这引起了一种防御性编程风格,该风格计划中止每个可能的命令。
为什么 API 必须遵守性能合约?因为应用程序程序的主要结构可能取决于 API 是否遵守此类合约。程序员部分基于对 API 性能的期望来选择 API、数据结构和整体程序结构。如果期望或性能严重错误,程序员无法仅通过调整 API 调用来恢复,而必须重写大部分(可能是主要的)程序部分。前面提到的交互式程序的防御性结构是另一个例子。
实际上,严重违反性能合约会导致组合失败:为合约编写的程序无法与未能遵守合约的实现相匹配(组合)。
当然,有许多程序的结构和性能很少受到库性能的影响(科学计算和大型模拟通常属于此类)。然而,当今的大部分“日常 IT”,尤其是 Web 服务中普遍存在的软件,都广泛使用库,这些库的性能对于整体性能至关重要。
即使是微小的性能变化也可能导致用户对程序的感知发生重大变化。在处理各种媒体的程序中尤其如此。丢帧偶尔的视频流可能是可以接受的(事实上,比允许帧率滞后于其他媒体更可接受),但人类已经进化到可以检测到音频中的轻微掉线,因此音频媒体性能的微小变化可能会对整个程序的可接受性产生重大影响。这种担忧导致人们对服务质量概念产生了相当大的兴趣,服务质量在许多方面都是确保高水平性能的尝试。
虽然违反性能合约的情况很少见,也很少是灾难性的,但在使用软件库时关注性能可以帮助开发更健壮的软件。以下是一些需要采取的预防措施和策略
1. 仔细选择 API 和程序结构。 如果您有幸从头开始编写程序,请在开始时思考性能合约的含义。如果程序最初是作为原型开始,然后保持服务一段时间,那么它无疑至少会被重写一次;重写是重新思考 API 和结构选择的机会。
2. API 实现者有义务在新版本和端口发布时提供一致的性能合约。 即使是新的实验性 API 也会吸引用户,他们将开始推导出 API 的性能模型。此后,更改性能合约肯定会激怒开发人员,并可能导致他们重写程序。
一旦 API 成熟,性能合约不发生变化就至关重要。事实上,最通用的 API(例如,libc)可以说之所以变得如此通用,部分原因是它们的性能合约在 API 的发展过程中一直保持稳定。API 的端口也是如此。
人们可以希望 API 实现者可能会例行测试新版本,以验证它们是否没有引入性能怪癖。不幸的是,这种测试很少进行。然而,这并不意味着您不能对您依赖的 API 部分进行自己的测试。使用分析器,通常可以发现程序依赖于少量 API。编写性能测试套件,将库的新版本与早期版本的记录性能进行比较,可以为程序员提供早期警告,即他们自己的代码的性能将随着新库的发布而发生变化。
许多程序员期望计算机及其软件随着时间的推移均匀地变得更快。也就是说,他们期望库或计算机系统的每个新版本都能平等地扩展所有 API 函数的性能,尤其是“廉价”的函数。事实上,供应商很难保证这一点,但这非常接近实际做法,以至于客户相信这一点。许多工作站客户期望新版本的图形库、驱动程序和硬件能够提高所有图形应用程序的性能,但他们同样热衷于各种各样的功能改进,这些改进通常会降低旧功能的性能,即使只是稍微降低。
人们也可以希望 API 规范能够明确性能合约,以便使用、修改或移植代码的人能够遵守合约。请注意,函数对动态内存分配的使用(无论是隐式的还是自动的)都应成为本文档的一部分。
3. 防御性编程可以提供帮助。 程序员在调用性能未知或高度可变的 API 函数时可以使用特别的谨慎;对于失败性能的考虑尤其如此。您可以将初始化移到性能关键区域之外,并尝试预热 API 可能使用的任何缓存数据(例如,字体)。表现出较大性能差异或具有大量内部缓存数据的 API 可以通过提供从应用程序向 API 传递关于如何分配或初始化这些结构的提示的函数来提供帮助。偶尔 ping 程序已知要联系的服务器可以建立一个可能不可用的服务器列表,从而避免一些长时间的故障暂停。
图形应用程序有时使用的一种技术是进行试运行,将图形窗口渲染到屏幕外(不可见)窗口中,仅仅是为了预热字体和其他图形数据结构的缓存。
4. 调整 API 公开的参数。 一些库提供了影响性能的显式方法(例如,控制为文件分配的缓冲区大小、表的初始大小或缓存的大小)。操作系统也提供调整选项。调整这些参数可以在性能合约的范围内提高性能;调整不能解决严重问题,但可以缓解嵌入在严重影响性能的库中的其他固定选择。
一些库提供了语义相同但实现不同的函数,通常以通用 API 的具体实现形式出现。通过选择最佳的具体实现进行调优通常非常容易。“Java 集合”包就是这种结构的一个很好的例子。
越来越多的 API 被设计为可以动态适应使用情况,从而使程序员无需选择最佳参数设置。如果哈希表变得太满,它会自动扩展和重新哈希(这种优点与偶尔扩展带来的性能损失相平衡)。如果正在顺序读取文件,则可以分配更多缓冲区,以便以更大的块读取。
5. 测量性能以验证假设。 给程序员的常见建议是检测关键数据结构,以确定每个结构是否被正确使用。例如,您可以测量哈希表的填充程度或哈希冲突发生的频率。或者,您可以验证旨在提高读取速度但牺牲写入性能的结构实际上是否读取多于写入。
添加足够的检测来准确测量许多 API 调用的性能是很困难的,工作量很大,并且可能不值得它产生的信息。然而,在那些对应用程序性能至关重要的 API 调用上添加检测(假设您可以识别它们并且您的识别是正确的),可以在出现问题时节省大量时间。请注意,正如前面提到的,许多此类代码可以作为库的下一个版本的性能监视器的一部分重用。
以上这些并非旨在阻止梦想家致力于开发能够自动化此类检测和测量的工具,或开发指定性能合同的方法,以便性能测量可以确定是否符合合同。这些都不是容易实现的目标,而且回报可能不大。
通常可以在没有预先检测软件的情况下进行性能测量(例如,通过使用性能分析器或 DTrace5 等工具)。这些方法的优点是不需要在出现问题需要追踪之前做任何工作。它们还可以帮助诊断在修改代码或库导致性能下降时出现的故障。正如Programming Pearls 的作者 Jon Bentley 建议的那样,“例行进行性能分析;测量性能相对于可信基线的漂移。”1
6. 使用日志:检测和记录异常。 越来越多地,当分布式服务组合形成复杂的系统时,性能合同的违反情况会在现场出现。(请注意,通过网络接口提供的主要服务有时具有 SLA [服务级别协议],其中规定了可接受的性能。在许多配置中,测量过程会偶尔发出服务请求,以检查是否满足 SLA。)由于这些服务是通过使用类似于 API 函数调用的方法(例如,远程过程调用或其变体,如 XML-RPC、SOAP 或 REST)通过网络连接调用的,因此性能合同的期望适用。应用程序检测到这些服务的故障,并且通常会优雅地适应。然而,响应缓慢,尤其是在有数十个相互依赖的服务时,会非常迅速地破坏系统性能。专业管理的服务器环境,例如在主要 Internet 站点提供 Web 服务的环境,具有精密的检测和工具来监控 Web 服务性能并解决出现的问题。家庭中更适度的计算机集合也依赖于此类服务——许多服务是每个笔记本电脑操作系统的一部分,还有一些服务嵌入在网络上的设备中(例如,打印机、网络文件系统或文件备份服务)——但没有辅助工具来帮助检测和处理性能问题。
如果这些服务的客户端记录他们期望的性能并生成有助于诊断问题的日志条目(这就是 syslog 的用途),这将很有帮助。当您的文件备份看起来异常缓慢(备份 200 MB 需要一小时)时,它是否比昨天慢?比上次操作系统软件更新之前慢?考虑到多台计算机可能正在共享备份设备,它是否比您应该期望的慢?或者是否存在一些合理的解释(例如,备份系统发现损坏的数据结构并开始执行漫长的重建过程)?
诊断不透明软件组合中的性能问题(其中没有源代码可用,并且没有形成组合的模块和 API 的详细信息)需要软件在报告性能和检测问题方面发挥作用。虽然您无法解决软件本身内部的性能问题(它是不可透明的),但您可以对操作系统和网络进行调整或修复。如果备份设备速度慢是因为其磁盘几乎已满,那么您当然可以添加更多磁盘空间。良好的日志和相关工具会有所帮助;遗憾的是,日志是计算机系统发展中一个被低估和忽视的领域。
今天的软件系统依赖于组合独立开发的组件,使其能够工作——意味着它们以可接受的速度执行所需的计算。静态检查组合以保证组合将是正确的(“通过组合保证正确”)的梦想仍然难以实现。相反,软件工程实践已经开发出用于测试组件和组合的方法,这些方法效果很好。每次应用程序绑定到动态库或在操作系统接口之上启动时,都需要组合的正确性。
尽管组合的正确性非常重要,但组合的性能——客户端和接口提供者是否遵守它们之间的性能合同——却被忽视了。诚然,此合同不如正确性合同重要,但组合的全部威力取决于它。
本文中的想法并非新颖;成千上万的人在我们之前已经提出了这些想法。特别要感谢 Butler Lampson,他创造了术语cheap来描述性能足够快以至于消除所有优化尝试的函数;它具有“经济实用”的美妙韵味。还要感谢 Eric Allman,他对一个过于草率的早期草稿提出了有益的评论,以及 Jon Bentley,一位执着的性能调试器。
1. Bentley, J. 私人交流。
2. GNU C Library; https://gnu.ac.cn/software/libc/manual/html_node/index.html。
3. Java Platform, Standard Edition 7. API Specification; http://docs.oracle.com/javase/7/docs/api/index.html。
4. Korn, D. G., Vo, K.-P. 1985. In search of a better malloc. In Proceedings of the Summer '85 Usenix Conference: 489-506.
5. Oracle. Solaris Dynamic Tracing Guide; http://docs.oracle.com/cd/E19253-01/817-6223/。
6. Pthreads(7) manual page; http://man7.org/linux/man-pages/man7/pthreads.7.html。
7. Saltzer, J. H., Kaashoek, M. F. 2009. Principle of least astonishment. In Principles of Computer System Design. Morgan Kaufmann: 85.
8. Vo, K.-P. 1996. Vmalloc: a general and efficient memory allocator. Software Practice and Experience 26(3): 357-374; http://www2.research.att.com/~astopen/download/ref/vmalloc/vmalloc-spe.pdf。
喜欢它,讨厌它?请告诉我们
Robert F. Sproull 是马萨诸塞大学(阿默斯特)计算机科学系的兼职教师,他在从 Sun Microsystems 实验室主任职位退休后担任该职位。
Jim Waldo 是哈佛大学计算机科学实践教授,同时也是首席技术官,他在离开 Sun Microsystems 实验室后担任该职位。
© 2013 1542-7730/14/0100 $10.00
最初发表于 Queue vol. 12, no. 1—
在 数字图书馆 中评论本文
David Collier-Brown - 你不了解应用程序性能
您不必在每次遇到性能或容量规划问题时都进行全面基准测试。一个简单的测量将提供您系统的瓶颈点:此示例程序在每个 CPU 每秒八个请求后会明显变慢。这通常足以告诉您最重要的事情:您是否会失败。
Peter Ward, Paul Wankadia, Kavita Guliani - 在 Google 重塑后端子集化
后端子集化对于降低成本很有用,甚至对于在系统限制内运行可能是必要的。十多年来,Google 使用确定性子集化作为其默认后端子集化算法,但尽管此算法平衡了每个后端任务的连接数,但确定性子集化具有高水平的连接抖动。我们在 Google 的目标是设计一种连接抖动减少的算法,该算法可以取代确定性子集化作为默认后端子集化算法。
Noor Mubeen - 工作负载频率缩放定律 - 推导与验证
本文介绍了与每个 DVFS 子系统级别的工作负载利用率缩放相关的方程。建立了频率、利用率和比例因子(比例因子本身随频率变化)之间的关系。这些方程的验证结果证明是棘手的,因为工作负载固有的利用率似乎也在治理样本的粒度上以未指定的方式变化。因此,应用了一种称为直方图脊迹的新方法。在将 DVFS 视为构建块时,量化缩放影响至关重要。典型应用包括 DVFS 管理器和或影响系统利用率、功耗和性能的其他层。
Theo Schlossnagle - DevOps 世界中的监控
监控看起来可能非常令人感到难以承受。最重要的事情要记住,完美永远不应该是更好的敌人。DevOps 使组织内部能够进行高度迭代的改进。如果您没有监控,请获取一些;获取任何东西。有总比没有好,如果您已经拥抱了 DevOps,那么您就已经注册了随着时间的推移使其变得更好。