[Программирование, Java] Ещё больше строковых оптимизаций
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В продолжение своей предыдущей статьи о строках (напоминаю, это была текстовая версия доклада на конференции JPoint-2020) решил дописать ещё одну заметку со строковыми оптимизациями, обнаруженными уже после вёрстки презентации (первые две есть на видео в самом конце, показывал их прямо из "Идеи").Снова StringBuilder.append(char)На сцене снова "Спринг", а именно o.s.u.StringUtils.deleteAny(String, String):
// org.springframework.util.StringUtils
public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) {
return inString;
}
StringBuilder sb = new StringBuilder(inString.length());
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt(i);
if (charsToDelete.indexOf(c) == -1) {
sb.append(c);
}
}
return sb.toString();
}
В разделе "Склейка: если всё-таки нужно" рассматривая StringBuilder.append(char) я отметил невозможность оптимизации компилятором проверок внутри этого метода даже в счётном цикле с заранее известным количеством проходов. Выходом станет использование массива.Этот же подход хорошо ложится на случаи, в которых длина преобразованной строки может быть меньше или равна исходной. Действительно, какой бы аргумент не передавался в deleteAny, длина возвращаемой строки никогда не превысит длину исходной. Следовательно, можно незначительно усложнив код избавиться от SB:
public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) {
return inString;
}
int lastCharIndex = 0;
char[] result = new char[inString.length()];
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt(i);
if (charsToDelete.indexOf(c) == -1) {
result[lastCharIndex++] = c;
}
}
return new String(result, 0, lastCharIndex);
}
Переменная lastCharIndex устраняет возможные "пустоты" в массиве при пропуске одного и более знаков.Используя бенчмарк легко обнаруживаем существенный прирост производительности даже для небольших объёмов данных:
Benchmark Mode Score Error Units
original avgt 90.203 ± 4.317 ns/op
patched avgt 25.391 ± 1.118 ns/op
original:·gc.alloc.rate.norm avgt 104.000 ± 0.001 B/op
patched:·gc.alloc.rate.norm avgt 104.000 ± 0.001 B/op
Но и это ещё не всё.Уже после моего коммита наблюдательный пользователь Johnny Lim сообразил, что если после завершения перебора значение переменной lastCharIndex равно длине исходной строки, то ни одной замены не сделано, а значит новую строку создавать не нужно. Итого:
public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) {
return inString;
}
int lastCharIndex = 0;
char[] result = new char[inString.length()];
for (int i = 0; i < inString.length(); i++) {
char c = inString.charAt(i);
if (charsToDelete.indexOf(c) == -1) {
result[lastCharIndex++] = c;
}
}
if (lastCharIndex == inString.length()) { // С - сообразительность
return inString;
}
return new String(result, 0, lastCharIndex);
}
Похожий подход использован тут.Коварный StringBuilder.append(Object)Следующий пример намного менее очевиден:
// org.springframework.http.ContentDisposition
private static String escapeQuotationsInFilename(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;
}
boolean escaped = false;
StringBuilder sb = new StringBuilder();
for (char c : filename.toCharArray()) {
sb.append((c == '"' && !escaped) ? "\\"" : c); // <----
escaped = (!escaped && c == '\\');
}
// Remove backslash at the end..
if (escaped) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
Больное место находится в строке 10, а заключение звучит так: поскольку тернарный оператор возвращает либо String, либо char, то аргументом метода StringBuilder.append() фактически является Object. И всё бы ничего, да преобразование знака в объект происходит с помощью String.valueOf() и мы на ровном месте получаем множество мелкого мусора по схеме:
Character.valueOf(c)
-> StringBuilder.append(Object)
-> String.valueOf()
-> Character.toString()
Отдельно доставляет реализация Character.toString:Осторожно, не ушибите лицо ладонью
public final class Character {
private final char value;
public String toString() {
char buf[] = {value};
return String.valueOf(buf);
}
}
Зачем было оборачивать знак в массив? Тайна сия велика... Как бы то ни было, это уже исправлено.Таким образом, достаточно избавиться от тернарного оператора:
for (int i = 0; i < filename.length() ; i++) {
char c = filename.charAt(i);
if (!escaped && c == '"') {
sb.append("\\""); // append(String)
}
else {
sb.append(c); // append(char)
}
escaped = (!escaped && c == '\\');
}
И вновь очень простое изменение приносит впечатляющий прирост (бенчмарк по ссылке):
JDK 8
Benchmark latin len Score Units
appendCovariant true 10 180.2 ± 10.3 ns/op
appendExact true 10 68.5 ± 1.4 ns/op
appendCovariant false 10 177.7 ± 4.4 ns/op
appendExact false 10 67.7 ± 1.3 ns/op
appendCovariant:·gc.alloc.rate.norm true 10 688.0 ± 0.0 B/op
appendExact:·gc.alloc.rate.norm true 10 112.0 ± 0.0 B/op
appendCovariant:·gc.alloc.rate.norm false 10 816.0 ± 0.0 B/op
appendExact:·gc.alloc.rate.norm false 10 112.0 ± 0.0 B/op
JDK 14
Benchmark latin len Score Units
appendCovariant true 10 228.8 ± 18.6 ns/op
appendExact true 10 57.9 ± 2.6 ns/op
appendCovariant false 10 292.8 ± 12.4 ns/op
appendExact false 10 90.2 ± 2.2 ns/op
appendCovariant:·gc.alloc.rate.norm true 10 688.0 ± 0.0 B/op
appendExact:·gc.alloc.rate.norm true 10 112.0 ± 0.0 B/op
appendCovariant:·gc.alloc.rate.norm false 10 1096.0 ± 0.0 B/op
appendExact:·gc.alloc.rate.norm false 10 200.0 ± 0.0 B/op
Обратите внимание, что исполнение не смогло выбросить мусорные объекты, хотя их область видимости крайне ограничена. Закономерно возникает вопрос: могёт ли Грааль?ОтветНе знаю, не проверял :)
Оставляю этот вопрос энтузиастам в качестве домашнего задания :)Коварный String.substringДавно известно, что метод String.substring всегда возвращает новую строку, и тем не менее в задачах на "выкусывание" он всё ещё пользуется незаслуженной популярностью:
// org.springframework.web.util.UrlPathHelper
/**
* Sanitize the given path replacing "//" with "/"
*/
private String getSanitizedPath(String path) {
String sanitized = path;
while (true) {
int idx = sanitized.indexOf("//");
if (idx < 0) {
break;
}
else {
sanitized = sanitized.substring(0, idx) + sanitized.substring(idx + 1);
}
}
return sanitized;
}
Здесь даже если исполнение каким-то чудом сможет распознать шаблон склеивания строк и подставить StringBuilder этот метод всё равно останется чертовски неэффективным.В такого рода задачах очень важно посмотреть на алгоритм с высокого уровня и описать простыми словами выполняемую работу. Здесь получается так: "заменить все двойные вхождения косой черты на единичные". Из строки знаки мы удалять не можем, но можем удалять из StringBuilder-а, а значит код выше можно превратить в этот:
private static String getSanitizedPath(String path) {
int index = path.indexOf("//");
if (index >= 0) {
StringBuilder sanitized = new StringBuilder(path);
while (index != -1) {
sanitized.deleteCharAt(index);
index = sanitized.indexOf("//", index); //не начинай сначала ;)
}
return sanitized.toString();
}
return path;
}
В некоторых случаях использование StringBuilder.deleteCharAt(int) позволяет существенно облегчить понимание кода:
// org.springframework.web.util.UrlPathHelper
private String removeSemicolonContentInternal(String requestUri) {
int semicolonIndex = requestUri.indexOf(';');
while (semicolonIndex != -1) {
int slashIndex = requestUri.indexOf('/', semicolonIndex);
String start = requestUri.substring(0, semicolonIndex);
requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
semicolonIndex = requestUri.indexOf(';', semicolonIndex);
}
return requestUri;
}
Логика здесь довольно запутанная, но на высоком уровне метод удаляет содержимое, выделенное ; внутри ссылки, превращая строку вроде /foo;f=F;o=O;o=O/bar;b=B;a=A;r=R в /foo/bar;b=B;a=A;r=R.Можно избавиться от взятия подстроки и склейки переписав метод вот так:
private static String removeSemicolonContentInternal(String requestUri) {
int semicolonIndex = requestUri.indexOf(';');
if (semicolonIndex == -1) {
return requestUri;
}
StringBuilder sb = new StringBuilder(requestUri);
while (semicolonIndex != -1) {
int slashIdx = sb.indexOf("/", semicolonIndex + 1);
if (slashIdx == -1) {
return sb.substring(0, semicolonIndex);
}
sb.delete(semicolonIndex, slashIdx);
semicolonIndex = sb.indexOf(";", semicolonIndex);
}
return sb.toString();
}
На первый взгляд, кода стало больше: 12 строк превратились в 16. С другой стороны, он стал выразительнее и проще для понимания, что идёт приятной добавкой к улучшенной производительности.Все изменения по теме, а также бенчмарк можно увидеть здесь.Мопед не мойВ этом разделе описано улучшение, автором которого я не являюсь, но которое представляет интерес.
// org.springframework.util.StringUtils
public static String trimLeadingWhitespace(String str) {
if (!hasLength(str)) {
return str;
}
StringBuilder sb = new StringBuilder(str);
while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) {
sb.deleteCharAt(0);
}
return sb.toString();
}
Чтобы улучшить этот код нужно вновь понять его высокоуровневый смысл (к счастью он полностью соответствует имени метода). Код удаляет все пробелы в начале строки, а значит вместо познакового удаления можно взять подстроку начиная от первого знака-непробела:
public static String trimLeadingWhitespace(String str) {
if (!hasLength(str)) {
return str;
}
int idx = 0;
while (idx < str.length() && Character.isWhitespace(str.charAt(idx))) {
idx++;
}
return str.substring(idx);
}
Этот код значительно быстрее первоначальной версии, а также потребляет меньше памяти, ведь в худшем случае создаётся только один объект (в лучшем при idx = 0 метод str.substring()вернёт строку, на которой он был вызван).Данный подход был использован для улучшения нескольких методов, больше подробностей тут.МелочьЕсли в коде есть что-то вроде
char[] chars;
//...
return new String(chars, 0, chars.length);
то это всегда можно переписать в виде
char[] chars;
//...
return new String(chars);
Производительность это сильно не улучшит, однако, перефразируя рекламу моющего средства из 90-х, "если не видно разницы, то зачем писать больше?" :)ЗаключениеНа этом всё, надеюсь примеры были вам интересны и полезны. Описывайте свои улучшения в комментариях, самые интересные добавлю в статью. До новых встреч!
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка веб-сайтов, ReactJS, Повышение конверсии, Поисковая оптимизация] SEO-оптимизация сайта на React или как добиться конверсии от поисковиков если у вас Single Page Application
- [Информационная безопасность, Python, Программирование, Машинное обучение, Искусственный интеллект] Deep Anomaly Detection
- [Python, Программирование] Аудит чат-ботов
- [Open source, JavaScript, HTML, Canvas, Инфографика] Опыт использования транслятора OberonJS для создания редактора интерактивных моделей
- [Java] Как Spring Data Jdbc соединяет таблицы
- [.NET, Разработка игр, Серверная оптимизация, C#] Череда проблем длиною в 16 лет
- [Программирование, Проектирование и рефакторинг, Профессиональная литература] Как событийно-ориентированная архитектура решает проблемы современных веб-приложений (перевод)
- [Высокая производительность, Системное администрирование, Apache, Big Data] Почему Kafka такая быстрая (перевод)
- [Программирование, Проектирование и рефакторинг] Рефакторинг без особой боли
- [Программирование, .NET, C#, Разработка под Linux, Разработка под Windows] Генерация типизированных ссылок на элементы управления Avalonia с атрибутом x:Name с помощью C# SourceGenerator
Теги для поиска: #_programmirovanie (Программирование), #_java, #_proizvoditelnost (производительность), #_java, #_stroki (строки), #_optimizatsija (оптимизация), #_optimizatsii (оптимизации), #_performans (перформанс), #_programmirovanie (
Программирование
), #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:31
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В продолжение своей предыдущей статьи о строках (напоминаю, это была текстовая версия доклада на конференции JPoint-2020) решил дописать ещё одну заметку со строковыми оптимизациями, обнаруженными уже после вёрстки презентации (первые две есть на видео в самом конце, показывал их прямо из "Идеи").Снова StringBuilder.append(char)На сцене снова "Спринг", а именно o.s.u.StringUtils.deleteAny(String, String): // org.springframework.util.StringUtils
public static String deleteAny(String inString, String charsToDelete) { if (!hasLength(inString) || !hasLength(charsToDelete)) { return inString; } StringBuilder sb = new StringBuilder(inString.length()); for (int i = 0; i < inString.length(); i++) { char c = inString.charAt(i); if (charsToDelete.indexOf(c) == -1) { sb.append(c); } } return sb.toString(); } public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) { return inString; } int lastCharIndex = 0; char[] result = new char[inString.length()]; for (int i = 0; i < inString.length(); i++) { char c = inString.charAt(i); if (charsToDelete.indexOf(c) == -1) { result[lastCharIndex++] = c; } } return new String(result, 0, lastCharIndex); } Benchmark Mode Score Error Units
original avgt 90.203 ± 4.317 ns/op patched avgt 25.391 ± 1.118 ns/op original:·gc.alloc.rate.norm avgt 104.000 ± 0.001 B/op patched:·gc.alloc.rate.norm avgt 104.000 ± 0.001 B/op public static String deleteAny(String inString, String charsToDelete) {
if (!hasLength(inString) || !hasLength(charsToDelete)) { return inString; } int lastCharIndex = 0; char[] result = new char[inString.length()]; for (int i = 0; i < inString.length(); i++) { char c = inString.charAt(i); if (charsToDelete.indexOf(c) == -1) { result[lastCharIndex++] = c; } } if (lastCharIndex == inString.length()) { // С - сообразительность return inString; } return new String(result, 0, lastCharIndex); } // org.springframework.http.ContentDisposition
private static String escapeQuotationsInFilename(String filename) { if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) { return filename; } boolean escaped = false; StringBuilder sb = new StringBuilder(); for (char c : filename.toCharArray()) { sb.append((c == '"' && !escaped) ? "\\"" : c); // <---- escaped = (!escaped && c == '\\'); } // Remove backslash at the end.. if (escaped) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } Character.valueOf(c)
-> StringBuilder.append(Object) -> String.valueOf() -> Character.toString() public final class Character {
private final char value; public String toString() { char buf[] = {value}; return String.valueOf(buf); } } for (int i = 0; i < filename.length() ; i++) {
char c = filename.charAt(i); if (!escaped && c == '"') { sb.append("\\""); // append(String) } else { sb.append(c); // append(char) } escaped = (!escaped && c == '\\'); } JDK 8
Benchmark latin len Score Units appendCovariant true 10 180.2 ± 10.3 ns/op appendExact true 10 68.5 ± 1.4 ns/op appendCovariant false 10 177.7 ± 4.4 ns/op appendExact false 10 67.7 ± 1.3 ns/op appendCovariant:·gc.alloc.rate.norm true 10 688.0 ± 0.0 B/op appendExact:·gc.alloc.rate.norm true 10 112.0 ± 0.0 B/op appendCovariant:·gc.alloc.rate.norm false 10 816.0 ± 0.0 B/op appendExact:·gc.alloc.rate.norm false 10 112.0 ± 0.0 B/op JDK 14 Benchmark latin len Score Units appendCovariant true 10 228.8 ± 18.6 ns/op appendExact true 10 57.9 ± 2.6 ns/op appendCovariant false 10 292.8 ± 12.4 ns/op appendExact false 10 90.2 ± 2.2 ns/op appendCovariant:·gc.alloc.rate.norm true 10 688.0 ± 0.0 B/op appendExact:·gc.alloc.rate.norm true 10 112.0 ± 0.0 B/op appendCovariant:·gc.alloc.rate.norm false 10 1096.0 ± 0.0 B/op appendExact:·gc.alloc.rate.norm false 10 200.0 ± 0.0 B/op Оставляю этот вопрос энтузиастам в качестве домашнего задания :)Коварный String.substringДавно известно, что метод String.substring всегда возвращает новую строку, и тем не менее в задачах на "выкусывание" он всё ещё пользуется незаслуженной популярностью: // org.springframework.web.util.UrlPathHelper
/** * Sanitize the given path replacing "//" with "/" */ private String getSanitizedPath(String path) { String sanitized = path; while (true) { int idx = sanitized.indexOf("//"); if (idx < 0) { break; } else { sanitized = sanitized.substring(0, idx) + sanitized.substring(idx + 1); } } return sanitized; } private static String getSanitizedPath(String path) {
int index = path.indexOf("//"); if (index >= 0) { StringBuilder sanitized = new StringBuilder(path); while (index != -1) { sanitized.deleteCharAt(index); index = sanitized.indexOf("//", index); //не начинай сначала ;) } return sanitized.toString(); } return path; } // org.springframework.web.util.UrlPathHelper
private String removeSemicolonContentInternal(String requestUri) { int semicolonIndex = requestUri.indexOf(';'); while (semicolonIndex != -1) { int slashIndex = requestUri.indexOf('/', semicolonIndex); String start = requestUri.substring(0, semicolonIndex); requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start; semicolonIndex = requestUri.indexOf(';', semicolonIndex); } return requestUri; } private static String removeSemicolonContentInternal(String requestUri) {
int semicolonIndex = requestUri.indexOf(';'); if (semicolonIndex == -1) { return requestUri; } StringBuilder sb = new StringBuilder(requestUri); while (semicolonIndex != -1) { int slashIdx = sb.indexOf("/", semicolonIndex + 1); if (slashIdx == -1) { return sb.substring(0, semicolonIndex); } sb.delete(semicolonIndex, slashIdx); semicolonIndex = sb.indexOf(";", semicolonIndex); } return sb.toString(); } // org.springframework.util.StringUtils
public static String trimLeadingWhitespace(String str) { if (!hasLength(str)) { return str; } StringBuilder sb = new StringBuilder(str); while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { sb.deleteCharAt(0); } return sb.toString(); } public static String trimLeadingWhitespace(String str) {
if (!hasLength(str)) { return str; } int idx = 0; while (idx < str.length() && Character.isWhitespace(str.charAt(idx))) { idx++; } return str.substring(idx); } char[] chars;
//... return new String(chars, 0, chars.length); char[] chars;
//... return new String(chars); =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_java |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:31
Часовой пояс: UTC + 5