Как я могу кодировать Java, чтобы разрешить использование SSE и исключение проверки границ (или другие расширенные оптимизации)?

Ситуация:

Я оптимизирую реализацию алгоритма сжатия LZF на чистом java, который включает в себя большой объем доступа byte [] и базовую математику int для хеширования и сравнения. Производительность действительно имеет значение, потому что цель сжатия - снизить требования к вводу-выводу. Я не публикую код, потому что он еще не очищен и может быть сильно реструктурирован.

Вопросы:

  • Как я могу написать свой код, чтобы он мог JIT-компилироваться в форму с использованием более быстрых операций SSE?
  • Как я могу структурировать его, чтобы компилятор мог легко исключить проверки границ массива?
  • Есть ли какие-либо общие сведения об относительной скорости определенных математических операций (сколько приращений / уменьшений требуется, чтобы равняться нормальному сложению / вычитанию, насколько быстро происходит сдвиг или доступ к массиву)?
  • Как я могу работать над оптимизацией ветвления - что лучше: иметь множество условных операторов с короткими телами, несколько длинных или коротких с вложенными условиями?
  • В текущей версии 1.6 JVM, сколько элементов необходимо скопировать, прежде чем System.arraycopy преодолеет цикл копирования?

Что я уже сделал:

Прежде чем меня атакуют за преждевременную оптимизацию: базовый алгоритм уже превосходен, но реализация Java менее чем на 2/3 скорости эквивалентного C.Я уже заменил копирование циклов на System.arraycopy, работал над оптимизацией циклов и устранил un -необходимые операции.

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

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

Требования к ХОРОШЕМУ (принятому) ответу:

  • Неприемлемые ответы: это быстрее без объяснения того, сколько И почему, ИЛИ не было протестировано с помощью JIT-компилятора.
  • Пограничные ответы: не тестировались ни с чем до Hotspot 1.4.
  • Основные ответы: общее правило и объяснение того, почему он быстрее на уровне компилятора и примерно насколько быстрее.
  • Хорошие ответы: включите несколько примеров кода для демонстрации
  • Отличные ответы: есть тесты с JRE 1.5 и 1.6.
  • ИДЕАЛЬНЫЙ ответ: это кто-то, кто работал над компилятором HotSpot и может полностью объяснить или сослаться на условия оптимизации, которая будет использоваться, и насколько она обычно быстрее. Может включать Java-код и образец кода сборки, созданный HotSpot.

Также: если у кого-то есть ссылки, подробно рассказывающие об оптимизации Hotspot и производительности ветвления, они приветствуются. Я достаточно знаю о байт-коде, чтобы сайт, анализирующий производительность на уровне байт-кода, а не исходного кода, был бы полезен.

(Править) Частичный ответ: Исключение проверки границ:

Это взято из предоставленной ссылки на внутреннюю вики-страницу HotSpot по адресу: https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination

HotSpot устранит проверку границ во всех циклах for при следующих условиях:

  • Массив инвариантен к циклу (не перераспределяется внутри цикла)
  • Переменная индекса имеет постоянный шаг (увеличивается / уменьшается на постоянную величину, только в одном месте, если возможно)
  • Массив индексируется линейной функцией переменной.

Пример: int val = array[index*2 + 5]

ИЛИ: int val = array[index+9]

НЕ: int val = array[Math.min(var,index)+7]


Ранняя версия кода:

Это пробная версия. Не крадите его, потому что это невыпущенная версия кода для проекта базы данных H2. Окончательная версия будет с открытым исходным кодом. Это оптимизация кода здесь: H2 CompressLZF code

Логически это идентично версии для разработки, но в ней используется цикл for (...) для пошагового выполнения ввода и цикл if / else для разной логики между режимами литерала и обратной ссылки. Это уменьшает доступ к массиву и проверки между режимами.

public int compressNewer(final byte[] in, final int inLen, final byte[] out, int outPos){
        int inPos = 0;
        // initialize the hash table
        if (cachedHashTable == null) {
            cachedHashTable = new int[HASH_SIZE];
        } else {
            System.arraycopy(EMPTY, 0, cachedHashTable, 0, HASH_SIZE);
        }
        int[] hashTab = cachedHashTable;
        // number of literals in current run
        int literals = 0;
        int future = first(in, inPos);
        final int endPos = inLen-4;

        // Loop through data until all of it has been compressed
        while (inPos < endPos) {
                future = (future << 8) | in[inPos+2] & 255;
//                hash = next(hash,in,inPos);
                int off = hash(future);
                // ref = possible index of matching group in data
                int ref = hashTab[off];
                hashTab[off] = inPos;
                off = inPos - ref - 1; //dropped for speed

                // has match if bytes at ref match bytes in future, etc
                // note: using ref++ rather than ref+1, ref+2, etc is about 15% faster
                boolean hasMatch = (ref > 0 && off <= MAX_OFF && (in[ref++] == (byte) (future >> 16) && in[ref++] == (byte)(future >> 8) && in[ref] == (byte)future));

                ref -=2; // ...EVEN when I have to recover it
                // write out literals, if max literals reached, OR has a match
                if ((hasMatch && literals != 0) || (literals == MAX_LITERAL)) {
                    out[outPos++] = (byte) (literals - 1);
                    System.arraycopy(in, inPos - literals, out, outPos, literals);
                    outPos += literals;
                    literals = 0;
                }

                //literal copying split because this improved performance by 5%

                if (hasMatch) { // grow match as much as possible
                    int maxLen = inLen - inPos - 2;
                    maxLen = maxLen > MAX_REF ? MAX_REF : maxLen;
                    int len = 3;
                    // grow match length as possible...
                    while (len < maxLen && in[ref + len] == in[inPos + len]) {
                        len++;
                    }
                    len -= 2;

                    // short matches write length to first byte, longer write to 2nd too
                    if (len < 7) {
                        out[outPos++] = (byte) ((off >> 8) + (len << 5));
                    } else {
                        out[outPos++] = (byte) ((off >> 8) + (7 << 5));
                        out[outPos++] = (byte) (len - 7);
                    }
                    out[outPos++] = (byte) off;
                    inPos += len;

                    //OPTIMIZATION: don't store hashtable entry for last byte of match and next byte
                    // rebuild neighborhood for hashing, but don't store location for this 3-byte group
                    // improves compress performance by ~10% or more, sacrificing ~2% compression...
                    future = ((in[inPos+1] & 255) << 16) | ((in[inPos + 2] & 255) << 8) | (in[inPos + 3] & 255);
                    inPos += 2;
                } else { //grow literals
                    literals++;
                    inPos++;
                } 
        }
        
        // write out remaining literals
        literals += inLen-inPos;
        inPos = inLen-literals;
        if(literals >= MAX_LITERAL){
            out[outPos++] = (byte)(MAX_LITERAL-1);
            System.arraycopy(in, inPos, out, outPos, MAX_LITERAL);
            outPos += MAX_LITERAL;
            inPos += MAX_LITERAL;
            literals -= MAX_LITERAL;
        }
        if (literals != 0) {
            out[outPos++] = (byte) (literals - 1);
            System.arraycopy(in, inPos, out, outPos, literals);
            outPos += literals;
        }
        return outPos; 
    }

Окончательное редактирование:

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


person BobMcGee    schedule 29.08.2009    source источник
comment
некоторые примечания. вы можете захотеть создать cachedHashTable при построении (класса, если он статический). Если это для каждого потока, то это должно быть ясно указано в api (IE пользователь несет ответственность за создание определенного экземпляра компрессора и который содержит кэшированная таблица. Прогнозирование ветвления будет правильным, но вы можете получить некоторую выгоду от этого, более быстрого перехода к более поздним поколениям, а также упрощения кода.   -  person ShuggyCoUk    schedule 15.09.2009
comment
неприятный хакер - но учитывая, что вы все равно делаете с ним что-то неприятное. вам не нужно уменьшать ref на 2, если он не используется на самом деле, и он используется только в одном месте после (внутри оператора if), поэтому просто попробуйте: while (len ‹maxLen && in [ref + len - 2] и удалить реф - = 2   -  person ShuggyCoUk    schedule 15.09.2009
comment
Фу, да, это довольно мерзко. У меня есть еще пара неприятных приемов, которые я тоже могу сделать. Если вы считаете, что код здесь плохой, вы должны увидеть оригинал!   -  person BobMcGee    schedule 15.09.2009
comment
У меня есть версия, которая предварительно инициализирует хеш-таблицу и использует проверку диапазона для возможной обратной ссылки. Но с короткими буферами ввода / вывода (обычно 8192 КБ при сжатии потоков) есть преимущество в возможности быстрого устранения возможных обратных ссылок, когда ссылка равна 0.   -  person BobMcGee    schedule 15.09.2009
comment
+1 Я просматривал документы по нескольким бесплатным JVM и не нашел четкого упоминания о том, как и когда SIMD (SSE и т. Д.) Используются JVM / JIT. Только то, что они действительно используют их с версии 1.4.2. Разработчики компилятора / JVM определенно отвергают любую идею предоставления подсказок или оптимизации через структуру кода. В 6u2 было несколько патчей Snorcle JVM, ссылающихся на оптимизацию SIMD в ошибке 6536652; если вы перейдете по ссылкам на связанные ошибки, кажется, что JVM выполнит элементарное распараллеливание цикла, но подробностей о фактической реализации и ее использовании мало.   -  person charstar    schedule 11.11.2010


Ответы (4)


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

Знай своего врага

Вы нацеливаетесь на комбинацию JVM (по сути, JIT) и базовой подсистемы ЦП / памяти. Таким образом, фраза «Это быстрее на JVM X» вряд ли будет верной во всех случаях, когда вы переходите к более агрессивной оптимизации.

Если ваш целевой рынок / приложение будет в основном работать на определенной архитектуре, вам следует подумать об инвестировании в инструменты, специфичные для нее. * Если ваша производительность на x86 является критическим фактором, тогда VTune отлично подходит для углубляясь в jit output analysis, которое вы описываете. * Различия между 64-битными и 32-битными JIT могут быть значительными, особенно на платформах x86, где соглашения о вызовах могут изменяться и регистрироваться возможности очень разные.

Получите нужные инструменты

Вы, вероятно, захотите получить профилировщик выборки. Накладные расходы на инструментарий (и связанный с этим удар по таким вещам, как встраивание, загрязнение кеша и увеличение размера кода) для ваших конкретных потребностей были бы слишком большими. Анализатор Intel VTune на самом деле можно использовать для Java, хотя интеграция не такая тесная, как у других.
Если вы используете Sun JVM и довольны только тем, что делает последняя / самая лучшая версия, тогда варианты доступны для исследовать вывод JIT значительны, если вы немного разбираетесь в ассемблере. В этой статье подробно рассказывается о некоторых интересных анализах с использованием этой функции

Учитесь на других реализациях

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

Некоторая специфика

Поскольку LZF в эффективной неуправляемой реализации на современном настольном CPUS, в значительной степени ограничен пропускной способностью памяти (следовательно, он конкурирует со скоростью неоптимизированного memcpy), вам понадобится код, чтобы полностью оставаться в кеш-памяти уровня 1.
Как таковой любой статические поля, которые вы не можете преобразовать в константы, должны быть помещены в один и тот же класс, поскольку эти значения часто будут помещаться в одну и ту же область памяти, выделенную для vtables и метаданных, связанных с классами.

Следует избегать выделения объектов, которые не могут быть перехвачены с помощью Escape Analysis (только в версии 1.6 и выше).

c-код агрессивно использует развертывание цикла. Однако производительность этого на более старых (эпоха 1.4) виртуальных машинах составляет

Ваша цель движется и будет двигаться дальше. И снова предыдущий опыт Марка Леманна:

размер HLOG по умолчанию теперь 15 (кеш-память процессора увеличена)

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

6544668 Не выполняет векторных операций с массивами, которые невозможно выровнять во время выполнения. 6536652 Реализовать некоторые оптимизации сверхслова (SIMD) 6531696 не использовать немедленное сохранение 16-битных значений в памяти на процессоре Intel 6468290 Разделение и выделение вне eden для каждого процессора

Капитан очевидный

Мера, мера, мера. ЕСЛИ вы можете заставить свою библиотеку включать (в отдельной dll) простой и легкий в выполнении тестовый тест, который регистрирует соответствующую информацию (версия vm, процессор, ОС, параметры командной строки и т. Д.) И упрощает отправку вам обратно. увеличьте охват, лучше всего вы будете покрывать тех людей, которые заботятся о нем.

person ShuggyCoUk    schedule 11.09.2009
comment
Хороший ответ (пока лучший)! Знаете ли вы какие-либо сайты, которые предоставляют информацию о циклах, необходимых для определенных команд сборки? Или совет о том, как байт-код отображается на сборку x86? Я не очень разбираюсь в сборке. VTune сейчас немного выходит за рамки моего бюджета (хех), хотя я буду помнить об этом на будущее. Я работаю над версией с более предсказуемым доступом к массиву и меньшим количеством выборок массива. Распределение объектов нулевое, но ветви в коде более сложные. Вероятно ли, что постоянные добавления (например, var2 = var1 + 2) будут сильно оптимизированы? - person BobMcGee; 14.09.2009
comment
количество циклов на инструкцию больше не является значимым показателем, поскольку он сильно зависит от взаимодействия с другими инструкциями и зависимостями, доступности кеша и OOE вместе с глубиной конвейера. Отображение jit на способ более сложное, чем байтовый код X = ›Y x86. последовательный доступ к массивам - это хорошо, но для того, чтобы это значительно окупилось, может потребоваться ›линейная скорость ветвления. Любые константы времени компиляции сворачиваются компилятором. - person ShuggyCoUk; 14.09.2009
comment
Добавление переменной-константы может не иметь существенного значения, за исключением того, что компилятор может эффективно использовать это значение в регистре, если оно используется часто. Бесплатные профилировщики доступны для java. Вам стоит хотя бы взглянуть на некоторые из них, code.google.com/p/oktech-profiler является открытым исходным кодом, бесплатным и имеет режим выборки. - person ShuggyCoUk; 14.09.2009
comment
Я подумал, что сопоставление байт-кода может быть сложным - пытаюсь упростить здесь зависимости, где это возможно, но не уверен, насколько хорошо это будет работать. Полезно знать, что добавления не создают проблем - они просто постоянные смещения относительно изменяющихся переменных. Я выполнил профилирование, но дальше это не поможет - примерно 99% времени выполнения находится в цикле (ах) сжатия и System.arraycopy для копирования буквальных прогонов для вывода. Можете ли вы дать дополнительную информацию о том, как подойти к оптимизации для кеша и максимальной глубины конвейера, начиная с конца исходного кода java? - person BobMcGee; 14.09.2009
comment
Оптимизация размеров кэша - самая простая задача: взять все используемые буферы и настроить их размеры в соответствии с размером и ассоциативностью ваших тестовых машин и исследовать эффекты. (это более сложно, но это может дать вам начало). Использование конвейера, вероятно, будет полностью зависеть от jit, если вы не можете изменить алгоритм для выполнения своего рода векторизации, при которой вы берете что-то вроде A1B1C1A2B2C2. где C зависит от B, который зависит от A, и вместо A1A2B1B2C1C2 копирование массива звучит не очень хорошо, почему вам нужно работать в пустом пространстве, вы не можете работать в выходном буфере? - person ShuggyCoUk; 15.09.2009
comment
Итак, я ничего не могу сделать, кроме как оптимизировать буферы, чтобы стимулировать использование кеша? Единственные задействованные буферы - это буферы ввода и вывода. Байты обрабатываются последовательно, начиная с ввода, и используются либо для увеличения длины несжимаемого ряда литералов, либо для обратной ссылки на предыдущие байты (сжатие). Как только любой прогон должен закончиться, он записывается (с использованием копирования массива для буквальных прогонов) в выходной буфер. ---------------------------------- Я думаю, что это можно векторизовать, упаковав в int / long, но это сделать разветвление чрезвычайно сложным и добавить неиспользуемые обращения к массиву. - person BobMcGee; 15.09.2009
comment
Я опубликовал раннюю версию кода (не самую последнюю, которая использует цикл FOR для предсказуемого обхода входного буфера. Эта версия все еще находится в стадии отладки. - person BobMcGee; 15.09.2009
comment
Копирование массива для буквальных прогонов, вероятно, не то, что вы можете превзойти, если общая буквальная длина не мала (я думаю, по крайней мере, менее 16 байт, возможно, меньше). Поощрение правильного использования кеша заключается в том, чтобы закрывать доступ к данным, к которым вы обращаетесь. Вероятно, в вашем случае основная проблема заключается в том, как вы структурируете свои структуры «оглядки назад» на основе того, как они взаимосвязаны. - person ShuggyCoUk; 15.09.2009
comment
Да, arraycopy на самом деле довольно оптимален в современных JVM. В последних выпусках HotSpot используется вручную настроенная сборка, и они примерно в 2 раза быстрее копируют 32 элемента. Я не думаю, что смогу сильно оптимизировать ретроспективные структуры без добавления копирования в небольшой буфер с возможностью кеширования (дополнительная стоимость копирования) или что-то в этом роде. Посмотрю с реструктурированным циклом. - person BobMcGee; 15.09.2009

Что касается исключения проверки границ, я считаю, что новый JDK уже будет включать улучшенный алгоритм, который устраняет его, когда это возможно. Это две основные статьи по этой теме:

  • В. Михеев, С. Федосеев, В. Сухарев, Н. Липский. 2002 Эффективное улучшение управления версиями цикла в Java. Ссылка. Эта статья написана ребятами из Excelsior, которые реализовали эту технику в своей Jet JVM.
  • Вюртингер, Томас, Кристиан Виммер и Ханспетер Мессенбек. 2007. Устранение проверки границ массива для клиентского компилятора Java HotSpot. PPPJ. Ссылка. Слегка основанная на вышеупомянутом документе, это реализация, которая, как я полагаю, будет включена в следующий JDK. Также представлены достигнутые ускорения.

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

Надеюсь, поможет.

person João Silva    schedule 14.09.2009
comment
Это ЯВЛЯЕТСЯ очень интересным, и это еще одна причина, по которой JDK / JRE 1.7 будет хорош, если он когда-нибудь будет выпущен. Между этим API асинхронного ввода-вывода (на полпути между потоковым вводом-выводом и NIO) и новые API-интерфейсы параллелизма должны значительно приблизить производительность Java к оптимизированному C. Ответ). - person BobMcGee; 14.09.2009

Маловероятно, что вам понадобится сильно помогать JIT-компилятору с оптимизацией такого простого алгоритма обработки чисел, как LZW. ShuggyCoUk упомянул об этом, но я думаю, что это заслуживает особого внимания:

Дружественность к кеш-памяти вашего кода будет иметь большое значение.

Вы должны уменьшить размер вашего рабочего набора и максимально улучшить локальность доступа к данным. Вы упомянули «упаковку байтов в целые для повышения производительности». Это похоже на использование целых чисел для хранения байтовых значений, чтобы они были выровнены по словам. Не делай этого! Увеличенный размер набора данных перевесит любые выгоды (однажды я преобразовал некоторый код обработки чисел ECC из int [] в byte [] и получил двукратное ускорение).

Если вы этого не знаете: если вам нужно обрабатывать некоторые данные как байтами, так и целыми числами, вам не нужно сдвигать и маскировать их - используйте _ 1_ и связанные методы.

В текущей версии 1.6 JVM, сколько элементов необходимо скопировать, прежде чем System.arraycopy преодолеет цикл копирования?

Лучше сделайте тест самостоятельно. Когда я делал это еще в Java 1.3 раза, это было где-то около 2000 элементов.

person Michael Borgwardt    schedule 14.09.2009
comment
Это LZF, а не LZW ... поэтому скорость важна. Я отредактировал исходный пост, включив в него самую последнюю стабильную версию кода сжатия и ссылку на самую раннюю версию (менее оптимизированную). Упаковка байтов в целое используется для упреждающего просмотра, который используется в хеш-таблице позиций для 3-байтовых групп, и для проверки возможности использования обратной ссылки кандидата. - person BobMcGee; 15.09.2009
comment
Также: я абсолютно уверен, что сейчас цифра меньше 32 элементов, потому что она примерно в 2 раза быстрее при использовании копирования массива по сравнению с циклом с таким количеством элементов. Опять же, петля не была полностью оптимальной. - person BobMcGee; 15.09.2009
comment
Я считаю, что jit был изменен некоторое время назад, чтобы позволить копии массива быть в особом корпусе. Я не могу найти ссылку нигде, кроме ibm.com/developerworks /java/library/j-devrtj2.html, который неясно, когда он изменился. Конечно, это тоже для JVM - person ShuggyCoUk; 15.09.2009
comment
ах да, вот еще обсуждение этого: mail-archive.com/ [email protected]/msg10172.html - person ShuggyCoUk; 15.09.2009

Пока много ответов, но еще пара вещей:

  • Измеряйте, измеряйте, измеряйте. Несмотря на то, что большинство разработчиков Java предостерегает от микротестирования, обязательно сравнивайте производительность между изменениями. Оптимизации, которые не приводят к ощутимым улучшениям, как правило, не стоит сохранять (конечно, иногда это комбинация вещей, и это становится еще сложнее)
  • Плотные циклы имеют такое же значение для Java, как и для C (и то же самое с распределением переменных - хотя вы не контролируете их напрямую, HotSpot в конечном итоге должен будет это сделать). Мне удалось практически удвоить скорость декодирования UTF-8, переставив жесткий цикл для обработки однобайтовых регистров (7-битный ascii) как жесткий (er) внутренний цикл, исключив другие случаи.
  • Не недооценивайте стоимость выделения и / или очистки больших массивов - если вы хотите, чтобы кодирование / декодирование lzf было быстрее и для небольших / средних фрагментов (а не только размером в мегабайт), имейте в виду, что ВСЕ выделения байта [] / int [ ] несколько дорого обходятся; не из-за GC, а потому, что JVM ДОЛЖНА очищать пространство.

Реализация H2 также была немного оптимизирована (например: она больше не очищает хэш-массив, это часто имеет смысл); и я действительно помог изменить его для использования в другом проекте Java. Мой вклад был в основном просто изменен, он был более оптимальным для случая без потоковой передачи, но это не коснулось жестких циклов кодирования / декодирования.

person StaxMan    schedule 08.12.2009
comment
Я хорошо осведомлен о последних оптимизациях H2 и частично несу ответственность: некоторые из оптимизаций фактически основаны на черновой версии моего кода, которую я отправил примерно в то время, когда я опубликовал это. Это включает в себя повторное использование хэш-переменной при проверке возможной обратной ссылки, более чистой структуры цикла и без инициализации хеш-таблицы. Из любопытства, какие были модификации? В качестве тизера: ищите пару вариантов более высокого сжатия и более быструю декомпрессию в коде H2, когда что-то выходит из проверки кода. - person BobMcGee; 08.12.2009