Pourquoi est la chaîne.strip() 5 fois plus rapide que String.trim () pour une chaîne vide en Java 11


J'ai rencontré un scénario intéressant. Pour une raison quelconque, strip() contre une chaîne vide (contient uniquement des espaces blancs) nettement plus rapide que trim() dans Java 11.

Référence

public class Test {

    public static final String TEST_STRING = "   "; // 3 whitespaces

    @Benchmark
    @Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
    @Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
    @BenchmarkMode(Mode.Throughput)
    public void testTrim() {
        TEST_STRING.trim();
    }

    @Benchmark
    @Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
    @Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
    @BenchmarkMode(Mode.Throughput)
    public void testStrip() {
        TEST_STRING.strip();
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

Les Résultats

# Run complete. Total time: 00:04:16

Benchmark        Mode  Cnt           Score          Error  Units
Test.testStrip  thrpt  200  2067457963.295 ± 12353310.918  ops/s
Test.testTrim   thrpt  200   402307182.894 ±  4559641.554  ops/s

Apparemment strip() surpasse trim() ~5 fois.

Bien que pour une chaîne non vide, les résultats soient presque identiques:

public class Test {

    public static final String TEST_STRING = " Test String ";

    @Benchmark
    @Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
    @Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
    @BenchmarkMode(Mode.Throughput)
    public void testTrim() {
        TEST_STRING.trim();
    }

    @Benchmark
    @Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
    @Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
    @BenchmarkMode(Mode.Throughput)
    public void testStrip() {
        TEST_STRING.strip();
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}


# Run complete. Total time: 00:04:16

Benchmark        Mode  Cnt          Score         Error  Units
Test.testStrip  thrpt  200  126939018.461 ± 1462665.695  ops/s
Test.testTrim   thrpt  200  141868439.680 ± 1243136.707  ops/s

Comment se fait-il? Est-ce un bug ou est-ce que je le fais de mal?


Environnement de Test

    Il s'agit d'un processeur Intel Xeon E3 - 1585L v5 à 3,00 GHz.]}
  • SYSTÈME d'exploitation - Windows 7 SP 1 64 bits
  • JVM Oracle JDK 11.0.1
  • [33]}Benchamrk-JMH v 1.19

Mise à jour

Ajout de tests de performance pour différentes chaînes (vide, vide, etc.).

Référence

@Warmup(iterations = 5, time = 1, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = SECONDS)
@Fork(value = 3)
@BenchmarkMode(Mode.Throughput)
public class Test {

    private static final String BLANK = "";              // Blank
    private static final String EMPTY = "   ";           // 3 spaces
    private static final String ASCII = "   abc    ";    // ASCII characters only
    private static final String UNICODE = "   абв    ";  // Russian Characters

    private static final String BIG = EMPTY.concat("Test".repeat(100)).concat(EMPTY);

    @Benchmark
    public void blankTrim() {
        BLANK.trim();
    }

    @Benchmark
    public void blankStrip() {
        BLANK.strip();
    }

    @Benchmark
    public void emptyTrim() {
        EMPTY.trim();
    }

    @Benchmark
    public void emptyStrip() {
        EMPTY.strip();
    }

    @Benchmark
    public void asciiTrim() {
        ASCII.trim();
    }

    @Benchmark
    public void asciiStrip() {
        ASCII.strip();
    }

    @Benchmark
    public void unicodeTrim() {
        UNICODE.trim();
    }

    @Benchmark
    public void unicodeStrip() {
        UNICODE.strip();
    }

    @Benchmark
    public void bigTrim() {
        BIG.trim();
    }

    @Benchmark
    public void bigStrip() {
        BIG.strip();
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

Les Résultats

# Run complete. Total time: 00:05:23

Benchmark           Mode  Cnt           Score          Error  Units
Test.asciiStrip    thrpt   15   356846913.133 ±  4096617.178  ops/s
Test.asciiTrim     thrpt   15   371319467.629 ±  4396583.099  ops/s
Test.bigStrip      thrpt   15    29058105.304 ±  1909323.104  ops/s
Test.bigTrim       thrpt   15    28529199.298 ±  1794655.012  ops/s
Test.blankStrip    thrpt   15  1556405453.206 ± 67230630.036  ops/s
Test.blankTrim     thrpt   15  1587932109.069 ± 19457780.528  ops/s
Test.emptyStrip    thrpt   15  2126290275.733 ± 23402906.719  ops/s
Test.emptyTrim     thrpt   15   406354680.805 ± 14359067.902  ops/s
Test.unicodeStrip  thrpt   15    37320438.099 ±   399421.799  ops/s
Test.unicodeTrim   thrpt   15    88226653.577 ±  1628179.578  ops/s

L'environnement de test est le même.

Un seul découverte intéressante. Chaîne qui contient des caractères Unicode obtenant trim() 'ed plus rapidement que strip()' ed

Author: Mikhail Kholodkov, 2018-12-05

3 answers

Sur OpenJDK 11.0.1 String.strip() (en fait StringLatin1.strip()) optimise le décapage à un String vide en renvoyant une constante String internée:

public static String strip(byte[] value) {
    int left = indexOfNonWhitespace(value);
    if (left == value.length) {
        return "";
    }

Alors que String.trim() (en fait StringLatin1.trim()) alloue toujours un nouvel objet String. Dans votre exemple st = 3 et len = 3, donc

return ((st > 0) || (len < value.length)) ?
        newString(value, st, len - st) : null;

Va sous le capot copier le tableau et crée un nouvel objet String

return new String(Arrays.copyOfRange(val, index, index + len),
                      LATIN1);

En faisant l'hypothèse ci-dessus, nous pouvons mettre à jour le benchmark pour le comparer à un String non vide qui ne devrait pas être affecté par {[5 mentionné]} optimisation:

@Warmup(iterations = 10, time = 200, timeUnit = MILLISECONDS)
@Measurement(iterations = 20, time = 500, timeUnit = MILLISECONDS)
@BenchmarkMode(Mode.Throughput)
public class MyBenchmark {

  public static final String EMPTY_STRING = "   "; // 3 whitespaces
  public static final String NOT_EMPTY_STRING = "  a "; // 3 whitespaces with a in the middle

  @Benchmark
  public void testEmptyTrim() {
    EMPTY_STRING.trim();
  }

  @Benchmark
  public void testEmptyStrip() {
    EMPTY_STRING.strip();
  }

  @Benchmark
  public void testNotEmptyTrim() {
    NOT_EMPTY_STRING.trim();
  }

  @Benchmark
  public void testNotEmptyStrip() {
    NOT_EMPTY_STRING.strip();
  }

}

En cours d'exécution, il ne montre aucune différence significative entre strip() et trim() pour un String non vide. Curieusement, le rognage vers un String vide est toujours le plus lent:

Benchmark                       Mode  Cnt           Score           Error  Units
MyBenchmark.testEmptyStrip     thrpt  100  1887848947.416 ± 257906287.634  ops/s
MyBenchmark.testEmptyTrim      thrpt  100   206638996.217 ±  57952310.906  ops/s
MyBenchmark.testNotEmptyStrip  thrpt  100   399701777.916 ±   2429785.818  ops/s
MyBenchmark.testNotEmptyTrim   thrpt  100   385144724.856 ±   3928016.232  ops/s
 16
Author: Karol Dowbecki, 2018-12-06 11:13:06

Après avoir examiné le code source d'OpenJDK, en supposant que l'implémentation de la version Oracle est similaire, j'imagine que la différence s'explique par les faits que

  • strip va essayer de trouver le premier caractère non-espace, et si aucun n'est trouvé, renvoie simplement ""
  • trim retournera toujours un new String(...the substring...)

On pourrait soutenir que strip est juste un tout petit peu plus optimisé que trim, du moins dans OpenJDK, car il esquive la création de nouveaux objet sauf si nécessaire.

(Note: Je n'ai pas pris la peine de vérifier les versions unicode de ces méthodes.)

 7
Author: Sami Hult, 2018-12-05 20:42:09

Oui. En Java 11 ou plus tôt semble que .trim() crée toujours une nouvelle chaîne() mais strip () renvoie une chaîne de cache. Vous pouvez tester ce code simple et le prouver vous-même.

public class JavaClass{
  public static void main(String[] args){
      //prints false
      System.out.println("     ".trim()=="");//CREATING A NEW STRING()
  }
}

Vs

public class JavaClass{
  public static void main(String[] args){
      //prints true
      System.out.println("     ".strip()=="");//RETURNING CACHE ""
  }
}
 1
Author: chiperortiz, 2019-03-04 15:33:22