在计算低值时,(普通)CPU 是否比计算大值更快?
Is a (common) CPU faster at computing low values than great ones?
问题就这么简单:
将两个低值与加法、除法、模数、位移位等常见的基本运算相结合是否会比具有更大值的相同运算更快地计算?
据我所知,这需要 CPU 跟踪最高有效位(我认为这不太可能),但也许还有其他事情要做。
我特意询问是因为我经常看到一些 Java 的基本 类' hashCode()
方法(例如String
和 List
),这是令人惊讶的,因为更大的值很可能会导致更多的扩散(这通常对哈希函数来说是一件好事)。
算术
我不认为有很多流水线处理器(即几乎所有的流水线处理器,除了最小的)其中简单算术指令的成本会随着寄存器或内存操作数的值而变化。这会使流水线的设计更加复杂,在实践中可能适得其反。
我可以想象,与流水线长度相比可能需要很多周期的非常复杂的指令(至少是一个除法)可能会表现出这种行为,因为它可能会引入等待状态。 Agner Fog 写道这是真的 "on AMD processors, but not on Intel processors."
如果无法在一条指令中计算某项操作,例如大于本机整数宽度的数字乘法,则实现很可能包含 "fast path" 的情况,例如两个操作数的上半部分为零。一个常见的例子是 MSVC 使用的 x86 32 位架构上的 64 位乘法。一些较小的处理器没有除法指令,有时甚至没有乘法指令。对于较小的操作数,用于计算这些操作的程序集可能会提前终止。这种影响在较小的架构上会更加明显。
立即值编码
对于立即值(常量),这可能不同。例如,有 RISC 处理器允许在 load/add-immediate 指令中编码多达 16 位立即数,并且需要两个操作来通过 load-upper-immediate + add-immediate 加载 32 位字,或者必须加载常量来自程序内存。
在 CISC 处理器中,较大的立即值可能会占用更多内存,这可能会减少每个周期可提取的指令数,或增加缓存未命中数。
在这种情况下,较小的常量可能比较大的常量便宜。
我不确定编码差异对 Java 是否同样重要,因为大多数代码至少最初会作为 Java 字节码分发,尽管支持 JIT 的 JVM 会翻译代码到机器代码和一些库 类 可能有预编译的实现。我对 Java 字节码的了解还不够,无法确定常量大小对它的影响。从我读到的内容来看,在我看来,大多数常量通常是通过常量池中的索引加载的,而不是直接在字节码流中编码的,所以我不希望这里有很大的差异,如果有的话。
强度降低优化
对于非常昂贵的操作(相对于处理器而言),编译器和程序员经常使用技巧将硬计算替换为对常数有效的更简单的计算,例如在提到的乘法示例中,乘法被替换为shift 和 subtraction/addition.
在给出的示例中(乘以 31 与乘以 65,537),我预计不会有差异。对于其他数字会有差异,但它不会与数字的大小完全相关。除以常数的除法通常也被神秘的乘法和移位序列所取代。
例如,请参阅 gcc 如何翻译 division by 13。
在 x86 处理器上,一些小常量的乘法可以用 load-effective-address 指令代替,但仅限于某些常量。
总而言之,我希望这种效果在很大程度上取决于处理器架构和要执行的操作。由于 Java 几乎在任何地方都应该 运行,我认为库作者希望他们的代码在大范围的处理器上高效,包括小型嵌入式处理器,其中操作数大小将发挥更大的作用。
问题就这么简单: 将两个低值与加法、除法、模数、位移位等常见的基本运算相结合是否会比具有更大值的相同运算更快地计算?
据我所知,这需要 CPU 跟踪最高有效位(我认为这不太可能),但也许还有其他事情要做。
我特意询问是因为我经常看到一些 Java 的基本 类' hashCode()
方法(例如String
和 List
),这是令人惊讶的,因为更大的值很可能会导致更多的扩散(这通常对哈希函数来说是一件好事)。
算术
我不认为有很多流水线处理器(即几乎所有的流水线处理器,除了最小的)其中简单算术指令的成本会随着寄存器或内存操作数的值而变化。这会使流水线的设计更加复杂,在实践中可能适得其反。
我可以想象,与流水线长度相比可能需要很多周期的非常复杂的指令(至少是一个除法)可能会表现出这种行为,因为它可能会引入等待状态。 Agner Fog 写道这是真的 "on AMD processors, but not on Intel processors."
如果无法在一条指令中计算某项操作,例如大于本机整数宽度的数字乘法,则实现很可能包含 "fast path" 的情况,例如两个操作数的上半部分为零。一个常见的例子是 MSVC 使用的 x86 32 位架构上的 64 位乘法。一些较小的处理器没有除法指令,有时甚至没有乘法指令。对于较小的操作数,用于计算这些操作的程序集可能会提前终止。这种影响在较小的架构上会更加明显。
立即值编码
对于立即值(常量),这可能不同。例如,有 RISC 处理器允许在 load/add-immediate 指令中编码多达 16 位立即数,并且需要两个操作来通过 load-upper-immediate + add-immediate 加载 32 位字,或者必须加载常量来自程序内存。
在 CISC 处理器中,较大的立即值可能会占用更多内存,这可能会减少每个周期可提取的指令数,或增加缓存未命中数。
在这种情况下,较小的常量可能比较大的常量便宜。
我不确定编码差异对 Java 是否同样重要,因为大多数代码至少最初会作为 Java 字节码分发,尽管支持 JIT 的 JVM 会翻译代码到机器代码和一些库 类 可能有预编译的实现。我对 Java 字节码的了解还不够,无法确定常量大小对它的影响。从我读到的内容来看,在我看来,大多数常量通常是通过常量池中的索引加载的,而不是直接在字节码流中编码的,所以我不希望这里有很大的差异,如果有的话。
强度降低优化
对于非常昂贵的操作(相对于处理器而言),编译器和程序员经常使用技巧将硬计算替换为对常数有效的更简单的计算,例如在提到的乘法示例中,乘法被替换为shift 和 subtraction/addition.
在给出的示例中(乘以 31 与乘以 65,537),我预计不会有差异。对于其他数字会有差异,但它不会与数字的大小完全相关。除以常数的除法通常也被神秘的乘法和移位序列所取代。
例如,请参阅 gcc 如何翻译 division by 13。
在 x86 处理器上,一些小常量的乘法可以用 load-effective-address 指令代替,但仅限于某些常量。
总而言之,我希望这种效果在很大程度上取决于处理器架构和要执行的操作。由于 Java 几乎在任何地方都应该 运行,我认为库作者希望他们的代码在大范围的处理器上高效,包括小型嵌入式处理器,其中操作数大小将发挥更大的作用。