单行字符串连接的速度差异
Posted
技术标签:
【中文标题】单行字符串连接的速度差异【英文标题】:Speed difference for single line String concatenation 【发布时间】:2013-05-19 07:37:51 【问题描述】:所以我一直lead to believe 认为,使用“+”运算符将字符串附加到单行上与使用 StringBuilder 一样有效(而且肯定更好看)。今天,虽然我在使用附加变量和字符串的 Logger 时遇到了一些速度问题,但它使用的是“+”运算符。所以我做了一个快速的test case,令我惊讶的是发现使用 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 -与最快算法的百分比差异图。
我查看了byte code,每个字符串比较方法都不同。
这是我使用的方法,你可以看到整个测试类here。
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);
我现在已经尝试过使用浮点数、整数和字符串。所有这些都显示出或多或少相同的时差。
问题
-
“+”运算符显然没有变成相同的字节码,时间与最优相差很大。那么给了什么?
100 到 10000 次追加之间的算法行为对我来说很奇怪,所以有人解释一下吗?
【问题讨论】:
算法在 100 和....之间的行为??? 已修复,由于某种原因它切断了它 很好的问题,有研究和数据支持。 +1 当你运行这些时,你在优化吗?通常,未优化的运算符和函数调用可能会在内存中表现异常,并导致在适当优化时不存在的异常性能。 所以我在我的电脑 Mac 上使用标准默认编译。java version "1.6.0_45"
。我没有使用任何特定的优化标志
【参考方案1】:
我不喜欢你的测试用例的两点。首先,您在同一进程中运行了所有测试。在处理“大”(我知道模棱两可)时,但在处理您的进程如何与内存交互是您主要关注的任何事情时,您应该始终在单独的运行中进行基准测试。只是我们已经启动了垃圾收集这一事实可能会影响早期运行的结果。您考虑结果的方式让我感到困惑。我所做的是在单独的跑步中进行每一次跑步,并将跑步次数减少为零。我还让它运行了许多“代表”,为每个代表计时。然后打印出每次运行所用的毫秒数。这是我的代码:
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 中移动的大字符串要小。
简化结论:您的“大型”运行结果相当有效,并且看起来与我的相似(尽管我仍然不完全了解您是如何获得结果的,但相比之下百分比似乎很好地排列)。但是,由于您的测试性质,您的较小运行结果无效。
【讨论】:
我认为你对分支预测器有所了解,我认为这是 gc 倾斜了较小的运行,但我真的很喜欢你从运行 1,2 到 3 的下降......这说明了体积对我来说。至于较大的运行,您显示的效果相同,方法不等效 您在这里假设了许多假设,其中大多数肯定是错误的。小批量加速的最明显原因是 JVM 从解释字节码开始,经过几次运行后,字节码被编译为本机机器码(导致一次性惩罚,但在随后的运行),在运行更多次之后,JVM 甚至可能根据收集的运行时统计信息使用不同的优化策略多次重新编译字节码。 另外,我不确定你的结果是什么意思。我只是将每次重复的时间加起来,类似于你所做的,但使用 +=,然后除以最后的重复次数。 @jarnbjo:我的意思是您考虑结果的方式(措辞不佳)是您的百分比。我不明白你展示结果的方法,我又看了一遍,现在更有意义了! @jarnbjo:我并不是说我的假设是我的结论是确定性的(注意“我相信”),我是在分享结果并猜测原因。我更像是一个 C++ 人而不是一个 java 人。您的解释似乎很合理,尽管我怀疑分支预测和内存管理与它无关,而且我确实提到 JVM 可能会放大这些影响,但我只是不知道 JVM 的内部结构更简洁条款。很可能是两种解释的结合【参考方案2】: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 当然可能会产生一些影响,但与例如所需的努力相比应该是微不足道的。格式化浮点数。
【讨论】:
我已经反编译了代码,“+”运算符与StringBuilder()不同,虽然我没有尝试过StringBuilder(“[”);,我看看 您可以将 javap 的输出添加到您的问题中吗? 我也在问题中链接了它。 gist.github.com/parnell/5c34f244e6f4588cc9ac。它们都非常相似,但在大批量运行时,一些差异似乎开始导致明显的差异。 如果我没有完全错,根据字节码,您的 stringSpeed5 方法实现为StringBuilder sb = new StringBuilder("[")... ; return sb.toString();
与字符串连接的等效方法将是 String s = "[" + ...; return s;
。如果不使用局部变量,而是直接将第 5 种方法实现为return new StringBuilder("[")...toString();
,则应该得到相同的字节码。
这看起来很神奇。 "new StringBuilder("[");"之间并直接返回最终使字节码等效。这对我来说意味着两件事。 1)另一个问题的答案仍然是错误的,因为原始发布者分配了一个新的 StringBuilder(capacity) 并返回它,这不会导致等效的字节码。 2) 我对 java 编译器不够聪明,无法分配一个新对象并立即返回它只是返回它感到非常失望......我确信这是它自动完成的。以上是关于单行字符串连接的速度差异的主要内容,如果未能解决你的问题,请参考以下文章
Clojure HoneySQL - 如何在连接后将字符串值聚合到单行中?
MySQL 查询:连接表并将记录显示为单行中的逗号分隔字符串
Microsoft Access ODBC 连接:连接字符串差异