HikariCP — самый быстрый пул соединений на java

Java недавно стукнуло 20 лет. Казалось бы, на сегодняшний день на java написано все. Любая идея, любой проект, любой инструмент на java? — это уже есть. Тем более когда речь идет о таких банальных вещах как пул соединений к базе данных, который используют миллионы разработчиков по всему миру. Но не тут то было! Встречайте — проект HikariCP — самый быстрый на сегодняшний день пул соединений на java.

HikariCP — еще один яркий пример того, что всегда стоить брать под сомнение эффективность некоторых решений, даже если их используют миллионы людей и живут они десятки лет. Хикари — прекрасный пример того, как микро оптимизации, которые по отдельности никогда не смогут дать вам больше 0.00001% прироста — в совокупности позволяют создать очень быстрый и эффективный инструмент.

Этот пост — вольный и частичный перевод статьи Down the Rabbit Hole от автора HikariCP перемешанный с потоком моего сознания.

image

Эта статья — рецепт нашего секретного соуса. Когда Вы начинаете просматривать разного рода бенчмарки, у Вас, как у нормального человека, должна возникнуть к ним здравая доля скептицизма. Когда Вы думаете о производительности и пуле соединений, трудно избежать коварной мысли о том, что пул — самая важная ее часть. На самом деле, это не совсем так. Количество вызовов 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.

© Habrahabr.ru