StringBuilder中的按位运算符优势

时间:2015-02-03 08:53:15

标签: java algorithm

为什么reverse() / StringBuffer类中的StringBuilder方法使用按位运算符?

我想知道它的优点。

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;
}    

4 个答案:

答案 0 :(得分:12)

右移一个意味着除以二,我不认为你会发现任何性能差异,编译器在编译时执行这些优化。

许多程序员习惯于在分割而不是写/ 2时右移两次,这是一种风格问题,或者也许有一天右移而不是实际划分{{1 ,(优化之前)。编译器知道如何优化这样的东西,我不会浪费时间去尝试编写其他程序员可能不清楚的东西(除非他们确实有所不同)。无论如何,循环等同于:

/ 2

正如@MarkoTopolnik在他的评论中提到的那样,JDK的编写完全没有考虑任何优化,这可能解释了为什么他们明确地将数字改为1而不是明确地划分它,如果他们考虑优化的最大功率,他们可能会写int n = count - 1; for (int j = (n-1) / 2; j >= 0; --j)


万一你想知道为什么它们是等价的,最好的解释就是例子,考虑数字32.假设8位,它的二进制表示是:

/ 2

右移它:

00100000

,其值为16(1 * 2 4

答案 1 :(得分:4)

它使用(n-1) >> 1而不是(n-1)/2来查找要反转的内部数组的中间索引。按位移位运算符通常比除法运算符更有效。

答案 2 :(得分:4)

总结:

  • Java中的>>运算符称为符号扩展右移位运算符。
  • X >> 1在数学上等同于X / 2,对于X的所有严格正值。
  • X >> 1 总是比X / 2更快,比例大约为1:16,但差异可能变得更多由于现代处理器架构,在实际基准测试中不太重要。
  • 所有主流JVM 都可以正确执行此类优化,但在优化实际发生之前,非优化字节代码将以解释模式执行数千次。
  • JRE源代码使用 lot 优化习语,因为它们对在解释模式下执行的代码(最重要的是,在JVM启动时)产生了重要的区别。
  • 系统使用整个开发团队接受的经过验证的有效代码优化习惯用法并非过早优化

答案很长

以下讨论尝试正确解决本页其他评论中发布的所有问题和疑问。它是如此之长,因为我觉得有必要强调为什么某些方法更好,而不是炫耀个人基准测试结果,信念和实践,其中millage可能因人而异下。

所以,让我们一次提出一个问题。

<强> 1。 Java中的X >> 1(或X << 1X >>> 1)是什么意思?

>><<>>>统称为 Bit Shift 运算符。 >>通常称为符号扩展右移位,或算术右移位>>>非符号扩展右移位(也称为逻辑右移位),而<<只是左位移(符号扩展不适用于该方向,因此不需要逻辑算术变体)。

许多编程语言中都有

Bit Shift 运算符(尽管有不同的符号)(实际上,从快速调查中我会说,几乎所有语言都或多或少都是C语言的后代,加上其他一些)。位移是基本的二进制操作,并且通常,几乎每个创建的CPU都为这些操作提供汇编指令。 Bit Shifters 也是电子设计中的经典建筑块,在给定合理数量的转换器的情况下,它可以在一个步骤中提供最终结果,并具有恒定且可预测的稳定周期时间。

具体而言,位移运算符通过 n 位置移动来转换数字,向左或向右移动 n 位置。 掉线的位被遗忘; &#34;进来的位#34;被强制为0,除了符号扩展右移位的情况,其中最左边的位保留其值(因此其符号)。有关此内容的详细信息,请参阅Wikipedia

<强> 2。 X >> 1是否等于X / 2

是的,只要保证股息是正面的。

更一般地说:

  • 左移N相当于乘以2N;
  • N的逻辑右移相当于2N无符号整数除法;
  • 算术右移N相当于非整数除以2N,舍入为整数为负无穷大(也相当于 2N对任何严格正整数的有符号整数除法

第3。 位移比同等的artihemtic操作更快,在CPU级别?

是的,是的。

首先,我们可以很容易地断言,在CPU的级别上,与同等的算术运算相比,位移确实需要更少的工作。对于乘法和除法都是如此,其原因很简单:整数乘法和整数除法电路本身都包含几个位移位器。换句话说:位移单元仅代表乘法或除法单元的复杂程度的一小部分。因此,保证执行简单的位移而不是完整的算术运算需要较少的能量。然而,最后,除非你监控CPU的耗电量或散热量,否则我怀疑你可能会注意到你的CPU正在消耗更多的能量。

现在,让我们谈谈速度。在具有相当简单架构的处理器上(粗略地说,在奔腾或PowerPC之前设计的任何处理器,以及不具有某种形式的执行流水线的最新处理器),通常实现整数除法(和乘法,在较小程度上)通过在一个操作数上迭代位(实际上是位组,称为基数)。每次迭代都需要一个CPU周期,这意味着32位处理器上的整数除法需要(最多)16个周期(假设 Radix 2 SRT 除法单元,假设的处理器)。乘法单元通常一次处理更多位,因此32位处理器可能在4到8个周期内完成整数乘法。这些单元可能使用某种形式的可变位移位器来快速跳过连续零序列,因此在乘以或除以简单操作数(例如2的正幂)时可能会很快终止;在这种情况下,算术运算将在较少的周期内完成,但仍然需要不止一个简单的位移操作。

显然,指令时序在处理器设计之间有所不同,但先前的比率(位移= 1,乘法= 4,除法= 16)是这些指令的实际性能的合理近似值。作为参考,在Intel 486上,SHR,IMUL和IDIV指令(对于32位,假设通过常数寄存器)分别需要2,13-42和43个周期(有关486条指令的列表,请参阅here他们的时间)。

现代计算机中的CPU怎么样?这些处理器是围绕流水线架构设计的,允许同时执行多个指令;结果是,现在大多数指令只需要一个专用时间的循环。但这是误导性的,因为指令实际上在释放之前会在流水线中保留几个周期,在此期间它们可能会阻止其他指令完成。整数乘法或除法单元保持&#34;保留&#34;在此期间,因此任何进一步的分工都将受阻。这在短循环中尤其是一个问题,其中单个多重复制或除法将最终被之前尚未完成的自身调用所停滞。位移指令不会受到这样的风险:大多数&#34;复杂&#34;处理器可以访问多个位移位单元,并且不需要长时间保留它们(尽管通常至少有2个周期,原因是流水线架构固有的原因)。实际上,为了将其编入数字,快速查看Atom的Intel Optimization Reference Manual似乎表明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实际上在实用代码优化方面撰写了数千页的读写能力。

正如Knuth所说:

  

程序员浪费了大量时间来考虑或担心程序中非关键部分的速度,而这些效率尝试实际上在考虑调试和维护时会产生很大的负面影响。我们应该忘记小的效率,大约97%的时间说:过早的优化是所有邪恶的根源。然而,我们不应该把这个关键的3%的机会放弃。

     

- Donald E. Knuth,&#34;使用Goto语句进行结构化编程&#34;

Knuth认为过早优化的优点是需要大量思考的优化仅适用于程序的非关键部分,对调试和维护有很大的负面影响。现在,所有这些都可以争论很长一段时间,但不要。

然而,应该理解的是,小的局部优化已被证明是有效的(即,至少在整体上是平均的),不会对程序的整体构造产生负面影响,不会减少代码的可维护性,并且不需要无关的思考根本不是坏事。这样的优化实际上是好的,因为它们不会给你带来任何损失,我们也不应该放弃这样的机会。

然而,这是最重要的事情要记住,在一个上下文中对程序员进行琐事的优化可能会变成另一个程序员的 incomprenhendable 上下文。由于这个原因,比特移位和掩蔽习语特别成问题。知道成语的程序员可以阅读并使用它而不需要太多思考,并且证明了这些优化的有效性,尽管通常无关紧要,除非代码包含数百个出现。这些成语很少是错误的实际来源。尽管如此,那些不熟悉特定习语的程序员也会花时间理解特定代码片段的内容,原因和方式。

最后,要么支持这样的优化,要么应该使用哪些成语实际上是团队决策和代码上下文的问题。我个人认为一定数量的习语在所有情况下都是最佳实践,任何加入我团队的新程序员都会很快获得这些成语。更多的习语保留给关键代码路径。放入内部共享代码库的所有代码都被视为关键代码路径,因为它们可能会被证明是从这样的关键代码路径调用的。无论如何,这是我个人的做法,你的耕作可能会有所不同。

答案 3 :(得分:2)

在这种方法中,只有这个表达式:(n-1) >> 1。我认为这是你所指的表达方式。这称为右移/换档。它等同于(n-1)/2,但通常认为它更快,更有效。它也常用于许多其他语言(例如在C / C ++中)。

请注意,即使您使用(n-1)/2之类的分区,现代编译器也会优化您的代码。所以使用右移不存在明显的好处。这更像是编码偏好,风格,习惯的问题。

另见:

Is shifting bits faster than multiplying and dividing in Java? .NET?