Подтвердить что ты не робот

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

Ситуация:

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

Вопросы:

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

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

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

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

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

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

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

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

(Редактировать) Частичный ответ: Обозначения выравнивания:

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

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

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

Пример: 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 для различной логики между режимами literal и backreference. Это уменьшает доступ к массиву и проверяет между режимами.

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; 
    }

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Узнайте о других реализациях

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

Некоторые особенности

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

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

c code делает агрессивное использование разворачивания цикла. Однако производительность этого на более ранней (1.4 эры) VM сильно зависит от режима, в котором находится JVM. По-видимому, последние версии jvm sun более агрессивны при встраивании и разворачивании, особенно в режиме сервера.

Инкрементация предварительной выборки, сгенерированная JIT, может сделать всю разницу в коде, подобном этому, который близок к памяти.

"Это прямо для нас"

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

размер по умолчанию HLOG теперь равен 15 (cpu caches увеличились)

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Кэширование вашего кода будет большим фактором.

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

В противном случае вы не знаете этого: если вам нужно обрабатывать некоторые данные как байты, так и int, вам не нужно сдвигать и | -маскировать его - используйте ByteBuffer.asIntBuffer() и связанные с ним методы.

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

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

Ответ 4

Множество ответов до сих пор, но пара дополнительных вещей:

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

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