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

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

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

image



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!

Комментарии (72)


  1. apangin
    18.10.2015 16:51
    +3

    лимит инлайнинга в Hostpot JVM — 35 байткод инструкций
    После этой ерунды можно дальше не читать. И что invokestatic якобы эффективнее invokevirtual, и что «лишние» push/pop якобы влияют на скорость — всё от начала и до конца — сплошная неправда. Уберите, пожалуйста, статью, чтоб не путать людей.

    «Оптимизировать» программу на уровне байткода — всё равно что улучшать автомобиль, выкидывая «лишние» детали.


    1. doom369
      18.10.2015 17:04
      +2

      После этой ерунды можно дальше не читать.

      А что тут не так? Может я как-то не правильно перевел.

      всё от начала и до конца — сплошная неправда

      У автора есть решение с открытым кодом, которым пользуются очень много людей. Есть бенчмарки, подтверждающие его слова. Все есть на гитхабе. Буду не против увидеть реальное опровержение слов автора.


      1. apangin
        18.10.2015 17:31
        +3

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


        1. johndow
          18.10.2015 17:39

          github.com/brettwooldridge/HikariCP-benchmark

          Hikari реально быстрая штука, используем давно — не нарадуемся.


          1. apangin
            19.10.2015 01:39
            +5

            Спасибо за ссылку.
            Действительно, бенчмарк оказался ни о чём. Проверяются пустые стабы, не имеющие ничего общего с реальными Connection и Statement.

            В таком случае наш Datasource почти в 4 раза быстрее на том же бенчмарке, и тогда заголовок статьи про «самый быстрый пул соединений» — тем более надувательство.


            1. doom369
              19.10.2015 01:59

              и тогда заголовок статьи про «самый быстрый пул соединений» — тем более надувательство.


              Ну на момент написания статьи Вашего кода даже не было в репозитории. Так что — нет. Не надувательство.


              1. leventov
                19.10.2015 03:13
                +5

                А может, эта статья — постановочный заход к публикации Андреем one-datasource?!


            1. doom369
              19.10.2015 11:34

              Автор хикари ответил Вам. И утверждает что одноклассники в 8 раз медленее, а не быстрее. К сожалению у меня нету приглоса, так что вот ответ github.com/brettwooldridge/HikariCP/issues/464#issuecomment-149141231


              1. apangin
                19.10.2015 17:56
                +1

                Как говорится, бывает ложь, большая ложь, и бенчмарки :) Всегда можно написать бенчмарк, который «докажет» что угодно, если не понимать, что именно он измеряет. А в случае с пулами, очевидно, запустив пустой getConnection() в 8 потоках, автор измерил стоимость contended блокировки. Очень полезно. Особенно, когда в продакшне даже при 5000 запросах в секунду у нас contention наблюдается в < 0.5% случаев. Хотя uncontended случай, на котором one-datasource оказался внезапно быстрее, куда ближе к реальности, на самом деле, и он абсолютно бесполезен, покуда в жизни приложение обычно занимается запросами к базе, а не синхронизацией на пуле.

                Окей, пускай даже автор сэкономил 500 наносекунд на доставании коннекшна из пула, и тут же потерял целую миллисекунду (в 2000 раз больше!) на валидации этого коннекшна. То, что товарищ назвал недостатком one-datasource, на самом деле сделано специально: мы целенаправлено избавились от валидации, заменив её ретраями уровнем выше, чтобы в два раза сократить количество запросов к базе и значительно снизить latency.

                Забавно, что автор Hikari указывает на наши якобы проблемы с транзакциями. При том, что его пул не поддерживает работу с TransactionManager в принципе! Т.е. вообще не работает! Если приглядеться, все озвученные «проблемы» — это неправильное использование DataSourceImpl. one-datasource используется только внутри наших проектов в контролируемом контексте. У нас нет планов делать из него open source продукт и, тем более, мериться с кем-либо. Я его выложил на GitHub, только чтобы показать несостоятельность бенчмарка.


                1. RusSuckOFF
                  19.10.2015 18:34

                  Автор хикари так же провел бенчмарк if vs try-catch на Java 7 и Java 8 и получил совсем странные результаты. Не могли бы тоже прокомментировать?


                  1. apangin
                    19.10.2015 21:47
                    +1

                    В аду приготовлен отдельный котёл для тех, кто делает выводы о производительности по одному бенчмарку без какого-либо анализа :) Этот бенчмарк, как зачастую и бывает, жульничает :)

                    Ничего не скажу про Java 7u60, т.к. под рукой есть только 7u80, который ведёт себя так же, как и Java 8. А что касается Java 8u60, дела обстоят так. В бенчмарке по умолчанию у списка выставлен initialCapacity = 32, но при этом добавляется лишь 15 элементов. Т.е. выхода за пределы массива никогда не происходит. Естественно, JIT это спекулятивно оптимизирует, ставя uncommon trap и выкидывая эту ветку вообще. В итоге оба варианта компилируются одинаково.

                    Зато, если поставить initialCapacity=14 или меньше, в профиле останется статистика, что исключение выкидывалось, и JIT уже по-честному скомпилирует ветку для расширения массива. При этом результаты для варианта с try-catch окажутся удручающими:

                    Benchmark               (initCapacity)  (listImpl)   Mode  Cnt      Score     Error   Units
                    FastListBench.testList              14         new  thrpt    6   6444,598 ± 195,891  ops/ms
                    FastListBench.testList              14        orig  thrpt    6  18426,890 ± 790,905  ops/ms
                    FastListBench.testList              15         new  thrpt    6  19122,083 ± 436,809  ops/ms
                    FastListBench.testList              15        orig  thrpt    6  18567,706 ± 101,215  ops/ms
                    

                    Впрочем, и такому бенчмарку нельзя верить, потому как реальный сценарий на продакшне может оказаться совсем другим. И не дай бог кто-то вдруг заиспользует List, основанный на try/catch, не для долгоживущих списков, а для типичного сценария «создал-поработал-забыл», где исключения начнут выскакивать часто, и тогда начнётся самое интересное.


                    1. qwwdfsad
                      19.10.2015 23:55

                      А почему JIT не смог в таком простом случае выкинуть лишний range check?

                      PrintAssembly для add
                        0x000000010f02e688: sub    $0x30,%rsp         ;*synchronization entry
                                                                      ; - com.zaxxer.microbench.FastList2::add@-1 (line 73)
                      
                        0x000000010f02e68c: mov    0x20(%rsi),%ebx    ;*getfield elementData
                                                                      ; - com.zaxxer.microbench.FastList2::add@5 (line 73)
                      
                        0x000000010f02e68f: mov    0xc(%r12,%rbx,8),%r11d  ;*arraylength
                                                                      ; - com.zaxxer.microbench.FastList2::add@8 (line 73)
                                                                      ; implicit exception: dispatches to 0x000000010f02e7a5
                        0x000000010f02e694: mov    0x18(%rsi),%ebp    ;*getfield size
                                                                      ; - com.zaxxer.microbench.FastList2::add@1 (line 73)
                      
                        0x000000010f02e697: cmp    %r11d,%ebp
                        0x000000010f02e69a: jge    0x000000010f02e785  ;*if_icmpge
                                                                      ; - com.zaxxer.microbench.FastList2::add@9 (line 73)
                      
                        0x000000010f02e6a0: mov    %ebp,%r8d
                        0x000000010f02e6a3: inc    %r8d
                        0x000000010f02e6a6: mov    %r8d,0x18(%rsi)    ;*putfield size
                                                                      ; - com.zaxxer.microbench.FastList2::add@24 (line 74)
                      
                        0x000000010f02e6aa: cmp    %r11d,%ebp         ; Лишняя проверка
                        0x000000010f02e6ad: jae    0x000000010f02e76d
                        0x000000010f02e6b3: mov    0x8(%rdx),%r10d    ; implicit exception: dispatches to 0x000000010f02e7b5
                      


                    1. xhumanoid
                      20.10.2015 00:14
                      +1

                      в тему бенчмарков

                      java 8
                      Benchmark               (initCapacity)  (listImpl)   Mode  Cnt      Score     Error   Units
                      FastListBench.testList              32         new  thrpt   18  24669,079 ± 245,068  ops/ms
                      FastListBench.testList              32        orig  thrpt   18  24275,624 ± 225,616  ops/ms
                      
                      java 9
                      Benchmark               (initCapacity)  (listImpl)   Mode  Cnt      Score     Error   Units
                      FastListBench.testList              32         new  thrpt   18  12797,725 ± 373,708  ops/ms
                      FastListBench.testList              32        orig  thrpt   18  11771,681 ± 286,235  ops/ms
                      


                      неужто нас ожидает медленная java? =)

                      p.s. по асму там перед сохранением в массив пачка магии включая локи или это издержки дебаг версии?


                      1. apangin
                        22.10.2015 05:10
                        +3

                        Посмотрел, действительно, в JDK 9 сгенерированный код слегка распух. «Магия» — это G1 барьеры, коими сопровождается каждый апдейт ссылки в хипе. В JDK 9 G1 стал дефолтным коллектором, отсюда и разница. Стало быть, для честного сравнения надо запускать с -XX:+UseParallelGC.

                        Ещё одно подтверждение тому, что микробенчмарки зачастую измеряют совсем другое, чем хотелось, и без должного анализа смысла не имеют. Если кто-то говорит, «мой код быстрее в 10 раз, я проверил бенчмарком», и при этом даже не смотрит на логи компилятора, не говоря уж об ассемблере, значит, он безбожно врёт :)


                    1. lany
                      20.10.2015 15:21
                      +1

                      На самом деле код автора написан под конкретный юзкейс — складывать стейтменты внутри коннекшна. И тут же, кстати, у меня возникает ощущение, что всё это абсолютно зря. Давайте на обратные числа посмотрим, что оптимизирует сам автор:

                      ArrayList: 373 нс
                      Список с if на Java 7u60: 70 нс
                      Список исключением на Java 7u60: 25 нс

                      То есть при переходе с ArrayList на if он выигрывает 303 нс на SQL-запрос, а при переходе на исключения — ещё 45 нс. Мне совершенно не верится, что в контексте выполнения целого SQL-запроса и чтения результатов из него даже отказ от ArrayList дал заметный прирост. Не говоря уж про исключение. В относительных цифрах-то смотрится солидно, но в абсолютных как-то не очень.


                      1. xhumanoid
                        20.10.2015 15:32
                        +1

                        причем это не объясняет почему так произошло, только предположения о том, что exception работают быстрее.
                        на практике в случае с if в его тестах сразу отваливается inlining на add(), а уже потом еще и на remove(). как итог тест полностью бесполезен.

                        на свежих версиях jdk при срабатывании инлайнинга на обоих методах поведение идентичное, но автора это не останавливает =( начинаются споры по поводу использования в проде старых 6 и 7 версий


                        1. doom369
                          20.10.2015 15:39

                          del


                      1. doom369
                        20.10.2015 15:39

                        И тут же, кстати, у меня возникает ощущение, что всё это абсолютно зря.


                        При этом сами же используете подобные трюки.


                        1. lany
                          20.10.2015 15:57
                          +2

                          Вы не знаете, о чём говорите. В моём случае есть тесты, на которых такой трюк ускоряет производительность в 1000 раз, либо вообще делает рабочим код, который бы иначе упал с OutOfMemory. Скажем, задача вычитать ровно один элемент из сплитератора, который пришёл из неизвестного источника. Создайте такой сплитератор для примера:

                          Spliterator<Integer> spliterator = Stream.of(1)
                                         .flatMap(x -> IntStream.range(0, Integer.MAX_VALUE).boxed())
                                         .spliterator();

                          Запустите ужасный некрасивый код, похожий на то, что я пишу:

                          static class MyException extends RuntimeException {} 
                          
                          try {
                              spliterator.forEachRemaining(x -> {
                                  System.out.println(x);
                                  throw new MyException();
                              });
                          } catch(MyException ex) { /* ignore */ }

                          И сравните с официальным красивым, модным и рекомендуемым способом сделать то же самое:

                          spliterator.tryAdvance(System.out::println);

                          Никакой JMH вам не потребуется, разницу увидите невооружённым взглядом. Вся красота разбивается о суровую действительность.


                          1. doom369
                            20.10.2015 16:16

                            Вы не знаете, о чём говорите.


                            Да. Не знаю. Но я и не делаю отсюда заключений, вроде:

                            И тут же, кстати, у меня возникает ощущение, что всё это абсолютно зря.


                            1. lany
                              20.10.2015 16:22

                              Вы пытаетесь меня убедить, что если вы не разбираетесь в X и не делаете никаких заключений, значит, я не разбираюсь в Y и тоже не должен делать заключений? :-)


                              1. doom369
                                20.10.2015 16:24

                                =) нет конечно. Вы в праве делать все что хотите.


                1. doom369
                  19.10.2015 18:36

                  А Вы можете в тикет ответить на гитхабе? Мне как-то не хочеться постоянно проксировать ответы =).


                  1. apangin
                    20.10.2015 00:06

                    А и не надо ничего проксировать. Никому от этого лучше не станет.


                    1. doom369
                      20.10.2015 00:10

                      Ну почему же, вдруг Вы укажете на некую лже-оптимизацию и это ускорит хикари. Все пользователи от этого выиграют.


        1. doom369
          18.10.2015 17:47

          github.com/brettwooldridge/HikariCP/wiki«My-benchmark-doesn't-show-a-difference.» вот еще про то что они тестят.


        1. evnuh
          18.10.2015 18:02

          Вас спросили что не так с 35 инструкциями, конкретно, и почему это — ерунда?


          1. Quetzal
            18.10.2015 18:11

            Потому что это вовсе не лимит инлайнинга. Это значение регулируется параметром JVM (-XX:MaxInlineSize=N) и по-умолчанию оно 35 байт.


            1. doom369
              18.10.2015 18:23

              это вовсе не лимит инлайнинга


              А что же?


          1. apangin
            18.10.2015 18:26
            +3

            Потому что лимит задаётся совсем другими параметрами JVM. HotSpot JVM может инлайнить методы гораздо длиннее 35 байткодов. Смотрите FreqInlineSize.

            Если всё ещё не убедительно, проверьте сами.
            Заодно и развеем миф invokestatic vs. invokevirtual:

            Benchmark                     Mode  Cnt    Score   Error   Units
            Inlining.inlineStaticSmall   thrpt    5  298,373 ± 5,752  ops/us
            Inlining.inlineVirtualLarge  thrpt    5  298,515 ± 9,902  ops/us
            

            Или запустите с -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining:

                @ 16   bench.Inlining::inlineStaticSmall (4 bytes)
                  @ 0   bench.Inlining::smallMethod (25 bytes)   inline (hot)
            
                @ 16   bench.Inlining::inlineVirtualLarge (5 bytes)
                  @ 1   bench.Inlining::largeMethod (109 bytes)   inline (hot)
            


            1. doom369
              18.10.2015 18:40

              лимит задаётся совсем другими параметрами JVM

              Так в статье и не про это. Просто мимоходом упоминается, что есть вот такая штука длина метода в байткодах для инлайнинга, дефолтная она 35 байткодов. Может Вас смутило слово «лимит»? Окей извиняюсь за плохой перевод.

              Заодно и развеем миф invokestatic vs. invokevirtual:

              А в статье не говорится что вызов invokestatic быстрее invokevirtual. Там говорится, что у JIT большей вохможностей по оптимизации в случае invokestatic.


              1. apangin
                18.10.2015 20:53
                +2

                длина метода в байткодах для инлайнинга, дефолтная она 35 байткодов
                Это называется «слышал звон, да не знает, где он». Есть параметр JVM MaxInlineSize=35, но он означает совсем не то, что думает автор. Это лишь некая эвристика для инлайнинга, и она не мешает заинлайниться и более длинному методу, если JIT сочтёт нужным. Попытки «помочь» JIT-компилятору могут привести ровно к обратному эффекту. Метод checkException не листовой. Принудительно заинлайнив его, автор, например, рискует лишиться инлайнинга вложенных методов, тем самым получив вместо одного вызова два или три.

                Там говорится, что у JIT больше возможностей по оптимизации в случае invokestatic.
                А это тоже неправда. Вот, навскидку, ровно противоположный пример.

                А уж чего стоит аргумент про уменьшение стека с 5 до 4 элементов! С таким же успехом можно сказать, что программа будет работать быстрее и занимать меньше памяти, если названия всех переменных сократить до 1-2 букв :)


                1. doom369
                  18.10.2015 21:28

                  Ок, спасибо за развернутый ответ. Думаю автор просто не усложнял в контексте той статьи.


                  1. lany
                    20.10.2015 13:55
                    +1

                    Дело не в том, что автор «не усложнял», а в том, что он просто неправ. По факту MaxInlineSize не играет никакой роли для горячего кода. Утверждение «Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций» — абсолютная ложь, а не просто «упрощение». И попытки вогнать метод в 34 байта байткода бессмысленны. В контексте статьи автор как раз усложнил себе жизнь, делая бессмысленные операции. Если оптимизировать инлайнинг, надо не гадать, а хотя бы посмотреть, что реально инлайнится, а что — нет. Для этого есть опция +PrintInlining. Про неё же ни слова в статье.


                    1. doom369
                      20.10.2015 14:41

                      Про неё же ни слова в статье.


                      Потому что статья не про инлайнинг. А про то, что делал автор, чтобы создать быстрое решение. Нету смысла описывать детали работы инлайнинга, если цель просто описать подходы, что применялись. Иначе надо было бы писать десятки постов.


                      1. lany
                        20.10.2015 14:55
                        +1

                        Не понял. Что значит, статья не про инлайнинг? Я вижу три экрана про инлайнинг, начиная с заголовка «Down the Rabbit Hole». Почти добрая половина статьи. Окей, я тогда скажу, что подходы неверные. Или вы ответите, что статья не про подходы? :-)


                        1. doom369
                          20.10.2015 15:08

                          Почти добрая половина статьи


                          Просто кода много =).

                          Окей, я тогда скажу, что подходы неверные.


                          Ну как бы популярность решения и куча счастливых пользователей опровергают ваше утверждение. Если вы считаете, что данные подходы неверны, то было бы не плохо это увидеть на конкретных примерах.


                          1. lany
                            20.10.2015 15:13

                            Вот тут вы мух с котлетами путаете. Если человек написал код, пусть даже популярный и быстрый (не факт, но допустим), это не значит, что его объяснение, почему код быстрый, верно. Представьте, миллиардера спросили, как ему удалось стать миллиардером, а он ответил, что молится летающему макаронному монстру, и монстр ему помог стать миллиардером. Да, налицо, перед нами миллиардер. Но это не означает, что его слова имеют отношение к истине. Даже если он сам в них верит.

                            Конкретные примеры вам Андрей Паньгин разъяснил, тут вроде бы нечего добавить.


                            1. doom369
                              20.10.2015 15:29

                              Вот тут вы мух с котлетами путаете.


                              Это делаете Вы приводя ложную аналогию. Представьте, миллиардера спросили, как ему удалось стать миллардером, он ответил, что долго и упорно работал и это помогло ему стать миллиардером. Вы же утверждаете, что этот труд не помог есть стать миллардером, а причина в молитве макаронному монстру.

                              Конкретные примеры вам Андрей Паньгин разъяснил, тут вроде бы нечего добавить.

                              Андрей привел пример FastList.add() утверждая, что такого рода оптимизация — это скорее деоптимизация. Пока из их раговора я не понял, кто же прав.


                              1. xhumanoid
                                20.10.2015 15:48
                                +2

                                Вы же утверждаете, что этот труд не помог есть стать миллардером, а причина в молитве макаронному монстру

                                делать выводы на основе байткода совершенно неверно, то что в некоторых случаях это сработало, закрепляет неправильные предложения и в итоге уводят в совершенно непонятных направлениях с постепенным превращением всего этого в cargo культ. посмотрите на статью автора с его манипуляциями байткодом с пачкой необоснованных утверждение и статьи Шипилева или Томпсона, разница гигантская, так как у них идет разбор на уровне asm и иногда железа, что же у нас происходит на самом деле.

                                Пока из их раговора я не понял, кто же прав.

                                прав некорректный бенчмарк:
                                1) автор сравнивает не просто add(), но и тут же remove(), что вносит дополнительную погрешность
                                2) автор утверждает, что методы add() имеют минимальные отличия, на практике в случае if в его тесте метод add() не инлайнится, а идет явный вызов
                                3) автор утверждает, что методы remove() полностью идентичны, на практике в случае if в его тесте метод remove() не инлайнится (предположительно предыдущий add() сломал оптимизации последующие), а идет явный вызов

                                по итогу:
                                1) метод с exception идет линейно, без дополнительных вызовов, к тому же нету случая когда этот exception выбрасывается
                                2) метод с if сразу производит 15 вызовов функции add() и следом 15 вызовов remove().

                                что мы вообще меряем? стоимость 30 вызовов функций? логично что вызывать их будет дороже, так как переходы не бесплатны и заметно дороже линейного выполнения кода


                          1. xhumanoid
                            20.10.2015 15:19
                            +1

                            первый же раздел Down the Rabbit Hole

                            Мы изучали скомпилированный байткод для каждого метода и даже изменяли методы так, чтобы они попадали под лимит инлайнинга

                            Иногда, видя что метод превышает лимит инлайнинга, мы думали о том как изменить его таким образом, чтобы избавится от нескольких байт-инструкций. Например:

                            Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций. Поэтому мы уделили некоторое внимание этому методу, чтобы сократить его и изменили его следующим образом:

                            Получилось довольно близко к лимиту, но все еще 36 инструкций. Поэтому мы сделали так:

                            Выглядит проще. Неправда ли? На самом деле, этот код хуже предыдущего — 45 инструкций.

                            Как раз ниже лимита в 35 байткод инструкций. Это маленький метод и на самом деле даже не высоконагруженный, но идею Вы поняли.


                            извиняюсь за то, что пришлось перепостить половину статьи, но первый раздел как раз по поводу инлайнинга и гадания на байткоде.

                            Общения в github, где автор на основе идентичности методов утверждает, что в его тесте они тоже работают одинаково радует. так же как и желания тестировать только на старых версиях jvm, которые работают не всегда корректно.


                    1. xhumanoid
                      20.10.2015 15:03

                      >> Для этого есть опция +PrintInlining

                      данная опция на версиях jvm, что использует автор крешит jvm на его бенчмарках. А так да, основная масса выводов автором сделана на основе анализа байткода, без самого анализа что же у нас происходит в реале


      1. apangin
        18.10.2015 17:47
        +4

        Зато я нашёл в исходниках ещё одну замечательную «оптимизацию» :)

              try {
                 elementData[size++] = element;
              }
              catch (ArrayIndexOutOfBoundsException e) {
                 // overflow-conscious code
                 final int oldCapacity = elementData.length;
                 final int newCapacity = oldCapacity << 1;
                 @SuppressWarnings("unchecked")
                 final T[] newElementData = (T[]) Array.newInstance(clazz, newCapacity);
                 System.arraycopy(elementData, 0, newElementData, 0, oldCapacity);
                 newElementData[size - 1] = element;
                 elementData = newElementData;
              }
        

        Почему это не оптимизация, а совсем наоборот, я рассказывал на JPoint в презентации про устройство виртуальной машины HotSpot.


        1. doom369
          18.10.2015 18:13

          С одной стороны Вы правы. С другой — вполне допускаю, что за время жизни пула исключение бросится несколько раз. В то время как add/remove будут происходить постоянно. И в долгосрочной перспективе, такая оптимизая выгодней «If».


          1. apangin
            18.10.2015 20:10

            Так а в чём здесь оптимизация, поясните?


            1. doom369
              18.10.2015 21:24

              Ну я так понял, что цель всей той колбасы просто избежать условия
              if (size < elements.length)

              Вообще я задал вопрос автору. Так что подождем. Я так не оптимизирую, если что =).


              1. apangin
                18.10.2015 22:29

                Избежать условия не получится. В принципе. По ссылке выше рассказано, почему.


                1. doom369
                  18.10.2015 22:33

                  Из ссылки выше:

                  if (index >= arr.length) {
                  break;
                  }


                  Речь об этом условии. Я так понимаю что автор Хикари избегает его. Так как выбрасывание эксепшена за жизнь пула происходит раз-два и все.


                  1. apangin
                    18.10.2015 22:38

                    А как, по-вашему, JVM понимает, что пора выкинуть exception, не проверяя условия?


                    1. doom369
                      18.10.2015 22:50

                      Я не говорил, что не JVM проверяет диапозоны массива.


          1. GrigoryPerepechko
            18.10.2015 20:44

            try сам по себе дорогой. Я почти уверен что намного дороже if.
            Вы ж не думаете что исключения дорогие только когда кидаются. Нужна ведь инфраструктура которая их ловит.


            1. johndow
              18.10.2015 20:55
              +2

            1. doom369
              18.10.2015 21:26

              Стоиомсть try не важна, если пул живет месяцами и за этот месяц try вызывается пару раз для увеличения списка. В то время как add/remove выполняются каждую секунду сотни раз в случае нагруженности пула. Но это лишь мое предположение.


        1. doom369
          18.10.2015 18:22

          Завел тикет с вопросом автору — github.com/brettwooldridge/HikariCP/issues/462 =). Посмотрим что он ответит.


        1. Moxa
          18.10.2015 23:19

          я себе написал StringBuilder с аналогичной оптимизацией, если создавать новый билдер каждый раз, то он на порядок медленнее дефолтного, но если переиспользовать сущействующий, чтобы массив расширился до некоторого максимально значения только один раз, то такая оптимизация дает выигрыш примерно в 30%


          1. doom369
            18.10.2015 23:41
            +1

            JVM и так отлично оптимизирует StringBuilder.

            String s = «aaa...»; //100500 chars
            new StringBuilder().append(«1»).append(s).toString();


            компилятор заоптимайзит в new char[1+s.length]. Опция — OptimizeStringConcat. Так что прирост очень сомнителен.


            1. Moxa
              18.10.2015 23:47

              в таком простом варианте, наверно, да, заоптимизирует… но в моем случае — есть бенчмарки, я вижу какой вариант быстрее


              1. doom369
                18.10.2015 23:51

                Давайте бенчмарки. А вдруг там ошибка =)?




        1. doom369
          19.10.2015 10:03

          I benchmarked both ways when the code was written. This is faster 95% of the time. It only incurs overhead when the list is expanded — but that is a slow path anyway because of the memory copy.


          github.com/brettwooldridge/HikariCP/issues/462


        1. lany
          20.10.2015 12:43

          Как я понял, это отсюда код. У меня в другом месте facepalm ещё случился: ну зачем же ArrayList-то расширять? Мало граблей на этом собрали? Если в девятке вдруг во все листы добавят новый метод (скажем, parallelSort), и у ArrayList будет оптимизированная реализация, в данном классе он будет молча ничего не делать. Можно будет хвастаться, что FastList нереально быстро сортирует в параллель :-) Неужели мало граблей на этом в восьмёрке собрали? Что плохого в том, чтобы AbstractList наследовать?


        1. lany
          20.10.2015 13:05

          Я, кстати, нагрешил тут недавно в своей либе — использую кастомное исключение для control flow. В этом плане очень помог конструктор Error(null, null, false, false), который в несколько раз ускоряет создание исключения. Наблюдаю константную потерю на выкидывании где-то 200-300 наносекунд (против примерно 1500±длина стектрейса при обычном исключении). В моём случае эта константная потеря окупается, потому что альтернативный вариант без исключения затормаживает даже на не очень больших объёмах данных (только на совсем маленьких входных данных наблюдается проигрыш при использовании исключения).


          1. doom369
            20.10.2015 13:11

            потому что альтернативный вариант без исключения затормаживает


            Речь о
            if (condition) return error_core;

            ?

            И что за библиотека такая, где надо волноваться о таких мелочах?


            1. lany
              20.10.2015 13:19

              Нет, речь о tryAdvance/forEachRemaining. Это отнюдь не мелочи.


  1. Alexins
    19.10.2015 00:41

    Не в упрек автору данной темы.
    Не поленился, сходил на github проекта. Посмотрел код. Я в шоке.
    Тут рассказывают о том, как удалось сократить количество JVM инструкций с 35 до 34, забывая про генерацию кода с помощью javassist.
    Может кто-то объяснит доходчиво, как динамическое создание класса, с помощью javassist ускоряет работу 34 инструкций перед 35 инструкциями.


    1. doom369
      19.10.2015 00:54
      +1

      как динамическое создание класса, с помощью javassist ускоряет работу 34 инструкций перед 35 инструкциями.


      А как эти 2 вещи вообще связаны =)?


      1. Alexins
        23.10.2015 10:43

        Я не сильно разбираюсь в оптимизации JIT. Разве генерация дополнительного кода не вносит затраты на его исполнение?
        Уточню вопрос. Дело в том, что система генерирует байт код для каждого нового DataSource, используя старый код только в виде шаблона.

        У меня вопрос. Этот самый пул соединений, он стоит поверх JDBC пула.
        Какова производительность этого пула при достижении максимума соединений или запросов в базовом пуле?
        Имеется в виду, что остановка некоторых потоков в режиме ожидания новых ресурсов, не влияет на производительность других потоков.
        Ведь там используется не честная блокировка (первый запросил, первый получил).

        Еще один вопрос можно? Как данный пул можно использовать, скажем в WildFly с исходным XADataSource?


  1. Vanger13
    19.10.2015 00:41
    +5

    Все это оч мило и приятно конечно — простым прописыванием library depndency получить «самый быстрый connection pool». Но, если кто-то по производительности уперся в переиспользование(!) коннектов в пуле(!), то я уже искренне бы поздравил этих людей. Да и скорее всего у них наверняка уже написан свой аналог Hikari.


  1. tmk826
    19.10.2015 11:21
    +1

    Используем HikariCP уже два года в нашем проекте. Прирост в производителъности ноль. Единственая разница с предыдущим пулом (c3p0) в том, что HikariCP активно развивается.


  1. guai
    19.10.2015 16:29

    ковырял не так давно код нескольких пулов. Хикари написан наиболее понятно. Хотя бы за это его автору респект.