问题标签 [bounds-check-elimination]
For questions regarding programming in ECMAScript (JavaScript/JS) and its various dialects/implementations (excluding ActionScript). Note JavaScript is NOT the same as Java! Please include all relevant tags on your question; e.g., [node.js], [jquery], [json], [reactjs], [angular], [ember.js], [vue.js], [typescript], [svelte], etc.
java - 为什么 JIT 在消除绑定检查方面做得这么差?
我正在测试 HotSpot JIT 数组绑定检查消除功能。下面是同一个堆排序实现的两个版本,一个使用普通数组索引,另一个sun.misc.Unsafe
API,没有绑定检查:
在 Intel SB 和 AMD K10 CPU 上,Unsafe 版本始终快 13% 。
我查看了生成的程序集:
- 所有索引操作都消除了下限检查 (1-8)
- 仅针对操作 5 消除上限检查,合并 2 和 3 的检查
- 是的,对于每次迭代的操作 4 (
arr[0]
) ,检查arr.length != 0
显然,所有绑定检查分支都被完美预测,这就是为什么使用简单索引的堆排序比不安全的仅慢 13%。
我认为 JIT 的工作至少是优化操作 1、2 和 3 的边界检查,其中索引从数组长度以下的某个值稳步递减到零。
问题就在标题中:为什么 HotSpot JIT 在这种情况下在消除绑定检查方面做得这么差?
arrays - 通过溢出中断实现循环边界检查
我今天有一个想法,可以通过构造一个迭代计数器来在数组上实现边界检查循环,该计数器将在最后一个增量时溢出并根据生成的溢出中断停止执行。
所以假设你有一个数组,例如int[32]
,你想迭代它。为了避免每次循环运行中的边界检查,您现在可以做的是为溢出中断注册一个中断处理程序并为该值分配一个寄存器MAX - 32
。该寄存器在每次循环迭代运行时递增,最后一次迭代运行将溢出,即触发中断处理程序。假设中断处理程序可以增加原始函数的程序计数器,这种机制可以用来避免边界检查。
所以像这样的代码
可以像这样实现
我不知道这是否可行,但我听说 Java 正在做类似的事情来避免在运行时生成的代码中进行空检查。您认为这是可行的还是任何语言运行时都考虑过/尝试过?
loops - 删除 Rust 循环中的边界检查以尝试达到最佳编译器输出
为了确定我是否可以/应该使用 Rust 而不是默认的 C/C++,我正在研究各种边缘情况,主要是考虑到这个问题:在 0.1% 确实重要的情况下,我总能得到编译器输出和 gcc 一样好(带有适当的优化标志)?答案很可能是否定的,但让我们看看...
在Reddit上有一个相当特殊的例子,它研究了无分支排序算法的子例程的编译器输出。
这是基准 C 代码:
这是带有编译器输出的godbolt链接。
Rust 的第一次尝试如下所示:
有相当多的边界检查正在进行,请参阅Godbolt。
下一次尝试消除了第一次边界检查:
这稍微好一点(参见上面相同的godbolt链接)。
最后,让我们尝试完全删除边界检查:
这会产生与 相同的输出foo1
,因此ptr::replace
仍会执行边界检查。unsafe
在这些操作中,我当然超出了我的深度。这导致了我的两个问题:
- 如何消除边界检查?
- 像这样分析边缘情况是否有意义?或者,如果提供整个算法而不是其中的一小部分,Rust 编译器会看穿这一切。
关于最后一点,我很好奇,总的来说,Rust 是否可以被屠杀到“字面”的程度,即像 C 一样接近金属。经验丰富的 Rust 程序员可能会对这种调查感到畏缩,但它是……
java - 当通过 and 限制索引范围时,Hotspot 可以消除边界检查吗?
考虑以下函数:
现代 HotSpot 能否消除对lookup[i & 0xFF]
访问的边界检查?此访问不能越界,因为i & 0xFF
在 0-255 范围内,并且数组有 256 个元素。
performance - String 构造函数中缺少边界检查消除?
查看 UTF8 解码性能,我注意到 protobuf 的性能UnsafeProcessor::decodeUtf8
优于String(byte[] bytes, int offset, int length, Charset charset)
以下非 ascii 字符串"Quizdeltagerne spiste jordbær med flØde, mens cirkusklovnen"
:
我试图找出原因,所以我复制了相关代码String
并将数组访问替换为不安全的数组访问,与UnsafeProcessor::decodeUtf8
. 以下是 JMH 基准测试结果:
我认为差异是由于缺少我希望启动的边界检查消除,特别是因为checkBoundsOffCount(offset, length, bytes.length)
在String(byte[] bytes, int offset, int length, Charset charset)
.
问题真的是缺少边界检查消除吗?
这是我使用 OpenJDK 17 和 JMH 进行基准测试的代码。请注意,这只是String(byte[] bytes, int offset, int length, Charset charset)
构造函数代码的一部分,并且仅适用于这个特定的德语字符串。静态方法是从String
. 查找// the unsafe version:
表明我将安全访问替换为不安全的位置的注释。
跟进
显然,如果我将 while 循环条件从 更改为offset < sl
,0 <= offset && offset < sl
我会在两个版本中获得相似的性能:
结论
这个问题被 HotSpot 开发人员选为https://bugs.openjdk.java.net/browse/JDK-8278518。
优化此代码最终使解码上述 Latin1 字符串的速度提高了 2.5 倍。
这种 C2 优化缩小了以下基准之间令人难以置信的超过7 倍的差距,commonBranchFirst
并将commonBranchSecond
在 Java 19 中实现。