为什么/ classes 中的reverse()方法使用按位运算符?StringBufferStringBuilder
我想知道它的优点.
public AbstractStringBuilder reverse() {
boolean hasSurrogate = false;
int n = count - 1;
for (int j = (n-1) >> 1; j >= 0; --j) {
char temp = value[j];
char temp2 = value[n - j];
if (!hasSurrogate) {
hasSurrogate = (temp >= Character.MIN_SURROGATE && temp <= Character.MAX_SURROGATE)
|| (temp2 >= Character.MIN_SURROGATE && temp2 <= Character.MAX_SURROGATE);
}
value[j] = temp2;
value[n - j] = temp;
}
if (hasSurrogate) {
// Reverse back all valid surrogate pairs
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
return this;
}
Run Code Online (Sandbox Code Playgroud)
Mar*_*oun 12
右移一个意味着除以二,我认为你不会注意到任何性能差异,编译器将在编译时执行这些优化.
许多程序员习惯于在分割而不是写作时右移两个/ 2,这是一种风格问题,或者有一天右移而不是通过写作实际划分/ 2(在优化之前)真的更有效.编译器知道如何优化这样的东西,我不会浪费时间去尝试编写其他程序员可能不清楚的东西(除非他们确实有所不同).无论如何,循环相当于:
int n = count - 1;
for (int j = (n-1) / 2; j >= 0; --j)
Run Code Online (Sandbox Code Playgroud)
正如@MarkoTopolnik在他的评论中提到的那样,JDK的编写完全没有考虑任何优化,这可能解释了为什么他们明确地将数字改为1而不是明确地将其划分,如果他们考虑优化的最大功率,他们可能会有写道/ 2.
万一你想知道为什么它们是等价的,最好的解释是例子,考虑数字32.假设8位,它的二进制表示是:
00100000
Run Code Online (Sandbox Code Playgroud)
把它改为一个:
00010000
Run Code Online (Sandbox Code Playgroud)
其值为16(1*2 4)
总之:
>>Java 中的运算符称为符号扩展右移运算符。X >> 1在数学上等价于X / 2,对于 X 的所有严格正值。X >> 1是总是快比X / 2,在大约1:16的比例,但这种差别可能会变成是在实际的基准更显著由于现代处理器架构。长答案
以下讨论尝试正确解决本页其他评论中提出的所有问题和疑虑。这么长是因为我觉得有必要强调为什么某些方法更好,而不是炫耀个人基准结果、信念和实践,因为每个人的差距可能会很大。
所以让我们一次回答一个问题。
1. Java 中X >> 1(or X << 1, or X >>> 1) 是什么意思?
的>>,<<并且>>>被统称为位移位运算符。>>通常称为符号扩展右移,或算术右移。>>>是无符号扩展右位移位(也称为逻辑右位移位),并且<<仅仅是左位移位(符号扩展并没有在这个方向应用,所以没有必要的逻辑和算法变种)。
在许多编程语言中都可以使用位移运算符(尽管使用不同的符号)(实际上,根据我的快速调查,几乎每种语言或多或少都是 C 语言的后代,以及其他一些语言)。位移位是基本的二进制运算,因此,几乎每个曾经创建的 CPU 都提供了这些指令的汇编指令。移位器也是电子设计中的经典构建块,它在给定合理数量的晶体管的情况下,一步提供最终结果,并具有恒定且可预测的稳定周期时间。
具体而言,位移运算符通过将数字的所有位向左或向右移动n 个位置来转换数字。掉下来的比特被遗忘;“进来”的位被强制为 0,除非在符号扩展右移的情况下,其中最左边的位保留其值(因此其符号)。有关此的一些图形,请参阅维基百科。
2. 是否X >> 1等于X / 2?
是的,只要保证股息为正。
更普遍:
N等价于乘以;2NN相当于一个无符号的整数除法通过;2NN相当于一个非整数由除法,舍入为向负无穷大(这也相当于一个整数符号整数除法通过任何严格的正整数)。2N2N3.在 CPU 级别,位移是否比等效的算术运算快?
是的。
首先,我们可以很容易地断言,在 CPU 级别,位移位确实比等效的算术运算需要更少的工作。这对于乘法和除法都是正确的,原因很简单:整数乘法和整数除法电路本身都包含几个位移位器。换句话说:位移单元仅代表乘法或除法单元复杂度级别的一小部分。因此可以保证执行简单的位移而不是完整的算术运算所需的能量更少。但是,最后,除非您监控 CPU 的耗电量或散热量,否则我怀疑您是否会注意到 CPU 消耗更多能量的事实。
现在,让我们谈谈速度。在具有合理简单架构的处理器上(大致是在 Pentium 或 PowerPC 之前设计的任何处理器,以及不具有某种形式的执行管道的最新处理器),通常实现整数除法(和乘法,在较小程度上)通过在其中一个操作数上迭代位(实际上是一组位,称为基数)。每次迭代需要一个 CPU 周期,这意味着在 32 位处理器上进行整数除法将需要(最多)16 个周期(假设基数为 2 SRT除法单元,在一个假设的处理器上)。乘法单元通常一次处理更多位,因此 32 位处理器可能会在 4 到 8 个周期内完成整数乘法。这些单元可能使用某种形式的可变位移位器来快速跳过连续的零序列,因此在乘以或除以简单的操作数(例如 2 的正幂)时可能会快速终止;在这种情况下,算术运算将在更少的周期内完成,但仍然需要比简单的位移操作更多的内容。
显然,指令时序因处理器设计而异,但前面的比率(位移 = 1,乘法 = 4,除法 = 16)是这些指令实际性能的合理近似值。作为参考,在 Intel 486 上,SHR、IMUL 和 IDIV 指令(对于 32 位,假设寄存器为常量)分别需要 2、13-42 和 43 个周期(有关 486 条指令及其时序的列表,请参见此处)。
现代计算机中的 CPU 怎么样?这些处理器围绕允许同时执行多条指令的流水线架构而设计;结果是现在大多数指令只需要一个周期的专用时间。但这是一种误导,因为指令实际上在释放之前会在管道中保留几个周期,在此期间它们可能会阻止其他指令完成。整数乘法或除法单元在这段时间内保持“保留”,因此任何进一步的除法都将被阻止。这在短循环中尤其成问题,在这种情况下,单个乘法或除法最终会因之前尚未完成的自身调用而停滞。位移位指令不会遭受这种风险:大多数“复杂”处理器可以访问多个位移位单元,并且不需要将它们保留很长时间(尽管由于流水线架构固有的原因,通常至少需要 2 个周期)。实际上,要将其转化为数字,请快速查看Intel Optimization Reference Manual for the Atom 似乎表明 SHR、IMUL 和 IDIV(与上述参数相同)分别具有 2、5 和 57 个延迟周期;对于 64 位操作数,它是 8、14 和 197 个周期。类似的延迟适用于最新的英特尔处理器。
所以,是的,位移比等效的算术运算更快,即使在某些情况下,在现代处理器上,它实际上可能完全没有区别。但在大多数情况下,它是非常重要的。
4. Java 虚拟机会为我做这样的优化吗?
当然,会的。嗯……当然,而且……最终。
与大多数语言编译器不同,常规 Java 编译器不执行优化。人们认为 Java 虚拟机最适合决定如何针对特定执行上下文优化程序。这确实在实践中提供了良好的结果。JIT 编译器获得对代码动态的非常深入的理解,并利用这些知识来选择和应用大量次要代码转换,以生成非常高效的本机代码。
但是将字节码编译成优化的本地方法需要大量的时间和内存。这就是为什么 JVM 在执行数千次之前甚至不会考虑优化代码块的原因。然后,即使已安排代码块进行优化,编译器线程可能需要很长时间才能真正处理该方法。之后,各种条件可能会导致优化的代码块被丢弃,恢复到字节码解释。
尽管 JSE API 的设计目标是可以由各种供应商实现,但声称 JRE 也是如此是不正确的。Oracle JRE 作为参考实现提供给其他人,但不鼓励将其用于其他 JVM(实际上,在不久前,Oracle 开源 JRE 的源代码之前,它是被禁止的)。
JRE 源代码中的优化是 JRE 开发人员采用约定和优化努力的结果,即使在 JIT 优化还没有或根本无法帮助的情况下也能提供合理的性能。例如,在调用 main 方法之前加载了数百个类。那个时候,JIT 编译器还没有获得足够的信息来正确优化代码。在这种情况下,手工优化会产生重要影响。
5. 这不是过早的优化吗?
是的,除非有理由不这样做。
现代生活中的一个事实是,每当一个程序员在某处展示代码优化时,另一个程序员都会反对 Donald Knuth 关于优化的名言(嗯,是他的吗?谁知道...)它甚至被许多人认为是Knuth 认为我们永远不应该尝试优化代码。不幸的是,这是对 Knuth 在过去几十年对计算机科学的重要贡献的重大误解:Knuth 实际上撰写了数千页关于实用代码优化的知识。
正如克努斯所说:
程序员浪费了大量时间来考虑或担心他们程序中非关键部分的速度,而在考虑调试和维护时,这些提高效率的尝试实际上会产生强烈的负面影响。我们应该忘记小效率,比如大约 97% 的时间:过早的优化是万恶之源。然而,我们不应该错过关键的 3% 的机会。
— Donald E. Knuth,“使用 Goto 语句的结构化编程”
Knuth 被称为过早优化的优化是需要大量思考并且仅适用于程序的非关键部分的优化,并且对调试和维护具有强烈的负面影响。现在,所有这些都可以争论很长时间,但我们不要。
然而,应该理解的是,已被证明是有效的(即,至少在总体上平均而言)的小局部优化不会对程序的整体构造产生负面影响,也不会降低代码的可维护性,并且不需要多余的思考也不是什么坏事。这样的优化其实很好,因为它们没有任何成本,我们不应该放弃这样的机会。
然而,这是要记住的最重要的事情,在一种情况下对程序员来说微不足道的优化可能在另一种情况下对程序员来说是不可理解的。出于这个原因,位移位和屏蔽习惯用法尤其成问题。知道这个习语的程序员可以不加思考地阅读和使用它,并且这些优化的有效性得到了证明,尽管除非代码包含数百次出现,否则通常是微不足道的。这些习语很少是错误的实际来源。尽管如此,不熟悉特定习语的程序员会浪费时间了解特定代码片段的作用、原因和方式。
最后,要么支持这种优化,要么到底应该使用哪些习语,实际上是团队决策和代码上下文的问题。我个人认为一定数量的习语在所有情况下都是最佳实践,任何加入我团队的新程序员都会很快掌握这些习语。更多的习语被保留给关键代码路径。放入内部共享代码库的所有代码都被视为关键代码路径,因为它们可能会从这样的关键代码路径调用。无论如何,这是我个人的做法,您的目标可能会有所不同。