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

    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!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 74

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

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

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

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

        У автора есть решение с открытым кодом, которым пользуются очень много людей. Есть бенчмарки, подтверждающие его слова. Все есть на гитхабе. Буду не против увидеть реальное опровержение слов автора.
          +3
          Лучше приведите исходники этих бенчмарков (на гитхабе проекта не нашёл), и я скажу, где они врут.
          Вы перевели всё честно, здесь спору нет, но ерунда написана в оригинальной статье.
            0
            github.com/brettwooldridge/HikariCP-benchmark

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

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


                Ну на момент написания статьи Вашего кода даже не было в репозитории. Так что — нет. Не надувательство.
                  +5
                  А может, эта статья — постановочный заход к публикации Андреем one-datasource?!
                  0
                  Автор хикари ответил Вам. И утверждает что одноклассники в 8 раз медленее, а не быстрее. К сожалению у меня нету приглоса, так что вот ответ github.com/brettwooldridge/HikariCP/issues/464#issuecomment-149141231
                    +1
                    Как говорится, бывает ложь, большая ложь, и бенчмарки :) Всегда можно написать бенчмарк, который «докажет» что угодно, если не понимать, что именно он измеряет. А в случае с пулами, очевидно, запустив пустой getConnection() в 8 потоках, автор измерил стоимость contended блокировки. Очень полезно. Особенно, когда в продакшне даже при 5000 запросах в секунду у нас contention наблюдается в < 0.5% случаев. Хотя uncontended случай, на котором one-datasource оказался внезапно быстрее, куда ближе к реальности, на самом деле, и он абсолютно бесполезен, покуда в жизни приложение обычно занимается запросами к базе, а не синхронизацией на пуле.

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

                    Забавно, что автор Hikari указывает на наши якобы проблемы с транзакциями. При том, что его пул не поддерживает работу с TransactionManager в принципе! Т.е. вообще не работает! Если приглядеться, все озвученные «проблемы» — это неправильное использование DataSourceImpl. one-datasource используется только внутри наших проектов в контролируемом контексте. У нас нет планов делать из него open source продукт и, тем более, мериться с кем-либо. Я его выложил на GitHub, только чтобы показать несостоятельность бенчмарка.
                      0
                      Автор хикари так же провел бенчмарк if vs try-catch на Java 7 и Java 8 и получил совсем странные результаты. Не могли бы тоже прокомментировать?
                        +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, не для долгоживущих списков, а для типичного сценария «создал-поработал-забыл», где исключения начнут выскакивать часто, и тогда начнётся самое интересное.
                          0
                          А почему 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
                            в тему бенчмарков

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

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

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

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

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


                                При этом сами же используете подобные трюки.
                                  +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 вам не потребуется, разницу увидите невооружённым взглядом. Вся красота разбивается о суровую действительность.
                                    0
                                    Вы не знаете, о чём говорите.


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

                                    И тут же, кстати, у меня возникает ощущение, что всё это абсолютно зря.
                                      0
                                      Вы пытаетесь меня убедить, что если вы не разбираетесь в X и не делаете никаких заключений, значит, я не разбираюсь в Y и тоже не должен делать заключений? :-)
                                        0
                                        =) нет конечно. Вы в праве делать все что хотите.
                            0
                            А Вы можете в тикет ответить на гитхабе? Мне как-то не хочеться постоянно проксировать ответы =).
                              0
                              А и не надо ничего проксировать. Никому от этого лучше не станет.
                                0
                                Ну почему же, вдруг Вы укажете на некую лже-оптимизацию и это ускорит хикари. Все пользователи от этого выиграют.
                      0
                      github.com/brettwooldridge/HikariCP/wiki«My-benchmark-doesn't-show-a-difference.» вот еще про то что они тестят.
                        0
                        Вас спросили что не так с 35 инструкциями, конкретно, и почему это — ерунда?
                          0
                          Потому что это вовсе не лимит инлайнинга. Это значение регулируется параметром JVM (-XX:MaxInlineSize=N) и по-умолчанию оно 35 байт.
                            0
                            это вовсе не лимит инлайнинга


                            А что же?
                            +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)
                            
                              0
                              лимит задаётся совсем другими параметрами JVM

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

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

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

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

                                А уж чего стоит аргумент про уменьшение стека с 5 до 4 элементов! С таким же успехом можно сказать, что программа будет работать быстрее и занимать меньше памяти, если названия всех переменных сократить до 1-2 букв :)
                                  0
                                  Ок, спасибо за развернутый ответ. Думаю автор просто не усложнял в контексте той статьи.
                                    +1
                                    Дело не в том, что автор «не усложнял», а в том, что он просто неправ. По факту MaxInlineSize не играет никакой роли для горячего кода. Утверждение «Наверное ни для кого уже не секрет, что лимит инлайнинга в Hostpot JVM — 35 байткод инструкций» — абсолютная ложь, а не просто «упрощение». И попытки вогнать метод в 34 байта байткода бессмысленны. В контексте статьи автор как раз усложнил себе жизнь, делая бессмысленные операции. Если оптимизировать инлайнинг, надо не гадать, а хотя бы посмотреть, что реально инлайнится, а что — нет. Для этого есть опция +PrintInlining. Про неё же ни слова в статье.
                                      0
                                      Про неё же ни слова в статье.


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


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

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


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

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


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

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

                                              Андрей привел пример FastList.add() утверждая, что такого рода оптимизация — это скорее деоптимизация. Пока из их раговора я не понял, кто же прав.
                                                +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
                                              первый же раздел Down the Rabbit Hole

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

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

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

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

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

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


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

                                              Общения в github, где автор на основе идентичности методов утверждает, что в его тесте они тоже работают одинаково радует. так же как и желания тестировать только на старых версиях jvm, которые работают не всегда корректно.
                                          0
                                          >> Для этого есть опция +PrintInlining

                                          данная опция на версиях jvm, что использует автор крешит jvm на его бенчмарках. А так да, основная масса выводов автором сделана на основе анализа байткода, без самого анализа что же у нас происходит в реале
                              +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.
                                0
                                С одной стороны Вы правы. С другой — вполне допускаю, что за время жизни пула исключение бросится несколько раз. В то время как add/remove будут происходить постоянно. И в долгосрочной перспективе, такая оптимизая выгодней «If».
                                  0
                                  Так а в чём здесь оптимизация, поясните?
                                    0
                                    Ну я так понял, что цель всей той колбасы просто избежать условия
                                    if (size < elements.length)

                                    Вообще я задал вопрос автору. Так что подождем. Я так не оптимизирую, если что =).
                                      0
                                      Избежать условия не получится. В принципе. По ссылке выше рассказано, почему.
                                        0
                                        Из ссылки выше:

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


                                        Речь об этом условии. Я так понимаю что автор Хикари избегает его. Так как выбрасывание эксепшена за жизнь пула происходит раз-два и все.
                                          0
                                          А как, по-вашему, JVM понимает, что пора выкинуть exception, не проверяя условия?
                                            0
                                            Я не говорил, что не JVM проверяет диапозоны массива.
                                    0
                                    try сам по себе дорогой. Я почти уверен что намного дороже if.
                                    Вы ж не думаете что исключения дорогие только когда кидаются. Нужна ведь инфраструктура которая их ловит.
                                      +2
                                        0
                                        Стоиомсть try не важна, если пул живет месяцами и за этот месяц try вызывается пару раз для увеличения списка. В то время как add/remove выполняются каждую секунду сотни раз в случае нагруженности пула. Но это лишь мое предположение.
                                      0
                                      Завел тикет с вопросом автору — github.com/brettwooldridge/HikariCP/issues/462 =). Посмотрим что он ответит.
                                        0
                                        я себе написал StringBuilder с аналогичной оптимизацией, если создавать новый билдер каждый раз, то он на порядок медленнее дефолтного, но если переиспользовать сущействующий, чтобы массив расширился до некоторого максимально значения только один раз, то такая оптимизация дает выигрыш примерно в 30%
                                          +1
                                          JVM и так отлично оптимизирует StringBuilder.

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


                                          компилятор заоптимайзит в new char[1+s.length]. Опция — OptimizeStringConcat. Так что прирост очень сомнителен.
                                        0
                                        0
                                        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
                                          0
                                          Как я понял, это отсюда код. У меня в другом месте facepalm ещё случился: ну зачем же ArrayList-то расширять? Мало граблей на этом собрали? Если в девятке вдруг во все листы добавят новый метод (скажем, parallelSort), и у ArrayList будет оптимизированная реализация, в данном классе он будет молча ничего не делать. Можно будет хвастаться, что FastList нереально быстро сортирует в параллель :-) Неужели мало граблей на этом в восьмёрке собрали? Что плохого в том, чтобы AbstractList наследовать?
                                            0
                                            Я, кстати, нагрешил тут недавно в своей либе — использую кастомное исключение для control flow. В этом плане очень помог конструктор Error(null, null, false, false), который в несколько раз ускоряет создание исключения. Наблюдаю константную потерю на выкидывании где-то 200-300 наносекунд (против примерно 1500±длина стектрейса при обычном исключении). В моём случае эта константная потеря окупается, потому что альтернативный вариант без исключения затормаживает даже на не очень больших объёмах данных (только на совсем маленьких входных данных наблюдается проигрыш при использовании исключения).
                                              0
                                              потому что альтернативный вариант без исключения затормаживает


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

                                              ?

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


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

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

                                          Еще один вопрос можно? Как данный пул можно использовать, скажем в WildFly с исходным XADataSource?
                                        +5
                                        Все это оч мило и приятно конечно — простым прописыванием library depndency получить «самый быстрый connection pool». Но, если кто-то по производительности уперся в переиспользование(!) коннектов в пуле(!), то я уже искренне бы поздравил этих людей. Да и скорее всего у них наверняка уже написан свой аналог Hikari.
                                          +1
                                          Используем HikariCP уже два года в нашем проекте. Прирост в производителъности ноль. Единственая разница с предыдущим пулом (c3p0) в том, что HikariCP активно развивается.
                                          • UFO just landed and posted this here
                                              0
                                              docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html
                                              В Spring используется HikariCP как Connection Pool.
                                              Наверное, не просто так…

                                            Only users with full accounts can post comments. Log in, please.