9

因此,我一直相信使用“+”运算符将字符串附加到一行上与使用 StringBuilder 一样有效(而且在视觉上肯定更好)。今天虽然我在附加变量和字符串的记录器上遇到了一些速度问题,但它使用了“+”运算符。所以我做了一个快速的测试用例,令我惊讶的是发现使用 StringBuilder 更快!

基础是我使用 4 种不同的方法(如下所示)对每个附加数量使用 20 次运行的平均值。

结果,时间(以毫秒为单位)

                                               # 追加
                           10^1 10^2 10^3 10^4 10^5 10^6 10^7
StringBuilder(容量) 0.65 1.25 2 11.7 117.65 1213.25 11570
StringBuilder() 0.7 1.2 2.4 12.15 122 1253.7 12274.6
“+”运算符 0.75 0.95 2.35 12.35 127.2 1276.5 12483.4
字符串格式 4.25 13.1 13.25 71.45 730.6 7217.15 -

与最快算法的差异百分比图。

字符串计时的百分比差异

我检查了字节码,每种字符串比较方法都不同。

这是我用于方法的内容,您可以在此处查看整个测试类。

public static String stringSpeed1(float a, float b, float c, float x, float y, float z){
    StringBuilder sb = new StringBuilder(72).append("[").append(a).append(",").append(b).append(",").append(c).append("][").
            append(x).append(",").append(y).append(",").append(z).append("]");
    return sb.toString();
}

public static String stringSpeed2(float a, float b, float c, float x, float y, float z){
    StringBuilder sb = new StringBuilder().append("[").append(a).append(",").append(b).append(",").append(c).append("][").
            append(x).append(",").append(y).append(",").append(z).append("]");
    return sb.toString();
}

public static String stringSpeed3(float a, float b, float c, float x, float y, float z){
    return "["+a+","+b+","+c+"]["+x+","+y+","+z+"]";
}

public static String stringSpeed4(float a, float b, float c, float x, float y, float z){
    return String.format("[%f,%f,%f][%f,%f,%f]", a,b,c,x,y,z);
}

我现在尝试过使用浮点数、整数和字符串。所有这些都或多或少地显示出相同的时差。

问题

  1. “+”运算符显然不是变成同一个字节码,时间与最优相差很大。那么给了什么?
  2. 100 到 10000 个附加数之间的算法行为对我来说很奇怪,所以有人解释一下吗?
4

2 回答 2

4

关于您的测试用例,我不喜欢两件事。首先,您在同一进程中运行了所有测试。在处理“大”(我知道模棱两可)时,但在处理您的进程如何与内存交互是您主要关注的任何事情时,您应该始终在单独的运行中进行基准测试。只是我们已经启动了垃圾收集这一事实可能会影响早期运行的结果。您考虑结果的方式让我感到困惑。我所做的是在单独的跑步中进行每一次跑步,并将跑步次数减少为零。我还让它运行了许多“代表”,为每个代表计时。然后打印出每次运行所用的毫秒数。这是我的代码:

import java.util.Random;

public class blah {
  public static void main(String[] args){
    stringComp();
    }

    private static void stringComp() {
        int SIZE = 1000000;
        int NUM_REPS = 5;
        for(int j = 0; j < NUM_REPS; j++) {
            Random r = new Random();
            float f;
            long start = System.currentTimeMillis();
            for (int i=0;i<SIZE;i++){
                f = r.nextFloat();
                stringSpeed3(f,f,f,f,f,f);
            }
            System.out.print((System.currentTimeMillis() - start));
            System.out.print(", ");
        }
    }

    public static String stringSpeed1(float a, float b, float c, float x, float y, float z){
        StringBuilder sb = new StringBuilder(72).append("[").append(a).append(",").append(b).append(",").append(c).append("][").
                append(x).append(",").append(y).append(",").append(z).append("]");
        return sb.toString();
    }

    public static String stringSpeed2(float a, float b, float c, float x, float y, float z){
        StringBuilder sb = new StringBuilder().append("[").append(a).append(",").append(b).append(",").append(c).append("][").
                append(x).append(",").append(y).append(",").append(z).append("]");
        return sb.toString();
    }

    public static String stringSpeed3(float a, float b, float c, float x, float y, float z){
        return "["+a+","+b+","+c+"]["+x+","+y+","+z+"]";
    }

    public static String stringSpeed4(float a, float b, float c, float x, float y, float z){
        return String.format("[%f,%f,%f][%f,%f,%f]", a,b,c,x,y,z);
    }

}

现在我的结果:

stringSpeed1(SIZE = 10000000): 11548, 11305, 11362, 11275, 11279
stringSpeed2(SIZE = 10000000): 12386, 12217, 12242, 12237, 12156
stringSpeed3(SIZE = 10000000): 12313, 12016, 12073, 12127, 12038

stringSpeed1(SIZE = 1000000): 1292, 1164, 1170, 1168, 1172
stringSpeed2(SIZE = 1000000): 1364, 1228, 1230, 1224, 1223
stringSpeed3(SIZE = 1000000): 1370, 1229, 1227, 1229, 1230

stringSpeed1(SIZE = 100000): 246, 115, 115, 116, 113
stringSpeed2(SIZE = 100000): 255, 122, 123, 123, 121
stringSpeed3(SIZE = 100000): 257, 123, 129, 124, 125

stringSpeed1(SIZE = 10000): 113, 25, 14, 13, 13
stringSpeed2(SIZE = 10000): 118, 23, 24, 16, 14
stringSpeed3(SIZE = 10000): 120, 24, 16, 17, 14

//This run SIZE is very interesting.  
stringSpeed1(SIZE = 1000): 55, 22, 8, 6, 4 
stringSpeed2(SIZE = 1000): 54, 23, 7, 4, 3
stringSpeed3(SIZE = 1000): 58, 23, 7, 4, 4

stringSpeed1(SIZE = 100): 6, 6, 6, 6, 6 
stringSpeed2(SIZE = 100): 6, 6, 5, 6, 6
stirngSpeed3(SIZE = 100): 8, 6, 7, 6, 6

正如您从我的结果中看到的那样,在“中间范围”内的值上,每个连续的代表都会变得更快。我相信,这可以通过 JVM 运行并获取所需的内存来解释。随着“大小”的增加,这种影响是不允许接管的,因为有太多的内存让垃圾收集器放手,让进程重新锁定。此外,当您进行这样的“重复”基准测试时,当您的大部分进程可以存在于较低级别的缓存中而不是 RAM 中时,您的进程对分支预测器更加敏感。这些非常聪明,并且会捕捉到您的进程正在做什么,我想 JVM 会放大这一点。这也有助于解释为什么初始循环上的值较慢,以及为什么您接近基准测试的方式是一个糟糕的解决方案。这就是为什么我认为你的值不是“大”的结果是倾斜的并且看起来很奇怪。然后,随着您的基准测试的“内存占用”增加,这个分支预测的影响(百分比)比您在 RAM 中移动的大字符串的影响要小。

简化结论:您的“大”运行结果相当有效,并且看起来与我的相似(尽管我仍然不完全了解您如何获得结果,但相比之下百分比似乎很好地排列)。但是,由于测试的性质,您的较小运行结果无效。

于 2013-05-23T20:33:19.933 回答
3

Java语言规范没有指定如何执行字符串连接,但我怀疑你的编译器除了相当于:

new StringBuilder("[").
  append(a).
  append(",").
  append(b).
  append(",").
  append(c).
  append("][").
  append(x).
  append(",").
  append(y).
  append(",").
  append(z).
  append("]").
  toString();

您可以使用“javap -c ...”来反编译您的类文件并验证这一点。

如果您测量方法之间运行时的任何显着且重复的差异,我宁愿假设垃圾收集器在不同的时间运行,而不是存在任何实际的显着性能差异。创建StringBuilder具有不同初始容量的 s 当然可能会产生一些影响,但与格式化浮点数所需的努力相比,它应该是微不足道的。

于 2013-05-23T20:31:28.437 回答