Java недавно стукнуло 20 лет. Казалось бы, на сегодняшний день на java написано все. Любая идея, любой проект, любой инструмент на java? — это уже есть. Тем более когда речь идет о таких банальных вещах как пул соединений к базе данных, который используют миллионы разработчиков по всему миру. Но не тут то было! Встречайте — проект HikariCP — самый быстрый на сегодняшний день пул соединений на java.
HikariCP — еще один яркий пример того, что всегда стоить брать под сомнение эффективность некоторых решений, даже если их используют миллионы людей и живут они десятки лет. Хикари — прекрасный пример того, как микро оптимизации, которые по отдельности никогда не смогут дать вам больше 0.00001% прироста — в совокупности позволяют создать очень быстрый и эффективный инструмент.
Этот пост — вольный и частичный перевод статьи Down the Rabbit Hole от автора HikariCP перемешанный с потоком моего сознания.
Эта статья — рецепт нашего секретного соуса. Когда Вы начинаете просматривать разного рода бенчмарки, у Вас, как у нормального человека, должна возникнуть к ним здравая доля скептицизма. Когда Вы думаете о производительности и пуле соединений, трудно избежать коварной мысли о том, что пул — самая важная ее часть. На самом деле, это не совсем так. Количество вызовов getConnection() в сравнении с другими операциями типичного JDBC довольно мало. Огромное число улучшений производительности достигается за счет оптимизации враперов вокруг Connection, Statement, и тд.
Для того чтобы сделать HikariCP быстрым (каким он и является), нам пришлось копнуть до уровня байткода и ниже. Мы использовали все известные нам трюки чтобы JIT помог Вам. Мы изучали скомпилированный байткод для каждого метода и даже изменяли методы так, чтобы они попадали под лимит инлайнинга. Мы уменьшали количество уровней наследования, ограничивали доступ к некоторым переменным, чтобы уменьшить область их видимости и удаляли любые приведения типов.
Иногда, видя что метод превышает лимит инлайнинга, мы думали о том как изменить его таким образом, чтобы избавится от нескольких байт-инструкций. Например:
Достаточно простой метод, который проверяет, есть ли ошибка потери соединения. А теперь байткод:
Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций. Поэтому мы уделили некоторое внимание этому методу, чтобы сократить его и изменили его следующим образом:
Получилось довольно близко к лимиту, но все еще 36 инструкций. Поэтому мы сделали так:
Выглядит проще. Неправда ли? На самом деле, этот код хуже предыдущего — 45 инструкций.
Еще одна попытка:
Обратите внимание на использование унарного ИЛИ (|). Это отличный пример жертвования теоретической производительностью (так как в теории || будет быстрее) ради реальной производительности (так как метод теперь будет заинлайнен). Байткод результата:
Как раз ниже лимита в 35 байткод инструкций. Это маленький метод и на самом деле даже не высоконагруженный, но идею Вы поняли. Небольшие методы не только позволяют JITу встраивать их в код, они так же означают меньше фактических машинных инструкций, что увеличивает количество кода, который поместится в L1 кэше процессора. Теперь умножьте все это на количество таких изменений в нашей библиотеке и Вы поймете почему HickaryCP действительно быстр.
В HikariCP много микро оптимизаций. По отдельности они, конечно же, не делают картины. Но все вместе сильно увеличивают общую производительность. Некоторые из этих оптимизаций — это доли микросекунды для миллионов вызовов.
Одной из самых не тривиальных оптимизаций было удаление коллекции ArrayList<Statement> в классе ConnectionProxy, которая использовалась для отслеживания открытых объектов Statement. Когда Statement закрывается, он должен быть удален из этой коллекции. Также, в случае если закрывается соединение — нужно пройтись по коллекции и закрыть любой открытый Statement и уже после — очистить коллекцию. Как известно ArrayList осуществляет проверку диапазонов индекса на каждый вызов get(index). Но, так как мы можем гарантировать выбор правильного индекса — эта проверка излишня. Также, реализация метода remove(Object) осуществляет проход от начала до конца списка. В тоже время общепринятый паттерн в JDBC — или сразу закрывать Statements после использования или же в порядке обратном открытию (FILO). Для таких случаев, проход, который начинается с конца списка — будет быстрее. Поэтому мы заменили ArrayList<Statement> на FastStatementList в котором нету проверки диапазонов и удаление элементов из списка начинается с конца.
Для того, чтобы сгенерировать прокси для объектов Connection, Statement, ResultSet HikariCP изначально использовал фабрику синглтонов. В случае, например, ConnectionProxy эта фабрика находилось в статическом поле PROXY_FACTORY. И в коде было несколько десятков мест, которые ссылались на это поле.
В байткоде это выглядело так:
Вы можете увидеть, что первым идет вызов getstatic, чтобы получить значение статического поля PROXY_FACTORY. Так же обратите внимание на последний вызов invokevirtual для метода getProxyPreparedStatement() объекта ProxyFactory.
Оптимизация заключалась в том, что мы удалили фабрику синглтонов и заменили ее классом со статическими методами. Код стал выглядеть так:
Где getProxyPreparedStatement() — статический метод класса ProxyFactory. А вот так выглядит байткод:
Здесь следует обратить внимание сразу на 3 момента. Вызова getstatic больше нету. invokevirtual был заменен на invokestatic, который в свою очередь лучше оптимизируется виртуальной машиной. И последний момент, который трудно заметить — размер стека уменьшился с 5-ти элементов до 4-х. Так как до оптимизации в случае с invokevirtual на стек должна так же прийти ссылка на сам объект ProxyFactory. Это значит и дополнительную pop инструкцию для получения этой ссылки из стека в момент вызова getProxyPreparedStatement(). В общем, если просуммировать, то мы избавились от доступа к статическому полю, убрали лишние операции push и pop на стеке и сделали вызов метода более пригодным для оптимизации JIT.
Конец.
Полный оригинал Down the Rabbit Hole.
UPDATE:
В комментариях часть статьи «Медленный синглтон» вызвала много обсуждений. apangin утверждает, что все эти микро оптимизации бессмысленны и не дают никакого прироста. В коментарии приводится простой бенчмарк одинаковой стоимости invokeVirtual и invokeStatic. А тут бенчмарк пула соединений одноклассников, который якобы в 4 раза быстрее HickaryCP. На что автор HickaryCP дает следующий ответ:
First I would like to comment on @odnoklassniki comment that their pool is 4x faster. I have added their pool to the JMH benchmark and committed the changes for anyone to run. Here is the result vs. HikariCP:
This is showing HikariCP at 8x faster than one-datasource.
Keep in mind that not only has HikariCP changed since that wiki page was written, but the JMH test harness itself has changed. In order to recreate the results I got at that time, I checked out HikariCP source with that specific commit, and checked out the source just before that commit. I ran both using the benchmark harness available at that time:
Before static proxy factory methods:
After static proxy factory methods:
It shows a minor improvement after the change that is above the mean error.
Typically, every change is checked with the benchmark before being committed, so it is doubtful that we would have committed that change unless the benchmark showed improvement.
EDIT: And wow has HikariCP performance improved since January 2014!
HikariCP — еще один яркий пример того, что всегда стоить брать под сомнение эффективность некоторых решений, даже если их используют миллионы людей и живут они десятки лет. Хикари — прекрасный пример того, как микро оптимизации, которые по отдельности никогда не смогут дать вам больше 0.00001% прироста — в совокупности позволяют создать очень быстрый и эффективный инструмент.
Этот пост — вольный и частичный перевод статьи Down the Rabbit Hole от автора HikariCP перемешанный с потоком моего сознания.
Down the Rabbit Hole
Эта статья — рецепт нашего секретного соуса. Когда Вы начинаете просматривать разного рода бенчмарки, у Вас, как у нормального человека, должна возникнуть к ним здравая доля скептицизма. Когда Вы думаете о производительности и пуле соединений, трудно избежать коварной мысли о том, что пул — самая важная ее часть. На самом деле, это не совсем так. Количество вызовов getConnection() в сравнении с другими операциями типичного JDBC довольно мало. Огромное число улучшений производительности достигается за счет оптимизации враперов вокруг Connection, Statement, и тд.
Для того чтобы сделать HikariCP быстрым (каким он и является), нам пришлось копнуть до уровня байткода и ниже. Мы использовали все известные нам трюки чтобы JIT помог Вам. Мы изучали скомпилированный байткод для каждого метода и даже изменяли методы так, чтобы они попадали под лимит инлайнинга. Мы уменьшали количество уровней наследования, ограничивали доступ к некоторым переменным, чтобы уменьшить область их видимости и удаляли любые приведения типов.
Иногда, видя что метод превышает лимит инлайнинга, мы думали о том как изменить его таким образом, чтобы избавится от нескольких байт-инструкций. Например:
public SQLException checkException(SQLException sqle) {
String sqlState = sqle.getSQLState();
if (sqlState == null)
return sqle;
if (sqlState.startsWith("08"))
_forceClose = true;
else if (SQL_ERRORS.contains(sqlState))
_forceClose = true;
return sqle;
}
Достаточно простой метод, который проверяет, есть ли ошибка потери соединения. А теперь байткод:
0: aload_1
1: invokevirtual #148 // Method java/sql/SQLException.getSQLState:()Ljava/lang/String;
4: astore_2
5: aload_2
6: ifnonnull 11
9: aload_1
10: areturn
11: aload_2
12: ldc #154 // String 08
14: invokevirtual #156 // Method java/lang/String.startsWith:(Ljava/lang/String;)Z
17: ifeq 28
20: aload_0
21: iconst_1
22: putfield #144 // Field _forceClose:Z
25: goto 45
28: getstatic #41 // Field SQL_ERRORS:Ljava/util/Set;
31: aload_2
32: invokeinterface #162, 2 // InterfaceMethod java/util/Set.contains:(Ljava/lang/Object;)Z
37: ifeq 45
40: aload_0
41: iconst_1
42: putfield #144 // Field _forceClose:Z
45: aload_1
46: return
Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций. Поэтому мы уделили некоторое внимание этому методу, чтобы сократить его и изменили его следующим образом:
String sqlState = sqle.getSQLState();
if (sqlState != null && (sqlState.startsWith("08") || SQL_ERRORS.contains(sqlState)))
_forceClose = true;
return sqle;
Получилось довольно близко к лимиту, но все еще 36 инструкций. Поэтому мы сделали так:
String sqlState = sqle.getSQLState();
_forceClose |= (sqlState != null && (sqlState.startsWith("08") || SQL_ERRORS.contains(sqlState)));
return sale;
Выглядит проще. Неправда ли? На самом деле, этот код хуже предыдущего — 45 инструкций.
Еще одна попытка:
String sqlState = sqle.getSQLState();
if (sqlState != null)
_forceClose |= sqlState.startsWith("08") | SQL_ERRORS.contains(sqlState);
return sqle;
Обратите внимание на использование унарного ИЛИ (|). Это отличный пример жертвования теоретической производительностью (так как в теории || будет быстрее) ради реальной производительности (так как метод теперь будет заинлайнен). Байткод результата:
0: aload_1
1: invokevirtual #153 // Method java/sql/SQLException.getSQLState:()Ljava/lang/String;
4: astore_2
5: aload_2
6: ifnull 34
9: aload_0
10: dup
11: getfield #149 // Field forceClose:Z
14: aload_2
15: ldc #157 // String 08
17: invokevirtual #159 // Method java/lang/String.startsWith:(Ljava/lang/String;)Z
20: getstatic #37 // Field SQL_ERRORS:Ljava/util/Set;
23: aload_2
24: invokeinterface #165, 2 // InterfaceMethod java/util/Set.contains:(Ljava/lang/Object;)Z
29: ior
30: ior
31: putfield #149 // Field forceClose:Z
34: return
Как раз ниже лимита в 35 байткод инструкций. Это маленький метод и на самом деле даже не высоконагруженный, но идею Вы поняли. Небольшие методы не только позволяют JITу встраивать их в код, они так же означают меньше фактических машинных инструкций, что увеличивает количество кода, который поместится в L1 кэше процессора. Теперь умножьте все это на количество таких изменений в нашей библиотеке и Вы поймете почему HickaryCP действительно быстр.
Микро оптимизации
В HikariCP много микро оптимизаций. По отдельности они, конечно же, не делают картины. Но все вместе сильно увеличивают общую производительность. Некоторые из этих оптимизаций — это доли микросекунды для миллионов вызовов.
ArrayList
Одной из самых не тривиальных оптимизаций было удаление коллекции ArrayList<Statement> в классе ConnectionProxy, которая использовалась для отслеживания открытых объектов Statement. Когда Statement закрывается, он должен быть удален из этой коллекции. Также, в случае если закрывается соединение — нужно пройтись по коллекции и закрыть любой открытый Statement и уже после — очистить коллекцию. Как известно ArrayList осуществляет проверку диапазонов индекса на каждый вызов get(index). Но, так как мы можем гарантировать выбор правильного индекса — эта проверка излишня. Также, реализация метода remove(Object) осуществляет проход от начала до конца списка. В тоже время общепринятый паттерн в JDBC — или сразу закрывать Statements после использования или же в порядке обратном открытию (FILO). Для таких случаев, проход, который начинается с конца списка — будет быстрее. Поэтому мы заменили ArrayList<Statement> на FastStatementList в котором нету проверки диапазонов и удаление элементов из списка начинается с конца.
Медленный синглтон
Для того, чтобы сгенерировать прокси для объектов Connection, Statement, ResultSet HikariCP изначально использовал фабрику синглтонов. В случае, например, ConnectionProxy эта фабрика находилось в статическом поле PROXY_FACTORY. И в коде было несколько десятков мест, которые ссылались на это поле.
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return PROXY_FACTORY.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
В байткоде это выглядело так:
public final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=5, locals=3, args_size=3
0: getstatic #59 // Field PROXY_FACTORY:Lcom/zaxxer/hikari/proxy/ProxyFactory;
3: aload_0
4: aload_0
5: getfield #3 // Field delegate:Ljava/sql/Connection;
8: aload_1
9: aload_2
10: invokeinterface #74, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
15: invokevirtual #69 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
18: return
Вы можете увидеть, что первым идет вызов getstatic, чтобы получить значение статического поля PROXY_FACTORY. Так же обратите внимание на последний вызов invokevirtual для метода getProxyPreparedStatement() объекта ProxyFactory.
Оптимизация заключалась в том, что мы удалили фабрику синглтонов и заменили ее классом со статическими методами. Код стал выглядеть так:
public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
{
return ProxyFactory.getProxyPreparedStatement(this, delegate.prepareStatement(sql, columnNames));
}
Где getProxyPreparedStatement() — статический метод класса ProxyFactory. А вот так выглядит байткод:
private final java.sql.PreparedStatement prepareStatement(java.lang.String, java.lang.String[]) throws java.sql.SQLException;
flags: ACC_PRIVATE, ACC_FINAL
Code:
stack=4, locals=3, args_size=3
0: aload_0
1: aload_0
2: getfield #3 // Field delegate:Ljava/sql/Connection;
5: aload_1
6: aload_2
7: invokeinterface #72, 3 // InterfaceMethod java/sql/Connection.prepareStatement:(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;
12: invokestatic #67 // Method com/zaxxer/hikari/proxy/ProxyFactory.getProxyPreparedStatement:(Lcom/zaxxer/hikari/proxy/ConnectionProxy;Ljava/sql/PreparedStatement;)Ljava/sql/PreparedStatement;
15: areturn
Здесь следует обратить внимание сразу на 3 момента. Вызова getstatic больше нету. invokevirtual был заменен на invokestatic, который в свою очередь лучше оптимизируется виртуальной машиной. И последний момент, который трудно заметить — размер стека уменьшился с 5-ти элементов до 4-х. Так как до оптимизации в случае с invokevirtual на стек должна так же прийти ссылка на сам объект ProxyFactory. Это значит и дополнительную pop инструкцию для получения этой ссылки из стека в момент вызова getProxyPreparedStatement(). В общем, если просуммировать, то мы избавились от доступа к статическому полю, убрали лишние операции push и pop на стеке и сделали вызов метода более пригодным для оптимизации JIT.
Конец.
Полный оригинал Down the Rabbit Hole.
UPDATE:
В комментариях часть статьи «Медленный синглтон» вызвала много обсуждений. apangin утверждает, что все эти микро оптимизации бессмысленны и не дают никакого прироста. В коментарии приводится простой бенчмарк одинаковой стоимости invokeVirtual и invokeStatic. А тут бенчмарк пула соединений одноклассников, который якобы в 4 раза быстрее HickaryCP. На что автор HickaryCP дает следующий ответ:
First I would like to comment on @odnoklassniki comment that their pool is 4x faster. I have added their pool to the JMH benchmark and committed the changes for anyone to run. Here is the result vs. HikariCP:
./benchmark.sh clean quick -p pool=one,hikari ".*Connection.*"
Benchmark (pool) Mode Cnt Score Error Units
ConnectionBench.cycleCnnection one thrpt 16 4991.293 ± 62.821 ops/ms
ConnectionBench.cycleCnnection hikari thrpt 16 39660.123 ± 1314.967 ops/ms
This is showing HikariCP at 8x faster than one-datasource.
Keep in mind that not only has HikariCP changed since that wiki page was written, but the JMH test harness itself has changed. In order to recreate the results I got at that time, I checked out HikariCP source with that specific commit, and checked out the source just before that commit. I ran both using the benchmark harness available at that time:
Before static proxy factory methods:
Benchmark (pool) Mode Samples Mean Mean error Units
ConnectionBench.testConnectionCycle hikari thrpt 16 9303.741 67.747 ops/ms
After static proxy factory methods:
Benchmark (pool) Mode Samples Mean Mean error Units
ConnectionBench.testConnectionCycle hikari thrpt 16 9436.699 71.268 ops/ms
It shows a minor improvement after the change that is above the mean error.
Typically, every change is checked with the benchmark before being committed, so it is doubtful that we would have committed that change unless the benchmark showed improvement.
EDIT: And wow has HikariCP performance improved since January 2014!
apangin
«Оптимизировать» программу на уровне байткода — всё равно что улучшать автомобиль, выкидывая «лишние» детали.
doom369
А что тут не так? Может я как-то не правильно перевел.
У автора есть решение с открытым кодом, которым пользуются очень много людей. Есть бенчмарки, подтверждающие его слова. Все есть на гитхабе. Буду не против увидеть реальное опровержение слов автора.
apangin
Лучше приведите исходники этих бенчмарков (на гитхабе проекта не нашёл), и я скажу, где они врут.
Вы перевели всё честно, здесь спору нет, но ерунда написана в оригинальной статье.
johndow
github.com/brettwooldridge/HikariCP-benchmark
Hikari реально быстрая штука, используем давно — не нарадуемся.
apangin
Спасибо за ссылку.
Действительно, бенчмарк оказался ни о чём. Проверяются пустые стабы, не имеющие ничего общего с реальными Connection и Statement.
В таком случае наш Datasource почти в 4 раза быстрее на том же бенчмарке, и тогда заголовок статьи про «самый быстрый пул соединений» — тем более надувательство.
doom369
Ну на момент написания статьи Вашего кода даже не было в репозитории. Так что — нет. Не надувательство.
leventov
А может, эта статья — постановочный заход к публикации Андреем one-datasource?!
doom369
Автор хикари ответил Вам. И утверждает что одноклассники в 8 раз медленее, а не быстрее. К сожалению у меня нету приглоса, так что вот ответ github.com/brettwooldridge/HikariCP/issues/464#issuecomment-149141231
apangin
Как говорится, бывает ложь, большая ложь, и бенчмарки :) Всегда можно написать бенчмарк, который «докажет» что угодно, если не понимать, что именно он измеряет. А в случае с пулами, очевидно, запустив пустой getConnection() в 8 потоках, автор измерил стоимость contended блокировки. Очень полезно. Особенно, когда в продакшне даже при 5000 запросах в секунду у нас contention наблюдается в < 0.5% случаев. Хотя uncontended случай, на котором one-datasource оказался внезапно быстрее, куда ближе к реальности, на самом деле, и он абсолютно бесполезен, покуда в жизни приложение обычно занимается запросами к базе, а не синхронизацией на пуле.
Окей, пускай даже автор сэкономил 500 наносекунд на доставании коннекшна из пула, и тут же потерял целую миллисекунду (в 2000 раз больше!) на валидации этого коннекшна. То, что товарищ назвал недостатком one-datasource, на самом деле сделано специально: мы целенаправлено избавились от валидации, заменив её ретраями уровнем выше, чтобы в два раза сократить количество запросов к базе и значительно снизить latency.
Забавно, что автор Hikari указывает на наши якобы проблемы с транзакциями. При том, что его пул не поддерживает работу с TransactionManager в принципе! Т.е. вообще не работает! Если приглядеться, все озвученные «проблемы» — это неправильное использование DataSourceImpl. one-datasource используется только внутри наших проектов в контролируемом контексте. У нас нет планов делать из него open source продукт и, тем более, мериться с кем-либо. Я его выложил на GitHub, только чтобы показать несостоятельность бенчмарка.
RusSuckOFF
Автор хикари так же провел бенчмарк if vs try-catch на Java 7 и Java 8 и получил совсем странные результаты. Не могли бы тоже прокомментировать?
apangin
В аду приготовлен отдельный котёл для тех, кто делает выводы о производительности по одному бенчмарку без какого-либо анализа :) Этот бенчмарк, как зачастую и бывает, жульничает :)
Ничего не скажу про Java 7u60, т.к. под рукой есть только 7u80, который ведёт себя так же, как и Java 8. А что касается Java 8u60, дела обстоят так. В бенчмарке по умолчанию у списка выставлен initialCapacity = 32, но при этом добавляется лишь 15 элементов. Т.е. выхода за пределы массива никогда не происходит. Естественно, JIT это спекулятивно оптимизирует, ставя uncommon trap и выкидывая эту ветку вообще. В итоге оба варианта компилируются одинаково.
Зато, если поставить initialCapacity=14 или меньше, в профиле останется статистика, что исключение выкидывалось, и JIT уже по-честному скомпилирует ветку для расширения массива. При этом результаты для варианта с try-catch окажутся удручающими:
Впрочем, и такому бенчмарку нельзя верить, потому как реальный сценарий на продакшне может оказаться совсем другим. И не дай бог кто-то вдруг заиспользует List, основанный на try/catch, не для долгоживущих списков, а для типичного сценария «создал-поработал-забыл», где исключения начнут выскакивать часто, и тогда начнётся самое интересное.
qwwdfsad
А почему JIT не смог в таком простом случае выкинуть лишний range check?
xhumanoid
в тему бенчмарков
неужто нас ожидает медленная java? =)
p.s. по асму там перед сохранением в массив пачка магии включая локи или это издержки дебаг версии?
apangin
Посмотрел, действительно, в JDK 9 сгенерированный код слегка распух. «Магия» — это G1 барьеры, коими сопровождается каждый апдейт ссылки в хипе. В JDK 9 G1 стал дефолтным коллектором, отсюда и разница. Стало быть, для честного сравнения надо запускать с
-XX:+UseParallelGC
.Ещё одно подтверждение тому, что микробенчмарки зачастую измеряют совсем другое, чем хотелось, и без должного анализа смысла не имеют. Если кто-то говорит, «мой код быстрее в 10 раз, я проверил бенчмарком», и при этом даже не смотрит на логи компилятора, не говоря уж об ассемблере, значит, он безбожно врёт :)
lany
На самом деле код автора написан под конкретный юзкейс — складывать стейтменты внутри коннекшна. И тут же, кстати, у меня возникает ощущение, что всё это абсолютно зря. Давайте на обратные числа посмотрим, что оптимизирует сам автор:
ArrayList: 373 нс
Список с if на Java 7u60: 70 нс
Список исключением на Java 7u60: 25 нс
То есть при переходе с ArrayList на if он выигрывает 303 нс на SQL-запрос, а при переходе на исключения — ещё 45 нс. Мне совершенно не верится, что в контексте выполнения целого SQL-запроса и чтения результатов из него даже отказ от ArrayList дал заметный прирост. Не говоря уж про исключение. В относительных цифрах-то смотрится солидно, но в абсолютных как-то не очень.
xhumanoid
причем это не объясняет почему так произошло, только предположения о том, что exception работают быстрее.
на практике в случае с if в его тестах сразу отваливается inlining на add(), а уже потом еще и на remove(). как итог тест полностью бесполезен.
на свежих версиях jdk при срабатывании инлайнинга на обоих методах поведение идентичное, но автора это не останавливает =( начинаются споры по поводу использования в проде старых 6 и 7 версий
doom369
del
doom369
При этом сами же используете подобные трюки.
lany
Вы не знаете, о чём говорите. В моём случае есть тесты, на которых такой трюк ускоряет производительность в 1000 раз, либо вообще делает рабочим код, который бы иначе упал с OutOfMemory. Скажем, задача вычитать ровно один элемент из сплитератора, который пришёл из неизвестного источника. Создайте такой сплитератор для примера:
Запустите ужасный некрасивый код, похожий на то, что я пишу:
И сравните с официальным красивым, модным и рекомендуемым способом сделать то же самое:
Никакой JMH вам не потребуется, разницу увидите невооружённым взглядом. Вся красота разбивается о суровую действительность.
doom369
Да. Не знаю. Но я и не делаю отсюда заключений, вроде:
lany
Вы пытаетесь меня убедить, что если вы не разбираетесь в X и не делаете никаких заключений, значит, я не разбираюсь в Y и тоже не должен делать заключений? :-)
doom369
=) нет конечно. Вы в праве делать все что хотите.
doom369
А Вы можете в тикет ответить на гитхабе? Мне как-то не хочеться постоянно проксировать ответы =).
apangin
А и не надо ничего проксировать. Никому от этого лучше не станет.
doom369
Ну почему же, вдруг Вы укажете на некую лже-оптимизацию и это ускорит хикари. Все пользователи от этого выиграют.
doom369
github.com/brettwooldridge/HikariCP/wiki«My-benchmark-doesn't-show-a-difference.» вот еще про то что они тестят.
evnuh
Вас спросили что не так с 35 инструкциями, конкретно, и почему это — ерунда?
Quetzal
Потому что это вовсе не лимит инлайнинга. Это значение регулируется параметром JVM (-XX:MaxInlineSize=N) и по-умолчанию оно 35 байт.
doom369
А что же?
apangin
Потому что лимит задаётся совсем другими параметрами JVM. HotSpot JVM может инлайнить методы гораздо длиннее 35 байткодов. Смотрите FreqInlineSize.
Если всё ещё не убедительно, проверьте сами.
Заодно и развеем миф invokestatic vs. invokevirtual:
Или запустите с -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining:
doom369
Так в статье и не про это. Просто мимоходом упоминается, что есть вот такая штука длина метода в байткодах для инлайнинга, дефолтная она 35 байткодов. Может Вас смутило слово «лимит»? Окей извиняюсь за плохой перевод.
А в статье не говорится что вызов invokestatic быстрее invokevirtual. Там говорится, что у JIT большей вохможностей по оптимизации в случае invokestatic.
apangin
MaxInlineSize=35
, но он означает совсем не то, что думает автор. Это лишь некая эвристика для инлайнинга, и она не мешает заинлайниться и более длинному методу, если JIT сочтёт нужным. Попытки «помочь» JIT-компилятору могут привести ровно к обратному эффекту. МетодcheckException
не листовой. Принудительно заинлайнив его, автор, например, рискует лишиться инлайнинга вложенных методов, тем самым получив вместо одного вызова два или три.А это тоже неправда. Вот, навскидку, ровно противоположный пример.
А уж чего стоит аргумент про уменьшение стека с 5 до 4 элементов! С таким же успехом можно сказать, что программа будет работать быстрее и занимать меньше памяти, если названия всех переменных сократить до 1-2 букв :)
doom369
Ок, спасибо за развернутый ответ. Думаю автор просто не усложнял в контексте той статьи.
lany
Дело не в том, что автор «не усложнял», а в том, что он просто неправ. По факту
MaxInlineSize
не играет никакой роли для горячего кода. Утверждение «Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций» — абсолютная ложь, а не просто «упрощение». И попытки вогнать метод в 34 байта байткода бессмысленны. В контексте статьи автор как раз усложнил себе жизнь, делая бессмысленные операции. Если оптимизировать инлайнинг, надо не гадать, а хотя бы посмотреть, что реально инлайнится, а что — нет. Для этого есть опция+PrintInlining
. Про неё же ни слова в статье.doom369
Потому что статья не про инлайнинг. А про то, что делал автор, чтобы создать быстрое решение. Нету смысла описывать детали работы инлайнинга, если цель просто описать подходы, что применялись. Иначе надо было бы писать десятки постов.
lany
Не понял. Что значит, статья не про инлайнинг? Я вижу три экрана про инлайнинг, начиная с заголовка «Down the Rabbit Hole». Почти добрая половина статьи. Окей, я тогда скажу, что подходы неверные. Или вы ответите, что статья не про подходы? :-)
doom369
Просто кода много =).
Ну как бы популярность решения и куча счастливых пользователей опровергают ваше утверждение. Если вы считаете, что данные подходы неверны, то было бы не плохо это увидеть на конкретных примерах.
lany
Вот тут вы мух с котлетами путаете. Если человек написал код, пусть даже популярный и быстрый (не факт, но допустим), это не значит, что его объяснение, почему код быстрый, верно. Представьте, миллиардера спросили, как ему удалось стать миллиардером, а он ответил, что молится летающему макаронному монстру, и монстр ему помог стать миллиардером. Да, налицо, перед нами миллиардер. Но это не означает, что его слова имеют отношение к истине. Даже если он сам в них верит.
Конкретные примеры вам Андрей Паньгин разъяснил, тут вроде бы нечего добавить.
doom369
Это делаете Вы приводя ложную аналогию. Представьте, миллиардера спросили, как ему удалось стать миллардером, он ответил, что долго и упорно работал и это помогло ему стать миллиардером. Вы же утверждаете, что этот труд не помог есть стать миллардером, а причина в молитве макаронному монстру.
Андрей привел пример FastList.add() утверждая, что такого рода оптимизация — это скорее деоптимизация. Пока из их раговора я не понял, кто же прав.
xhumanoid
делать выводы на основе байткода совершенно неверно, то что в некоторых случаях это сработало, закрепляет неправильные предложения и в итоге уводят в совершенно непонятных направлениях с постепенным превращением всего этого в cargo культ. посмотрите на статью автора с его манипуляциями байткодом с пачкой необоснованных утверждение и статьи Шипилева или Томпсона, разница гигантская, так как у них идет разбор на уровне asm и иногда железа, что же у нас происходит на самом деле.
прав некорректный бенчмарк:
1) автор сравнивает не просто add(), но и тут же remove(), что вносит дополнительную погрешность
2) автор утверждает, что методы add() имеют минимальные отличия, на практике в случае if в его тесте метод add() не инлайнится, а идет явный вызов
3) автор утверждает, что методы remove() полностью идентичны, на практике в случае if в его тесте метод remove() не инлайнится (предположительно предыдущий add() сломал оптимизации последующие), а идет явный вызов
по итогу:
1) метод с exception идет линейно, без дополнительных вызовов, к тому же нету случая когда этот exception выбрасывается
2) метод с if сразу производит 15 вызовов функции add() и следом 15 вызовов remove().
что мы вообще меряем? стоимость 30 вызовов функций? логично что вызывать их будет дороже, так как переходы не бесплатны и заметно дороже линейного выполнения кода
xhumanoid
первый же раздел Down the Rabbit Hole
извиняюсь за то, что пришлось перепостить половину статьи, но первый раздел как раз по поводу инлайнинга и гадания на байткоде.
Общения в github, где автор на основе идентичности методов утверждает, что в его тесте они тоже работают одинаково радует. так же как и желания тестировать только на старых версиях jvm, которые работают не всегда корректно.
xhumanoid
>> Для этого есть опция +PrintInlining
данная опция на версиях jvm, что использует автор крешит jvm на его бенчмарках. А так да, основная масса выводов автором сделана на основе анализа байткода, без самого анализа что же у нас происходит в реале
apangin
Зато я нашёл в исходниках ещё одну замечательную «оптимизацию» :)
Почему это не оптимизация, а совсем наоборот, я рассказывал на JPoint в презентации про устройство виртуальной машины HotSpot.
doom369
С одной стороны Вы правы. С другой — вполне допускаю, что за время жизни пула исключение бросится несколько раз. В то время как add/remove будут происходить постоянно. И в долгосрочной перспективе, такая оптимизая выгодней «If».
apangin
Так а в чём здесь оптимизация, поясните?
doom369
Ну я так понял, что цель всей той колбасы просто избежать условия
if (size < elements.length)
Вообще я задал вопрос автору. Так что подождем. Я так не оптимизирую, если что =).
apangin
Избежать условия не получится. В принципе. По ссылке выше рассказано, почему.
doom369
Из ссылки выше:
Речь об этом условии. Я так понимаю что автор Хикари избегает его. Так как выбрасывание эксепшена за жизнь пула происходит раз-два и все.
apangin
А как, по-вашему, JVM понимает, что пора выкинуть exception, не проверяя условия?
doom369
Я не говорил, что не JVM проверяет диапозоны массива.
GrigoryPerepechko
try сам по себе дорогой. Я почти уверен что намного дороже if.
Вы ж не думаете что исключения дорогие только когда кидаются. Нужна ведь инфраструктура которая их ловит.
johndow
apangin с вами не согласен :)
doom369
Стоиомсть try не важна, если пул живет месяцами и за этот месяц try вызывается пару раз для увеличения списка. В то время как add/remove выполняются каждую секунду сотни раз в случае нагруженности пула. Но это лишь мое предположение.
doom369
Завел тикет с вопросом автору — github.com/brettwooldridge/HikariCP/issues/462 =). Посмотрим что он ответит.
Moxa
я себе написал StringBuilder с аналогичной оптимизацией, если создавать новый билдер каждый раз, то он на порядок медленнее дефолтного, но если переиспользовать сущействующий, чтобы массив расширился до некоторого максимально значения только один раз, то такая оптимизация дает выигрыш примерно в 30%
doom369
JVM и так отлично оптимизирует StringBuilder.
компилятор заоптимайзит в new char[1+s.length]. Опция — OptimizeStringConcat. Так что прирост очень сомнителен.
Moxa
в таком простом варианте, наверно, да, заоптимизирует… но в моем случае — есть бенчмарки, я вижу какой вариант быстрее
doom369
Давайте бенчмарки. А вдруг там ошибка =)?
Moxa
github.com/wizzardo/benchmarks/blob/master/src%2Fjmh%2Fjava%2Fcom%2Fwizzardo%2Fbenchmarks%2FStringBuilderBenchmarks.java
leventov
Все по кругу
doom369
Ну вроде нет — github.com/brettwooldridge/HikariCP/issues/464
doom369
github.com/brettwooldridge/HikariCP/issues/462
lany
Как я понял, это отсюда код. У меня в другом месте facepalm ещё случился: ну зачем же
ArrayList
-то расширять? Мало граблей на этом собрали? Если в девятке вдруг во все листы добавят новый метод (скажем, parallelSort), и уArrayList
будет оптимизированная реализация, в данном классе он будет молча ничего не делать. Можно будет хвастаться, чтоFastList
нереально быстро сортирует в параллель :-) Неужели мало граблей на этом в восьмёрке собрали? Что плохого в том, чтобыAbstractList
наследовать?lany
Я, кстати, нагрешил тут недавно в своей либе — использую кастомное исключение для control flow. В этом плане очень помог конструктор
Error(null, null, false, false)
, который в несколько раз ускоряет создание исключения. Наблюдаю константную потерю на выкидывании где-то 200-300 наносекунд (против примерно 1500±длина стектрейса при обычном исключении). В моём случае эта константная потеря окупается, потому что альтернативный вариант без исключения затормаживает даже на не очень больших объёмах данных (только на совсем маленьких входных данных наблюдается проигрыш при использовании исключения).doom369
Речь о
?
И что за библиотека такая, где надо волноваться о таких мелочах?
lany
Нет, речь о tryAdvance/forEachRemaining. Это отнюдь не мелочи.
Alexins
Не в упрек автору данной темы.
Не поленился, сходил на github проекта. Посмотрел код. Я в шоке.
Тут рассказывают о том, как удалось сократить количество JVM инструкций с 35 до 34, забывая про генерацию кода с помощью javassist.
Может кто-то объяснит доходчиво, как динамическое создание класса, с помощью javassist ускоряет работу 34 инструкций перед 35 инструкциями.
doom369
А как эти 2 вещи вообще связаны =)?
Alexins
Я не сильно разбираюсь в оптимизации JIT. Разве генерация дополнительного кода не вносит затраты на его исполнение?
Уточню вопрос. Дело в том, что система генерирует байт код для каждого нового DataSource, используя старый код только в виде шаблона.
У меня вопрос. Этот самый пул соединений, он стоит поверх JDBC пула.
Какова производительность этого пула при достижении максимума соединений или запросов в базовом пуле?
Имеется в виду, что остановка некоторых потоков в режиме ожидания новых ресурсов, не влияет на производительность других потоков.
Ведь там используется не честная блокировка (первый запросил, первый получил).
Еще один вопрос можно? Как данный пул можно использовать, скажем в WildFly с исходным XADataSource?
Vanger13
Все это оч мило и приятно конечно — простым прописыванием library depndency получить «самый быстрый connection pool». Но, если кто-то по производительности уперся в переиспользование(!) коннектов в пуле(!), то я уже искренне бы поздравил этих людей. Да и скорее всего у них наверняка уже написан свой аналог Hikari.
tmk826
Используем HikariCP уже два года в нашем проекте. Прирост в производителъности ноль. Единственая разница с предыдущим пулом (c3p0) в том, что HikariCP активно развивается.
guai
ковырял не так давно код нескольких пулов. Хикари написан наиболее понятно. Хотя бы за это его автору респект.