【问题标题】:What's the fastest way to concatenate two Strings in Java?在 Java 中连接两个字符串的最快方法是什么?
【发布时间】:2011-07-01 21:29:42
【问题描述】:

在 Java 中连接两个字符串的最快方法是什么?

String ccyPair = ccy1 + ccy2;

我使用cyPair 作为HashMap 中的键,并在一个非常紧凑的循环中调用它来检索值。

当我分析时,这就是瓶颈

java.lang.StringBuilder.append(StringBuilder.java:119)  
java.lang.StringBuilder.(StringBuilder.java:93)

【问题讨论】:

  • 字符串连接的瓶颈?这意味着所有 Java 程序都存在性能问题。不要微优化。
  • 但是他已经分析了代码,这就是瓶颈。这不是微优化,也不是过早的优化,它只是优化。
  • @Duncan,实际上这是问题之一。真正的问题是将 ccy 代码生成到循环中。它包含多个分配+内存屏障,+不是那么快的哈希码(14 mul+add;假设 ccy 对就像“eur/usdusd/jpy”),然后等于。使用带有对 2 个字符串的引用的保持对将是一个更好的解决方案。

标签: java performance string


【解决方案1】:

大量理论 - 是时候练习一下了!

private final String s1 = new String("1234567890");
private final String s2 = new String("1234567890");

在预热的 64 位 Hotspot 上使用 10,000,000 的普通 for 循环,在 Intel Mac OS 上使用 1.6.0_22。

例如

@Test public void testConcatenation() {
    for (int i = 0; i < COUNT; i++) {
        String s3 = s1 + s2;
    }
}

在循环中使用以下语句

String s3 = s1 + s2; 

1.33 秒

String s3 = new StringBuilder(s1).append(s2).toString();

1.28 秒

String s3 = new StringBuffer(s1).append(s2).toString();

1.92s

String s3 = s1.concat(s2);

0.70s

String s3 = "1234567890" + "1234567890";

0.0s

所以 concat 是明显的赢家,除非你有静态字符串,在这种情况下编译器已经处理好了你。

【讨论】:

  • 代码将被完全优化,因此您正在有效地测试未优化的代码。这就是你不写微基准的方式。尽管如此,对于 2 个字符串,String.contact 应该是最快的。
  • 我对没有进一步检查结果感到内疚,因为它们正是我所期望的!但我不明白我如何测试未优化的代码。如果 Hotspot 正在删除没有副作用的代码,所有这些循环将花费相同的时间,如果不是,那么我正在测试运行语句(加上循环)的时间。我们不知道循环所花费的时间,但我手上没有太多时间我没有考虑到这一点;-)
  • @DuncanMcGregor JVM 优化代码需要一段时间。
  • StringBuilder 是一种快速处理大字符串的方法,但速度慢。
【解决方案2】:

我相信答案可能已经确定,但我发布代码是为了分享。

简短的回答,如果您要寻找的只是纯串联,那就是:String.concat(...)

输出:

ITERATION_LIMIT1: 1
ITERATION_LIMIT2: 10000000
s1: STRING1-1111111111111111111111
s2: STRING2-2222222222222222222222

iteration: 1
                                          null:    1.7 nanos
                                 s1.concat(s2):  106.1 nanos
                                       s1 + s2:  251.7 nanos
   new StringBuilder(s1).append(s2).toString():  246.6 nanos
    new StringBuffer(s1).append(s2).toString():  404.7 nanos
                 String.format("%s%s", s1, s2): 3276.0 nanos

Tests complete

示例代码:

package net.fosdal.scratch;

public class StringConcatenationPerformance {
    private static final int    ITERATION_LIMIT1    = 1;
    private static final int    ITERATION_LIMIT2    = 10000000;

    public static void main(String[] args) {
        String s1 = "STRING1-1111111111111111111111";
        String s2 = "STRING2-2222222222222222222222";
        String methodName;
        long startNanos, durationNanos;
        int iteration2;

        System.out.println("ITERATION_LIMIT1: " + ITERATION_LIMIT1);
        System.out.println("ITERATION_LIMIT2: " + ITERATION_LIMIT2);
        System.out.println("s1: " + s1);
        System.out.println("s2: " + s2);
        int iteration1 = 0;
        while (iteration1++ < ITERATION_LIMIT1) {
            System.out.println();
            System.out.println("iteration: " + iteration1);

            // method #0
            methodName = "null";
            iteration2 = 0;
            startNanos = System.nanoTime();
            while (iteration2++ < ITERATION_LIMIT2) {
                method0(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #1
            methodName = "s1.concat(s2)";
            iteration2 = 0;
            startNanos = System.nanoTime();
            while (iteration2++ < ITERATION_LIMIT2) {
                method1(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #2
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "s1 + s2";
            while (iteration2++ < ITERATION_LIMIT2) {
                method2(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #3
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "new StringBuilder(s1).append(s2).toString()";
            while (iteration2++ < ITERATION_LIMIT2) {
                method3(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #4
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "new StringBuffer(s1).append(s2).toString()";
            while (iteration2++ < ITERATION_LIMIT2) {
                method4(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

            // method #5
            iteration2 = 0;
            startNanos = System.nanoTime();
            methodName = "String.format(\"%s%s\", s1, s2)";
            while (iteration2++ < ITERATION_LIMIT2) {
                method5(s1, s2);
            }
            durationNanos = System.nanoTime() - startNanos;
            System.out.println(String.format("%50s: %6.1f nanos", methodName, ((double) durationNanos) / ITERATION_LIMIT2));

        }
        System.out.println();
        System.out.println("Tests complete");

    }

    public static String method0(String s1, String s2) {
        return "";
    }

    public static String method1(String s1, String s2) {
        return s1.concat(s2);
    }

    public static String method2(String s1, String s2) {
        return s1 + s2;
    }

    public static String method3(String s1, String s2) {
        return new StringBuilder(s1).append(s2).toString();
    }

    public static String method4(String s1, String s2) {
        return new StringBuffer(s1).append(s2).toString();
    }

    public static String method5(String s1, String s2) {
        return String.format("%s%s", s1, s2);
    }

}

【讨论】:

  • 不错的评论。我一直在寻找 string.format 的速度,现在我发现它有点慢 :-) 我将使用 concat 代替。
【解决方案3】:

这些例程出现在基准测试中的原因是因为这就是编译器在幕后实现您的“+”的方式。

如果你真的需要串联的字符串,你应该让编译器用“+”来发挥它的魔力。如果您只需要一个用于地图查找的键,那么一个包含两个字符串并具有合适的 equalshashMap 实现的键类可能是一个好主意,因为它避免了复制步骤。

【讨论】:

  • 你有任何示例代码来防止瓶颈,因为你可能知道实现部分
  • @Deepak,我不认为这是一个瓶颈,但在 Eclipse 3.6 中创建这样一个类的最简单方法是创建一个新类,给它字段 ccy1 和 ccy2,要求 Eclipse 基于字段创建构造函数,并生成 hashCode() 和 equals() 方法。
【解决方案4】:

您应该使用在运行时生成的字符串(如 UUID.randomUUID().toString())而不是在编译时(如“我的字符串”)进行测试。我的结果是

plus:     118 ns
concat:    52 ns
builder1: 102 ns
builder2:  66 ns
buffer1:  119 ns
buffer2:   87 ns

使用此实现:

private static long COUNT = 10000000;

public static void main(String[] args) throws Exception {
    String s1 = UUID.randomUUID().toString();
    String s2 = UUID.randomUUID().toString();
    for(String methodName : new String[] {
            "none", "plus", "concat", "builder1", "builder2", "buffer1", "buffer2"
    }) {
        Method method = ConcatPerformanceTest.class.getMethod(methodName, String.class, String.class);
        long time = System.nanoTime();
        for(int i = 0; i < COUNT; i++) {
            method.invoke((Object) null, s1, s2);
        }
        System.out.println(methodName + ": " + (System.nanoTime() - time)/COUNT + " ns");
    }
}

public static String none(String s1, String s2) {
    return null;
}

public static String plus(String s1, String s2) {
    return s1 + s2;
}

public static String concat(String s1, String s2) {
    return s1.concat(s2);
}

public static String builder1(String s1, String s2) {
    return new StringBuilder(s1).append(s2).toString();
}

public static String builder2(String s1, String s2) {
    return new StringBuilder(s1.length() + s2.length()).append(s1).append(s2).toString();
}

public static String buffer1(String s1, String s2) {
    return new StringBuffer(s1).append(s2).toString();
}

public static String buffer2(String s1, String s2) {
    return new StringBuffer(s1.length() + s2.length()).append(s1).append(s2).toString();
}

【讨论】:

    【解决方案5】:

    对于标题中的问题:String.concat 通常是连接两个Strings 的最快方法(但请注意nulls)。不涉及 [oversized] 中间缓冲区或其他对象。奇怪的是+ 被编译成涉及StringBuilder 的相对低效的代码。

    但是,您的问题主体指向其他问题。为映射生成键的字符串连接是一种常见的“反习惯用法”。这是一种黑客攻击并且容易出错。您确定生成的密钥是唯一的吗?在您的代码针对一些未知的需求进行维护后,它会保持唯一吗?最好的方法是为键创建一个不可变的值类。使用List 和通用元组类是一种草率的技巧。

    【讨论】:

    • StringBuilder 变体真的比 concat 效率低很多吗?
    【解决方案6】:

    对我来说,下面的 concat3 方法是在我的 windows 和远程 linux 机器上进行基准测试后最快的方法:- 虽然我相信 concat1 的性能取决于 JVM 的实现和优化,并且在未来的版本中可能会表现得更好

        public class StringConcat {
    
        public static void main(String[] args) {
            int run = 100 * 100 * 1000;
            long startTime, total = 0;
    
            final String a = "a";
            final String b = "assdfsaf";
            final String c = "aasfasfsaf";
            final String d = "afafafdaa";
            final String e = "afdassadf";
    
            startTime = System.currentTimeMillis();
            concat1(run, a, b, c, d, e);
            total = System.currentTimeMillis() - startTime;
            System.out.println(total);
    
            startTime = System.currentTimeMillis();
            concat2(run, a, b, c, d, e);
            total = System.currentTimeMillis() - startTime;
            System.out.println(total);
    
            startTime = System.currentTimeMillis();
            concat3(run, a, b, c, d, e);
            total = System.currentTimeMillis() - startTime;
            System.out.println(total);
        }
    
        private static void concat3(int run, String a, String b, String c, String d, String e) {
            for (int i = 0; i < run; i++) {
                String str = new StringBuilder(a.length() + b.length() + c.length() + d.length() + e.length()).append(a)
                        .append(b).append(c).append(d).append(e).toString();
            }
        }
    
        private static void concat2(int run, String a, String b, String c, String d, String e) {
            for (int i = 0; i < run; i++) {
                String str = new StringBuilder(a).append(b).append(c).append(d).append(e).toString();
            }
        }
    
        private static void concat1(int run, String a, String b, String c, String d, String e) {
            for (int i = 0; i < run; i++) {
                String str = a + b + c + d + e;
            }
        }
    }
    

    【讨论】:

    • 您能否提供有关您测试这些的 JVM 的详细信息?
    • @Redandwhite java version "1.6.0_31" Java(TM) SE Runtime Environment (build 1.6.0_31-b05) Java HotSpot(TM) Client VM (build 20.6-b01,混合模式,共享)
    【解决方案7】:

    我建议尝试 Thorbjørn Ravn Andersens 的建议。

    如果您需要连接的字符串,具体取决于两个部分的长度,创建具有所需大小的 StringBuilder 实例以避免重新分配可能会稍好一些。默认 StringBuilder 构造函数在当前实现中保留 16 个字符 - 至少在我的机器上。因此,如果连接的 String 比初始缓冲区大小长,则 StringBuilder 必须重新分配。

    试试这个并告诉我们您的分析器对此有何评论:

    StringBuilder ccyPair = new StringBuilder(ccy1.length()+ccy2.length());
    ccyPair.append(ccy1); 
    ccyPair.append(ccy2); 
    

    【讨论】:

      【解决方案8】:

      也许您应该创建一个 Pair 类而不是串联?

      public class Pair<T1, T2> {
          private T1 first;
          private T2 second;
      
          public static <U1,U2> Pair<U1,U2> create(U1 first, U2 second) {
              return new Pair<U1,U2>(U1,U2);
          }
      
          public Pair( ) {}
      
          public Pair( T1 first, T2 second ) {
              this.first = first;
              this.second = second;
          }
      
          public T1 getFirst( ) {
              return first;
          }
      
          public void setFirst( T1 first ) {
              this.first = first;
          }
      
          public T2 getSecond( ) {
              return second;
          }
      
          public void setSecond( T2 second ) {
              this.second = second;
          }
      
          @Override
          public String toString( ) {
              return "Pair [first=" + first + ", second=" + second + "]";
          }
      
          @Override
          public int hashCode( ) {
              final int prime = 31;
              int result = 1;
              result = prime * result + ((first == null)?0:first.hashCode());
              result = prime * result + ((second == null)?0:second.hashCode());
              return result;
          }
      
          @Override
          public boolean equals( Object obj ) {
              if ( this == obj )
                  return true;
              if ( obj == null )
                  return false;
              if ( getClass() != obj.getClass() )
                  return false;
              Pair<?, ?> other = (Pair<?, ?>) obj;
              if ( first == null ) {
                  if ( other.first != null )
                      return false;
              }
              else if ( !first.equals(other.first) )
                  return false;
              if ( second == null ) {
                  if ( other.second != null )
                      return false;
              }
              else if ( !second.equals(other.second) )
                  return false;
              return true;
          }
      
      }
      

      并将其用作 HashMap 中的键

      HashMap&lt;Pair&lt;String,String&gt;,Whatever&gt;代替HashMap&lt;String,Whatever&gt;

      在您的紧密循环中,您将使用 map.get( str1 + str2 ),而不是 map.get( Pair.create(str1,str2) )

      【讨论】:

      • @KitsuneYMG,您能否发布一个完整的工作示例,以便在未来解决此类问题时方便。
      • @Deepak 查看编辑。如果您需要三元组、四元组等,很容易将其用作添加更多的基础。
      • @KitsuneYMG,你能把public static void main method发给你的配对课程,以便进一步参考
      • 我很想知道这是否真的使用起来更快,因为它不会缓存 Pair 的 hashCode,而缓存连接字符串的 hashCode。
      • @Duncan 您可以轻松缓存哈希码并在设置时将其丢弃*。这应该比连接两个需要两个 memcpy 的字符串更快(除非特定的 JVM 使用绳索)。
      【解决方案9】:

      根据Java specificationand since the very first version of Java),在“字符串连接运算符+”部分中说:

      为了提高重复字符串连接的性能,Java 编译器可以使用 StringBuffer 类或类似的技术 减少创建的中间字符串对象的数量 表达式的评估

      所以基本上,对变量使用+ operatorStringBuilder.append 基本相同。


      另外,我知道在您的问题中您提到只添加 2 个字符串,但请记住添加 3 个或更多字符串会导致不同的结果:

      我使用了一个稍加修改的@Duncan McGregor 示例。我有 5 种方法使用 concat 连接 2 到 6 个字符串,还有 5 种方法使用 StringBuilder 连接 2 到 6 个字符串:

      // Initialization
          private final String s1 = new String("1234567890");
          private final String s2 = new String("1234567890");
          private final String s3 = new String("1234567890");
          private final String s4 = new String("1234567890");
          private final String s5 = new String("1234567890");
          private final String s6 = new String("1234567890");
      
      // testing the concat
          public void testConcatenation2stringsConcat(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = s1.concat(s2);
              }
          }
          public void testConcatenation3stringsConcat(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = s1.concat(s2).concat(s3);
              }
          }
          public void testConcatenation4stringsConcat(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = s1.concat(s2).concat(s3).concat(s4);
              }
          }
          public void testConcatenation5stringsConcat(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5);
              }
          }
          public void testConcatenation6stringsConcat(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = s1.concat(s2).concat(s3).concat(s4).concat(s5).concat(s6);
              }
          }
      
      //testing the StringBuilder
          public void testConcatenation2stringsSB(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = new StringBuilder(s1).append(s2).toString();
              }
          }
          public void testConcatenation3stringsSB(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = new StringBuilder(s1).append(s2).append(s3).toString();
              }
          }
          public void testConcatenation4stringsSB(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).toString();
              }
          }
          public void testConcatenation5stringsSB(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).toString();
              }
          }
          public void testConcatenation6stringsSB(int count) {
              for (int i = 0; i < count; i++) {
                  String s100 = new StringBuilder(s1).append(s2).append(s3).append(s4).append(s5).append(s6).toString();
              }
          }
      

      我得到了这些结果(以秒为单位):

      testConcatenation2stringsConcat:0.018 |||||||||||||||| testConcatenation2stringsSB:0.2 testConcatenation3stringsConcat : 0.35 ||||||||||||||||||| testConcatenation3stringsSB:0.25 testConcatenation4stringsConcat : 0.5 |||||||||||||||||||||| testConcatenation4stringsSB:0.3 testConcatenation5stringsConcat : 0.67 ||||||||||||||||||| testConcatenation5stringsSB:0.38 testConcatenation5stringsConcat : 0.9 ||||||||||||||||||||| testConcatenation5stringsSB : 0.43

      • 你可以看到 concat 比 StringBuilder 快,只有当 仅连接 2 个字符串
      • 看到当添加越来越多的字符串时,StringBuilder 产生的时间越来越多 慢慢地使用 concat
      • 请注意,当使用字符串时差异会更显着 很长

      【讨论】:

        【解决方案10】:

        也许您可以通过单独计算两个字符串的哈希值,然后将它们组合起来,也许可以使用一个单独的整数哈希函数来解决这个问题?

        类似:

        int h1 = ccy1.hashCode(), h2 = ccy2.hashCode(), h = h1 ^ h2;
        

        这可能会更快,因为连接字符串只是为了计算连接的哈希值似乎很浪费。

        请注意,上面将两个哈希与二进制 XOR(^ 运算符)结合在一起,这通常有效,但您可能需要进一步调查。

        【讨论】:

        • 这对常规哈希图没有帮助。
        【解决方案11】:

        好的,那你的问题是什么? 无事可做:如果你必须连接字符串,那就去做吧。很好地分析了您的代码。现在你可以看到字符串连接运算符 + 自动使用 StringBuilder 的 append() 方法,所以使用

        StringBuilder ccyPair = new StringBuilder(ccy1)
        ccyPair.append(ccy2);
        

        不会给你带来严重的优势。

        优化代码的唯一严肃方法可能是更改设计以完全省略串联。但是只有当你真的需要它时才这样做,即连接会占用大量的 CPU 时间。

        【讨论】:

          【解决方案12】:

          @Duncan McGregor 的回答为一个特定示例(输入字符串的大小)和一个 JVM 版本提供了一些基准数字。在这种情况下,看起来String.concat() 在很大程度上是赢家。这个结果可能会或可能不会一概而论。

          旁白:这让我很吃惊!我原以为编译器编写者会选择在 String.concat 可能更快的情况下使用它。解释在this bug report ...的评估中,并植根于字符串连接运算符的定义。

          (如果+ 的字符串类型操作数是null,则JLS 声明使用字符串"null"。如果他们将s + s2 生成为s.concat(s2),这将不起作用而ss2 恰好是null;你会得到NPE。s == null 的情况意味着concat 的替代版本不能解决NPE 问题。)


          不过,@unwind 的回答让我想到了一种替代解决方案,可以避免字符串连接的需要。

          如果ccy1ccy2 的连接只是为了连接两个键,那么也许你可以通过定义一个使用两个键而不是一个键的特殊哈希表类来获得更好的性能。它会有如下操作:

              public Object get(String key1, String key2) ...
          
              public void put(String key1, String key2, Object value) ...
          

          效果类似于Map&lt;Pair&lt;String, String&gt;, Object&gt;(请参阅@KitsuneYMG 的答案),只是您不需要在每次执行getput 时创建Pair&lt;String, String&gt; 对象。缺点是:

          • 您必须从头开始实现一个新的哈希表类,并且
          • 新类不符合Map 接口。

          通常,我不建议这样做。但是,如果字符串连接和映射查找确实是一个关键瓶颈,那么自定义多键哈希表可能会给您带来显着的加速。

          【讨论】:

          • 您是否有任何证据表明“您无法改进字符串连接本身”?
          • @Stephen,看看 String.concat() impl。毫不奇怪,它是仅连接 2 个字符串的最佳方法。它完全根据需要分配 char[] 并通过 System.arrayCopy 复制(所以一个 char[] alloc,2 memcpy,一个 string alloc,永远无法击败),但最重要的是,它是创建带 w/ 的字符串的唯一方法o char 数组的额外副本(截至目前,StringBuffer 也没有复制)
          • 令人惊讶的是他们不能s.concat(s2) 用于s + s2。但这是有道理的;见上文。
          • @Stephen,是的,如果任何字符串为空,它就不起作用。但考虑一下:String.valueOf(s1).contact(String.valueOf(s2));实际上我发誓我已经看到 JBuilder 做到了(但至少是 8 年前,所以我不会发誓是真的)
          • @Stephen,自定义地图(2 值地图)是解决问题的最佳方案。我想我可以发一个。
          【解决方案13】:

          这里是线性探针映射的完整实现,带有双键、单值。它的性能也应该优于 java.util.HashMap。

          警告,它是从头开始写的,所以它可能包含错误。请随时编辑。

          解决方案必须击败任何包装器,随时连接一个。 get/put 上的无分配也使其快速通用映射。

          希望这能解决问题。 (代码带有一些不需要的简单测试)


          package bestsss.util;
          
          
          @SuppressWarnings("unchecked")
          public class DoubleKeyMap<K1, K2, V> {
              private static final int MAX_CAPACITY =  1<<29; 
          
              private static final Object TOMBSTONE = new String("TOMBSTONE");
          
              Object[] kvs; 
              int[] hashes;
              int count = 0;
          
              final int rehashOnProbes;   
          
              public DoubleKeyMap(){
                  this(8, 5);
              }
          
              public DoubleKeyMap(int capacity, int rehashOnProbes){
                  capacity = nextCapacity(Math.max(2, capacity-1));
                  if (rehashOnProbes>capacity){
                      throw new IllegalArgumentException("rehashOnProbes too high");
                  }
                  hashes = new int[capacity];
                  kvs = new Object[kvsIndex(capacity)];       
                  count = 0;
                  this.rehashOnProbes = rehashOnProbes;
              }
          
              private static int nextCapacity(int c) {
                  int n = Integer.highestOneBit(c)<<1;
                  if (n<0 || n>MAX_CAPACITY){
                      throw new Error("map too large");
                  }
                  return n;
              }
          
              //alternatively this method can become non-static, protected and overriden, the perfoamnce can drop a little
              //but if better spread of the lowest bit is possible, all good and proper
              private static<K1, K2> int hash(K1 key1, K2 key2){
                  //spread more, if need be
                  int h1 = key1.hashCode();
                  int h2 = key2.hashCode();
                  return h1+ (h2<<4) + h2; //h1+h2*17
              }
          
              private static int kvsIndex(int baseIdx){
                  int idx = baseIdx;  
                  idx+=idx<<1;//idx*3
                  return idx;
              }
          
              private int baseIdx(int hash){
                  return hash & (hashes.length-1);
              }
          
              public V get(K1 key1, K2 key2){
                  final int hash = hash(key1, key2);
          
          
                  final int[] hashes = this.hashes;
                  final Object[] kvs = this.kvs;
                  final int mask = hashes.length-1;
          
                  for(int base = baseIdx(hash);;base=(base+1)&mask){
                      int k = kvsIndex(base);
                      K1 k1 = (K1) kvs[k];
                      if (k1==null)
                          return null;//null met; no such value
          
                      Object value;
                      if (hashes[base]!=hash || TOMBSTONE==(value=kvs[k+2]))
                          continue;//next
          
                      K2 k2 = (K2) kvs[k+1];
                      if ( (key1==k1 || key1.equals(k1)) && (key2==k2 || key2.equals(k2)) ){
                          return (V) value;
                      }
                  }
              }
              public boolean contains(K1 key1, K2 key2){
                  return get(key1, key2)!=null;
              }
          
              public boolean containsValue(final V value){
                  final Object[] kvs = this.kvs;
                  if (value==null)
                      return false;
          
                  for(int i=0;i<kvs.length;i+=3){
                      Object v = kvs[2];
                      if (v==null || v==TOMBSTONE)
                          continue;
                      if (value==v || value.equals(v))
                          return true;
                  }
                  return false;
              }
          
              public V put(K1 key1, K2 key2, V value){
                  int hash = hash(key1, key2);
                  return doPut(key1, key2, value, hash);
              }
              public V remove(K1 key1, K2 key2){
                  int hash = hash(key1, key2);
                  return doPut(key1, key2, null, hash);   
              }
          
              //note, instead of remove a TOMBSTONE is used to mark the deletion
              //this may leak keys but deletion doesn't need to shift the array like in Knuth 6.4
              protected V doPut(final K1 key1, final K2 key2, Object value, final int hash){
                  //null value -> remove
                  int probes = 0;
                  final int[] hashes = this.hashes;
                  final Object[] kvs = this.kvs;
                  final int mask = hashes.length-1;
          
                  //conservative resize: when too many probes and the count is greater than the half of the capacity
                  for(int base = baseIdx(hash);probes<rehashOnProbes || count<(mask>>1);base=(base+1)&mask, probes++){
                      final int k = kvsIndex(base);
                      K1 k1 = (K1) kvs[k];
                      K2 k2;
                      //find a gap, or resize
                      Object  old  = kvs[k+2];
                      final boolean emptySlot = k1==null || (value!=null && old==TOMBSTONE); 
                      if (emptySlot || (
                              hashes[base] == hash &&
                              (k1==key1  || k1.equals(key1)) &&
                              ((k2=(K2) kvs[k+1])==key2 || k2.equals(key2))) 
                      ){
          
                          if (value==null){//remove()
                              if (emptySlot)
                                  return null;//not found, and no value ->nothing to do
          
                              value = TOMBSTONE;
                              count-=2;//offset the ++later
                          }
          
                          if (emptySlot){//new entry, update keys
                              hashes[base] = hash;                
                              kvs[k] = key1;
                              kvs[k+1] = key2;
                          }//else -> keys and hash are equal
          
          
                          if (old==TOMBSTONE) 
                              old=null;
          
                          kvs[k+2] = value;
                          count++;
          
                          return (V) old;
                      }
                  }
                  resize();
                  return doPut(key1, key2, value, hash);//hack w/ recursion, after the resize
              }
          
              //optimized version during resize, doesn't check equals which is the slowest part   
              protected void doPutForResize(K1 key1, K2 key2, V value, final int hash){
                  final int[] hashes = this.hashes;
                  final Object[] kvs = this.kvs;
                  final int mask = hashes.length-1;
          
                  //find the 1st gap and insert there
                  for(int base = baseIdx(hash);;base=(base+1)&mask){//it's ensured, no equal keys exist, so skip equals part
                      final int k = kvsIndex(base);
                      K1 k1 = (K1) kvs[k];
                      if (k1!=null) 
                          continue;
          
                      hashes[base] = hash;                
                      kvs[k] = key1;
                      kvs[k+1] = key2;
                      kvs[k+2] = value;
                      return;
                  }
              }
          
              //resizes the map by doubling the capacity, 
              //the method uses altervative varian of put that doesn't check equality, or probes; just inserts at a gap
              protected void resize(){        
                  final int[] hashes = this.hashes;
                  final Object[] kvs = this.kvs;
                  final int capacity = nextCapacity(hashes.length);
          
                  this.hashes = new int[capacity];
                  this.kvs = new Object[kvsIndex(capacity)];
          
                  for (int i=0;i<hashes.length; i++){
                      int k = kvsIndex(i);
                      K1 key1 = (K1) kvs[k];
                      Object value = kvs[k+2];
                      if (key1!=null && TOMBSTONE!=value){
                          K2 key2 = (K2) kvs[k+1];
                          doPutForResize(key1, key2, (V) value, hashes[i]);
                      }
                  }   
              }
          
              public static void main(String[] args) {
                  DoubleKeyMap<String, String, Integer> map = new DoubleKeyMap<String, String, Integer>(4,2);
                  map.put("eur/usd", "usd/jpy", 1);
                  map.put("eur/usd", "usd/jpy", 2);
          
                  map.put("eur/jpy", "usd/jpy", 3);
          
                  System.out.println(map.get("eur/jpy", "usd/jpy"));
                  System.out.println(map.get("eur/usd", "usd/jpy"));
                  System.out.println("======");
          
                  map.remove("eur/usd", "usd/jpy");
                  System.out.println(map.get("eur/jpy", "usd/jpy"));
                  System.out.println(map.get("eur/usd", "usd/jpy"));
                  System.out.println("======");
                  testResize();
              }
              static void testResize(){
                  DoubleKeyMap<String, Integer, Integer> map = new DoubleKeyMap<String, Integer, Integer>(18, 17);
                  long s = 0;
                  String pref="xxx";
          
                  for (int i=0;i<14000;i++){
                      map.put(pref+i, i, i);
          
                      if ((i&1)==1)
                          map.remove(pref+i, i);
                      else
                          s+=i;
                  }
                  System.out.println("sum: "+s);
                  long sum = 0;
          
                  for (int i=0;i<14000;i++){
                      Integer n = map.get(pref+i, i);
                      if (n!=null && n!=i){
                          throw new AssertionError(); 
                      }
                      if (n!=null){
                          System.out.println(n);
                          sum+=n;
                      }
                  }
                  System.out.println("1st sum: "+s);
                  System.out.println("2nd sum: "+sum);
          
          
              }
          }
          

          【讨论】:

            【解决方案14】:
            StringBuffer ccyPair =  new StringBuffer();      
            ccyPair.append("ccy1").append("ccy2"); 
            

            您是否尝试过使用字符串缓冲区,然后使用分析器检查瓶颈在哪里。试一试,看看会发生什么。

            【讨论】:

            • StringBuffer 在这里肯定不会表现得更好,因为 StringBuilder 不是线程安全的对应物,避免了不必要的同步开销。
            • 确实 - StringBuilder 明显更快
            • 实际上 - 你最终总是带有“ccy1ccy2”。
            【解决方案15】:

            请记住,如果您要连接数百万个字符串,那么 string.concat 很可能会生成数百万个新的字符串对象引用。这将增加 CPU 使用率。

            【讨论】:

              【解决方案16】:

              有趣的是, StringJoiner 这里没有提到......

              通常必须在字符串之间插入一个分隔符,例如。 ", "
              使用StringJoiner 比使用StringBuilder 更容易阅读代码,而且速度也同样快。

              StringJoiner joiner = new StringJoiner( ", " );
              joiner.add( ccy1 ).add( ccy2 );
              

              【讨论】:

                【解决方案17】:

                我决定尝试对其进行基准测试,这就是我的结果。我想使用默认的“+”连接是最简单和最快的(或几乎是最快的)方法。

                JMH version: 1.19
                VM version: JDK 1.8.0_211, VM 25.211-b12
                VM options: -Xms2G -Xmx2G
                Warmup: 10 iterations, 1 s each
                Measurement: 30 iterations, 1 s each
                Timeout: 10 min per iteration
                Threads: 1 thread, will synchronize iterations
                Benchmark mode: Average time, time/op
                Parameters: (N = 1000000)
                
                Benchmark          (N)  Mode  Cnt  Score     Error  Units
                concat         1000000  avgt   30  24.839  ± 0.211  ms/op
                plus           1000000  avgt   30  15.072  ± 0.155  ms/op
                stringBuffer   1000000  avgt   30  14.835  ± 0.118  ms/op
                stringBuilder  1000000  avgt   30  14.775  ± 0.205  ms/op
                

                这是测试代码:

                @BenchmarkMode(Mode.AverageTime)
                @OutputTimeUnit(TimeUnit.MILLISECONDS)
                @State(Scope.Benchmark)
                @Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
                @Warmup(iterations = 10)
                @Measurement(iterations = 30)
                public class BenchmarkString {
                
                    @Param({"1000000"})
                    private int N;
                    private final String s1 = new String("1234567890124567890");
                    private final String s2 = new String("1234567890124567890");
                
                    public static void main(String[] args) throws RunnerException {
                
                        Options opt = new OptionsBuilder()
                                .include(BenchmarkString.class.getSimpleName())
                                .forks(1)
                                .build();
                
                        new Runner(opt).run();
                    }
                
                    @Benchmark
                    public void plus() {
                        for (int i = 0; i < N; i++) {
                            String s = s1 + s2;
                        }
                    }
                
                    @Benchmark
                    public void stringBuilder() {
                        for (int i = 0; i < N; i++) {
                            String s = new StringBuilder(s1).append(s2).toString();
                        }
                    }
                
                    @Benchmark
                    public void stringBuffer() {
                        for (int i = 0; i < N; i++) {
                            String s = new StringBuffer(s1).append(s2).toString();
                        }
                    }
                
                    @Benchmark
                    public void concat() {
                        for (int i = 0; i < N; i++) {
                            String s = s1.concat(s2);
                        }
                    }
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2014-03-19
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-03-15
                  • 1970-01-01
                  • 1970-01-01
                  • 2014-08-27
                  相关资源
                  最近更新 更多