Knowing Where to Type ‘Zero’ (2015)
Knowing Where to Type ‘Zero’ (2015)
知道在哪里输入“零”(2015)
Some code optimizations requires complex data structures and thousands lines of code. But, in a surprising number of cases, significant improvements can be made by simple changes – sometimes as simple as typing a single zero. 有些代码优化需要复杂的数据结构和数千行代码。但在许多令人惊讶的情况下,通过简单的修改就能实现显著的性能提升——有时甚至简单到只需输入一个“零”。
It’s like the old story of the boilermaker who knows the right place to tap with his hammer – he sends an itemized bill for $0.50 for tapping the valve, and $999.50 for knowing where to tap. I’ve been personally involved in several performance bugs that were fixed with the typing of a single zero and in this post I want to share two of them. 这就像那个关于锅炉工的古老故事:他知道用锤子敲击哪里——他开出的明细账单上写着:敲击阀门收费 0.50 美元,而知道敲哪里收费 999.50 美元。我曾亲身参与过几个通过输入一个“零”就修复的性能 Bug,在这篇文章中,我想分享其中两个案例。
The importance of measurement
测量的意义
Back in the days of the original Xbox I helped optimize a lot of games. While working on one of these games the profiler led me to a matrix transformation function that was consuming 7% of CPU time – the biggest spike on the graph. So, I dutifully went to work to optimize this function. 在初代 Xbox 的时代,我曾协助优化过许多游戏。在处理其中一款游戏时,性能分析器(Profiler)指向了一个矩阵转换函数,它占用了 7% 的 CPU 时间——这是图表上最突出的峰值。于是,我尽职尽责地开始优化这个函数。
I was not the first person who had been down this path. The function had already been rewritten in assembly language. I found a few potential improvements to the assembly language and tried to measure the improvements. This is a crucial step – otherwise I could easily have ended up checking in an ‘optimization’ that made no difference, or perhaps even made things worse. 我并不是第一个走这条路的人。该函数之前已经被重写为汇编语言。我发现了一些对汇编代码的潜在改进,并试图测量这些改进的效果。这是一个至关重要的步骤——否则,我很容易提交一个毫无作用,甚至可能让情况变得更糟的“优化”。
However measuring the improvements was difficult. I’d launch the game, play for a bit while profiling, and then examine the profile to see whether the code was faster. It looked like there might be some modest progress, but there was so much randomness that it was hard to be sure. 然而,测量改进效果非常困难。我会启动游戏,在进行性能分析的同时玩一会儿,然后检查分析结果,看看代码是否变快了。看起来似乎有一点微小的进步,但由于随机性太大,很难确定。
So I got all scientific. I wrote a test harness that could drive the old and new versions of the code so that I could precisely measure the performance differences. This didn’t take long and it let me see that, as predicted, the new code was about 10% faster than the old. But it turns out that that 10% speedup was not interesting. What was more interesting was that the code inside of the test harness was running about 10x faster (900%!!!) than the same code inside of the game. That was an exciting discovery. After checking my results, and starting into space for a while I realized what must be happening. 于是我变得“科学”起来。我编写了一个测试工具,可以驱动新旧版本的代码,以便精确测量性能差异。这没花多长时间,它让我看到,正如预期的那样,新代码比旧代码快了约 10%。但事实证明,那 10% 的提速并不重要。更有趣的是,测试工具中的代码运行速度比游戏内相同的代码快了约 10 倍(900%!!!)。这是一个令人兴奋的发现。在核对结果并对着虚空发呆了一会儿后,我意识到发生了什么。
Caching counts
缓存至关重要
In order to give game developers full control and maximum performance, video game consoles let game developers allocate memory with different attributes. In particular, the original Xbox would let game developers allocate non-cacheable memory. This type of memory (actually, this type of tag in the page tables) is useful when writing data that will be used by the GPU. Because the memory is non-cacheable the writes will go almost straight to RAM, avoiding the delays and cache-pollution that would happen with ‘normal’ memory mappings. 为了给游戏开发者提供完全的控制权和最大化的性能,游戏主机允许开发者分配具有不同属性的内存。特别是初代 Xbox,它允许开发者分配“不可缓存”(non-cacheable)内存。这种类型的内存(实际上是页表中的一种标记)在写入将由 GPU 使用的数据时非常有用。由于内存是不可缓存的,写入操作几乎会直接进入 RAM,从而避免了“普通”内存映射中可能发生的延迟和缓存污染。
So non-cacheable memory is an important optimization, but it must be used carefully. In particular, it is crucial that games never try to read from non-cacheable memory, or their performance will be severely compromised. Even the relatively slow 733 MHz CPU of the original Xbox needed its caches to give adequate performance when reading data. 因此,不可缓存内存是一种重要的优化手段,但必须谨慎使用。特别关键的是,游戏绝不能尝试从不可缓存内存中读取数据,否则性能会受到严重损害。即使是初代 Xbox 那相对较慢的 733 MHz CPU,在读取数据时也需要缓存来提供足够的性能。
With this knowledge in hand I realized what must be happening. The data used by this function must have been allocated in non-cacheable memory, and that was why the performance was poor. A bit of investigation confirmed this hypothesis and the stage was set for the fix. I located the line of code that allocated the memory, double-clicked on the flag value that was erroneously requesting non-cacheable memory, and typed zero. 掌握了这些知识后,我意识到发生了什么。该函数使用的数据一定是被分配在了不可缓存内存中,这就是性能低下的原因。稍作调查证实了这一假设,修复工作也随之展开。我定位到分配内存的那行代码,双击了那个错误请求不可缓存内存的标志值,然后输入了“零”。
The cost of this function went from ~7% of CPU time down to about 0.7% of CPU time, and was no longer of interest. My status report at the end of that week was something like “39.999 hours of investigation, 0.001 hours of coding – huge success!” 该函数的开销从约 7% 的 CPU 时间下降到了约 0.7%,不再值得关注。那周末我的状态报告写的是:“39.999 小时的调查,0.001 小时的编码——巨大的成功!”
Most developers don’t need to worry about accidentally allocating non-cacheable memory – that option isn’t easily available in user space in most operating systems. But, if you want to see how much non-cacheable memory can slow down your code, trying using the PAGE_NOCACHE or PAGE_WRITECOMBINE flags with VirtualAlloc.
大多数开发者不需要担心意外分配不可缓存内存——在大多数操作系统中,用户空间并不容易获得该选项。但是,如果你想看看不可缓存内存会让你的代码变慢多少,可以尝试在 VirtualAlloc 中使用 PAGE_NOCACHE 或 PAGE_WRITECOMBINE 标志。
Zero GiB is better than four GiB
零 GiB 优于四 GiB
The other tale I want to share is of a bug that I found, but which somebody else fixed. A couple of years ago I noticed that the disk cache on my laptop was getting purged quite frequently. I tracked this down to a transient 4 GiB allocation, and I eventually discovered that the device driver for my new backup drive was setting SectorSize to 0xFFFFFFFF (or –1) to indicate an unknown sector size. The Windows kernel interpreted this value as 4 GiB, allocated that big a block of memory, and that was the cause of the problem.
我想分享的另一个故事是一个我发现但由别人修复的 Bug。几年前,我注意到我笔记本电脑上的磁盘缓存被频繁清除。我追踪到这是一个瞬时的 4 GiB 内存分配,最终发现我新备份驱动器的设备驱动程序将 SectorSize 设置为 0xFFFFFFFF(或 -1)来表示未知的扇区大小。Windows 内核将此值解释为 4 GiB,并分配了这么大一块内存,这就是问题的根源。
I don’t have any contacts at Western Digital but it is pretty safe to assume that they fixed this bug by selecting the 0xFFFFFFFF (or -1) constant and then typing zero. A single character typed, and a significant performance regression fixed.
我与西部数据(Western Digital)没有任何联系,但可以肯定的是,他们修复这个 Bug 的方法就是选中 0xFFFFFFFF(或 -1)常量,然后输入“零”。只需输入一个字符,一个重大的性能倒退就被修复了。
Observations
观察总结
-
In both cases the problem was related to caching
-
Using a profiler to accurately identify the problem is crucial
-
A fix that is not verified through measurements is not necessarily a fix
-
I could write about many instances of this but the other examples are either too secret or too boring
-
The right fix needn’t be complicated. Sometimes a huge improvement can be made by a tiny change, but you’ve got to know where to tap
-
在这两个案例中,问题都与缓存有关
-
使用性能分析器准确识别问题至关重要
-
未经测量验证的修复不一定是真正的修复
-
我还可以写很多这样的例子,但其他的例子要么太机密,要么太无聊
-
正确的修复方案不一定很复杂。有时微小的改动就能带来巨大的提升,但你必须知道该“敲”哪里。
I’ve also optimized code by commenting out a #define, and other trivial changes. Share your similar stories in the comments.
我也曾通过注释掉一个 #define 以及其他琐碎的改动来优化代码。欢迎在评论区分享你类似的故事。