为什么 Java 不优化 String.split("regex") 对字符串文字的调用?

Why doesn't Java optimize String.split("regex") calls for String literals?

在Java中,Stringclass有个方法:public String[] split(String regex)

此方法接受正则表达式并拆分此正则表达式调用的字符串,返回 Strings 的数组,即拆分结果。

基本上,它对长度为 1 的正则表达式进行了一些优化,对于更长的正则表达式,它只调用以下代码:

Pattern.compile(regex).split(this, 0)

所以基本上,每次调用 split(String regex) 方法时它都会编译正则表达式。但实际上,模式编译是一个相对昂贵的操作,但是一个模式可以用来在不同时间处理(在这种情况下拆分)多个字符串(而且它是不可变的,因此是线程安全的)。

我的问题是:为什么 Java 编译器 或 JVM 不以某种方式优化它,无论是在编译还是运行时,通过(可能是懒惰的)预编译正则表达式split("regex") 个电话?我想不出将字符串文字一遍又一遍地编译为模式有什么好处。我正在考虑类似于字符串实习的东西,或者只是在某个地方保留一个数组用于预编译 Patterns?

我写了下面的代码来观察性能的差异(正则表达式和字符串取自this answer)(示例字符串emailsSOMETHING之间的电子邮件列表他们每个人):

public static final String REGEX = "(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*(?:(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\"(?:[^\\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*\"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[\"()<>@,;:\\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*))*)?;\s*)";

public static void main(String[] args) {
    String emails = "\"Fred Bloggs\"@example.comSOMETHINGuser@.invalid.comSOMETHINGChuck Norris <gmail@chucknorris.com>SOMETHINGwebmaster@müller.deSOMETHINGmatteo@78.47.122.114";

    long t1 = System.currentTimeMillis();
    test1(emails);
    System.out.println("Test 1 took " + (System.currentTimeMillis() - t1) + " milliseconds...");

    long t2 = System.currentTimeMillis();
    test2(emails);
    System.out.println("Test 2 took " + (System.currentTimeMillis() - t2) + " milliseconds...");
}

public static void test1(String input) {
    for (int i = 0; i < 100000; i++) {
        input.split(REGEX);
    }
}

public static void test2(String input) {
    Pattern pattern = Pattern.compile(REGEX);
    for (int i = 0; i < 100000; i++) {
        pattern.split(input);
    }
}

输出为:

Test 1 took 34831 milliseconds...
Test 2 took 11799 milliseconds...

毕竟,这是一个很大的不同,不是吗?

Java 编译器只是不进行优化。它执行常量折叠,并且会删除条件在常量折叠下减少为 false 的分支,但它不会做任何更实质性的事情。对于来自 C/C++ 的人来说,这常常令人惊讶:Java 是一种源代码级微优化并不总是完全无用的语言(尽管 JIT 确实使它们成为 通常 所以)

在某些情况下,它会做出可笑的可怕事情。例如,这个代码片段:

Integer X = 3;
X++;

在字节码中结束为:

0: iconst_3            // Push 3
1: invokestatic #2     // Integer.valueOf(3)
4: astore_1            // Store the Integer in x.
5: aload_1             // Push x....
6: astore_2            // ... Store x in y.
7: aload_1             // Push x again...
8: invokevirtual #3    // x.intValue()
11: iconst_1           // Push 1
12: iadd               // x.intValue() + 1
13: invokestatic #2    // Integer . valueOf ( x . intValue () + 1)
16: dup                // Duplicate the new Integer .
17: astore_1           // Store it in x.
18: astore_3           // ... And also in z
19: aload_2            // Put y on the stack ...
20: pop                // .. discard it .

...这显然是荒谬的。

类似地,每个字符串追加操作编译成单独的 StringBuilder 使用。

声称即时编译器可以进行所有优化。这并不总是能很好地工作:二进制大小没有用,一些平台的 JIT 很差,而且 JIT 无论如何也不能非常积极地优化,因为它的工作时间非常短。

更糟糕的是,过度的字节码膨胀会导致 JIT 更不愿意处理您的函数,并损害最终输出的质量。哎呀

确实存在针对 Java 字节码的优化器,例如 Proguard,但我认为没有任何一个可以处理您的特定情况。 Java 的有用的源代码级编译时间优化器可能还有空间,但目前存在 none。好吧,除非你算上我在 uni 写的那个(如果你 super 热衷的话,它就在我的 GitHub 上。你可以将它用作实现优化的平台,比如那个你想要的。欢迎 PRs :P).

本质上,如果您想要提前优化,Java 不是要使用的语言。实际上,在 JIT 良好的平台上,一切都很好,尽管看起来 AoT 优化可以使它们更好一些。在旧的 Android 平台上,JIT 很糟糕或 none 存在,Java 中的这个缺陷可能非常有害(这基本上就是 Proguard 存在的原因)

改进您对 API 的使用不是编译器的责任。

编译器耦合到大量标准 Java classes 的最小子集。 Java Language Specification, section 1.4: Relationship to Predefined Classes and Interfaces:

中列出了其中一些

As noted above, this specification often refers to classes of the Java SE platform API. In particular, some classes have a special relationship with the Java programming language. Examples include classes such as Object, Class, ClassLoader, String, Thread, and the classes and interfaces in package java.lang.reflect, among others. This specification constrains the behavior of such classes and interfaces, but does not provide a complete specification for them. The reader is referred to the Java SE platform API documentation.

满足您对 String.split() 的特殊情况的请求将:

  • 需要将编译器耦合到模式 class。
  • 需要将编译器耦合到 String.split() 的特定实现,这可能与 运行 时使用的库不同。
  • 提出关于可能许多其他 API 方法的类似问题。

用一般方法满足您的要求会更困难。 Java 不是函数式语言。方法调用不一定是引用透明的——使用相同的参数多次调用相同的方法可能 return 不同的结果,并且有副作用。在您的情况下,Pattern.compile() return 每次调用都会有不同的结果。在函数式语言中自动记忆更容易。

如您所见,您有一种有效使用 API 的方法 - 编译一次,拆分多次,而不是使用便捷方法 String.split()。为这种特殊情况(和其他情况)增加编译器的复杂性和耦合性会带来超过收益的成本。