Подтвердить что ты не робот

Commons Lang StringUtils.replace производительность vs String.replace

Когда я сравнивал производительность Apache StringUtils.replace() vs String.replace(), я был удивлен, узнав, что первый примерно в 4 раза быстрее. Я использовал инфраструктуру Google Caliper для измерения производительности. Здесь мой тест

public class Performance extends SimpleBenchmark {
    String s = "111222111222";

    public int timeM1(int n) {
        int res = 0;
        for (int x = 0; x < n; x++) {
            res += s.replace("111", "333").length();
        }
        return res;
    }

    public int timeM2(int n) {
        int res = 0;
        for (int x = 0; x < n; x++) {
            res += StringUtils.replace(s, "111", "333", -1).length();
        }
        return res;
    }

    public static void main(String... args) {
        Runner.main(Performance.class, args);
    }
}

Выход

 0% Scenario{vm=java, trial=0, benchmark=M1} 9820,93 ns; ?=1053,91 ns @ 10 trials
50% Scenario{vm=java, trial=0, benchmark=M2} 2594,67 ns; ?=58,12 ns @ 10 trials

benchmark   us linear runtime
       M1 9,82 ==============================
       M2 2,59 =======

Почему? Оба метода, похоже, выполняют ту же работу, StringUtils.replace() еще более гибкий.

4b9b3361

Ответ 1

Из исходного кода java.lang.String 1:

public String replace(CharSequence target, CharSequence replacement) {
   return Pattern
            .compile(target.toString(), Pattern.LITERAL)
            .matcher(this )
            .replaceAll(
                    Matcher.quoteReplacement(replacement.toString()));
}

String.replace(CharSequence target, CharSequence replacement) реализуется с помощью java.util.regex.Pattern, поэтому неудивительно, что он медленнее, чем StringUtils.replace(String text, String searchString, String replacement) 2 который реализуется с помощью indexOf и StringBuffer.

public static String replace(String text, String searchString, String replacement) {
    return replace(text, searchString, replacement, -1);
}

public static String replace(String text, String searchString, String replacement, int max) {
    if (isEmpty(text) || isEmpty(searchString) || replacement == null || max == 0) {
        return text;
    }
    int start = 0;
    int end = text.indexOf(searchString, start);
    if (end == -1) {
        return text;
    }
    int replLength = searchString.length();
    int increase = replacement.length() - replLength;
    increase = (increase < 0 ? 0 : increase);
    increase *= (max < 0 ? 16 : (max > 64 ? 64 : max));
    StringBuffer buf = new StringBuffer(text.length() + increase);
    while (end != -1) {
        buf.append(text.substring(start, end)).append(replacement);
        start = end + replLength;
        if (--max == 0) {
            break;
        }
        end = text.indexOf(searchString, start);
    }
    buf.append(text.substring(start));
    return buf.toString();
}

Сноска

1 Версия, с которой я ссылаюсь и скопировал исходный код, - это JDK 7

2 Версия, с которой я ссылаюсь и скопировал исходный код, - common-lang-2.5

Ответ 2

В современной Java это уже не так. String.replace был улучшен в Java-9 при переходе от регулярного выражения к StringBuilder, и еще больше улучшен в Java-13 при переходе к прямому выделению целевого массива byte[] с вычислением его Точный размер заранее. Благодаря используемым внутренним функциям JDK, таким как возможность выделения неинициализированного массива, возможность доступа к String-кодеру и возможность использовать закрытый конструктор String, который позволяет избежать копирования, маловероятно, что текущая реализация может быть побита сторонней реализацией.

Вот мои результаты тестирования вашего теста с использованием JDK 8, JDK 9 и JDK 13 (суппорт: 0.5-rc1; commons-lang3: 3.9)

Java 8 (в 4 раза медленнее):

 0% Scenario{vm=java, trial=0, benchmark=M1} 291.42 ns; σ=6.56 ns @ 10 trials
50% Scenario{vm=java, trial=0, benchmark=M2} 70.34 ns; σ=0.15 ns @ 3 trials

benchmark    ns linear runtime
       M1 291.4 ==============================
       M2  70.3 =======

Java 9 (почти одинаковая производительность):

 0% Scenario{vm=java, trial=0, benchmark=M2} 99,15 ns; σ=8,34 ns @ 10 trials
50% Scenario{vm=java, trial=0, benchmark=M1} 103,43 ns; σ=9,01 ns @ 10 trials

benchmark    ns linear runtime
       M2  99,1 ============================
       M1 103,4 ==============================

Java 13 (стандартный метод на 38% быстрее):

 0% Scenario{vm=java, trial=0, benchmark=M2} 91,64 ns; σ=5,12 ns @ 10 trials
50% Scenario{vm=java, trial=0, benchmark=M1} 57,38 ns; σ=2,51 ns @ 10 trials

benchmark   ns linear runtime
       M2 91,6 ==============================
       M1 57,4 ==================

Ответ 3

Попробуйте это, вы заметите, что он чрезвычайно эффективен, чем Apache:

public static String replace (String source, String os, String ns) {
    if (source == null) {
        return null;
    }
    int i = 0;
    if ((i = source.indexOf(os, i)) >= 0) {
        char[] sourceArray = source.toCharArray();
        char[] nsArray = ns.toCharArray();
        int oLength = os.length();
        StringBuilder buf = new StringBuilder (sourceArray.length);
        buf.append (sourceArray, 0, i).append(nsArray);
        i += oLength;
        int j = i;
        // Replace all remaining instances of oldString with newString.
        while ((i = source.indexOf(os, i)) > 0) {
            buf.append (sourceArray, j, i - j).append(nsArray);
            i += oLength;
            j = i;
        }
        buf.append (sourceArray, j, sourceArray.length - j);
        source = buf.toString();
        buf.setLength (0);
    }
    return source;
}

Ответ 4

на моем тесте с JMH:https://github.com/qxo/Benchmark4StringReplace Окружение лоукили образом:

java -jar target/benchmarks.jar StringReplaceBenchmark -wi 3 -i 6 -f 1 -tu ms Benchmark Mode Cnt Score Error Units StringReplaceBenchmark.test4String thrpt 6 1255.017 ± 230.012 ops/ms StringReplaceBenchmark.test4StringUtils thrpt 6 4068.229 ± 67.708 ops/ms StringReplaceBenchmark.test4fast thrpt 6 4821.035 ± 97.790 ops/ms StringReplaceBenchmark.test4lang3StringUtils thrpt 6 3186.007 ± 102.786 ops/ms

Ответ 5

Почему? Оба метода, похоже, выполняют ту же работу.

Вам нужно будет посмотреть исходный код и провести серьезное расследование с профилировщиком, чтобы получить хороший (технический) ответ на этот вопрос.

Однако одно из возможных объяснений состоит в том, что StringUtils.replace и String.replace были настроены для разных случаев использования. Вы смотрите только на один случай... с довольно маленькой строкой и заменяющую строку, размер которой совпадает с заменой подстроки.

Еще одно возможное объяснение заключается в том, что разработчики Apache просто потратили больше времени на настройку. (И давайте не будем обвинять разработчиков Java в этом. Они долгое время работали под серьезными кадровыми ограничениями. В большой схеме вещей есть много задач, более важных, чем настройка производительности String.replace.)


Фактически, глядя на исходный код, похоже, что версия Java 7 использует под replace на основе выражения под капотом. Напротив, версия Apache имеет значительную длину, чтобы избежать копирования. Исходя из этих доказательств, я ожидал бы, что разница в производительности между двумя версиями будет относительно меньше для больших целевых строк. И я подозреваю, что версия Java 7 может быть даже лучше в некоторых случаях.

(Любое нетехническое объяснение также правдоподобно, основываясь на доказательствах в коде.)