All streams
Search
Write a publication
Pull to refresh
170
0
Андрей @apangin

Пользователь

Send message
Но ваш вариант мне тоже понравился!
Работает не только с интерфейсами. И со статическими методами тоже.

import sun.misc.SharedSecrets;
import sun.reflect.ConstantPool;

import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.function.Function;

public class MethodRefs {

    public static void main(String[] args) throws Exception {
        Function<Object, Integer> hashCodeRef = Object::hashCode;
        System.out.println(unreference(hashCodeRef));
    }

    public static Method unreference(Function<?, ?> ref) {
        ConstantPool pool = SharedSecrets.getJavaLangAccess().getConstantPool(ref.getClass());
        int size = pool.getSize();
        for (int i = 1; i < size; i++) {
            try {
                Member member = pool.getMethodAt(i);
                if (member instanceof Method) {
                    return (Method) member;
                }
            } catch (IllegalArgumentException e) {
                // skip non-method entry
            }
        }
        throw new IllegalArgumentException("Not a method reference");
    }
}
Страницами, то есть, по 4 КБ. Однако в случае, когда идёт последовательное обращение к виртуальной памяти, ОС может использовать упреждающее чтение, то есть, коммитить последующие страницы заранее.
Нет, дело не в оптимизациях. Выделение памяти работает по-другому.

Если попросить достаточно большой кусок памяти (согласно документации, более 128 KB), то new в C++ сводится к системному вызову mmap, который не выделяет память, а лишь резервирует виртуальное адресное пространство. Вся магия происходит при первом обращении к зарезервированной странице: случается page fault, в ответ на который ядро уже коммитит данную страницу, то есть, размещает её в физической памяти.

Стало быть, в тесте на C++ реальное выделение памяти неявно скрыто в обращении к массиву. А если «трогать» лишь первые 100 KB массива, как происходит во втором тесте, то и физически размещено в RAM будет лишь 100 KB. В Java же выделение «честное»: аллоцировали 1 MB — столько же сразу и будет закоммичено. При этом вся область будет ещё обязательно обнулена.
Спасибо за этот пример — добавлю его в копилочку классических ошибок бенчмаркинга :)
Я уже писал, что большинство заблуждений про производительность Java происходят из-за плохого кода и неумения анализировать. Давайте же вместе проанализируем.

Смотрите: ваш тест на C++ выделяет 100 тыс. раз по 4 мегабайта, так? (Размер int обычно 4 байта). Итого около 400 GB. И всё это за одну секунду. При этом пропускная способность самой современной топовой DDR4 памяти составляет всего 25 GB/s. Опа! Ваш тест в 16 раз опередил время!

Подключив здравый смысл, становится очевидно, что ваш тест замеряет что-то не то. (А что именно, кстати?) Более или менее корректный тест уже написал Владимир выше. И из него следуют ровно обратное. Действительно, выделение памяти — это как раз сильная сторона Java. Я на StackOverflow рассказывал, как работает выделение в Java — в большинстве случаев это всего 10-15 тактов CPU. Даже самым крутым сишным аллокаторам (tcmalloc, jemalloc) далеко в этом смысле до JVM.
Вот отличный канонический пример на тему «Java тормозит».

Чувак пишет бенчмарк, замеряющий скорость работы с memory-mapped файлом на Java и на C.
Запускает… Программа на Java работает в 500 раз медленнее! Какой вывод? Дрянь эта ваша Java!

Но надо отдать должное автору, он не стал сразу ругать Java, а обратился на StackOverflow с вопросом «почему?» Можно было пойти дальше и проанализировать самому, хотя бы с помощью профайлера. И тут становится понятно, что Java-то не виновата. Это программист дёргает на каждый байтик file.size(). А если переписать тест грамотно, то окажется, что разница в скорости вообще копеечная.
Фокус в том, что в реальных программах ничего прогревать не нужно. Это делается само собой, причём статистика собирается на полезной нагрузке. Приложения могут работать часами, днями, месяцами… А, вот, в микробенчмарках такое поведение воспроизвести сложно. Хочется померить маленький кусок кода и очень быстро. Отсюда и приёмчики с искусственным прогревом и т. п.

в реальной программе тестируемый метод будет вызываться, вероятно, относительно редко
Редко вызываемый код почти никогда не будет проблемой. Ну, отработает он за 10мс. Ну, за 100… Человек разницы даже не заметит. А если код работает долго — значит, там, есть циклы, есть повторения… Те самые тысячи и миллионы прогонов по одним и тем же инструкциям, которые JIT и призван оптимизировать.
Да, есть общие правила, применимые почти ко всем микробенчмаркам. Хотя даже если следовать им, не всегда удаётся измерить правильно. Поэтому любой бенчмарк без должного анализа результатов не имеет смысла.

Вот знаменитый вопрос на StackOverflow. Или ещё полезный пост с примерами.

По моим наблюдениям самые частые ошибки такие.

  • Один большой метод, в котором замеряют все алгоритмы. Это в корне неверно. Единица JIT компиляции — метод. Чем длиннее метод, тем сложнее его оптимизировать. Скорее всего, код будет адаптирован лишь под один из алгоритмов, по которому успела собраться run-time статистика. Разные алгоритмы надо запускать в отдельных JVM.
     
  • Весь бенчмарк — один длинный цикл в main(). Таким образом, вместо полноценно скомпилированного метода тестируют OSR заглушку. Один прогон бенчмарка следует поместить в отдельный метод, который запускается несколько раз.
     
  • Замеры надо проводить в устойчивом состоянии, когда все классы уже загружены, все компиляции-рекомпиляции уже прошли и т. д. Нет единого «золотого» числа итераций, после которого приложение заведомо будет работать с максимальной скоростью. Для одних тестов это пара секунд, для других — несколько минут.
     
  • «Прогревать» следует ровно тот код, который измеряется. Иначе Profile Pollution может всё испортить.
     
  • Бенчмарк должен иметь эффект, чтобы JIT не выкинул код, который что-то вычисляет, но нигде результаты вычислений не использует.
     
  • Самые честные замеры — на свежей 64-битной JDK в режиме Tiered или C2. Показатели 32-битной JVM и 64-битной, «клиентского» компилятора и «серверного» могут отличаться на порядок.
     
  • Убедитесь, что памяти достаточно, и что GC не оказывает влияния на производительность.
     
  • Минимизируйте сторонние эффекты. Избегайте лишних println-ов, nanoTime-ов и прочих операций, не имеющих отношения к измеряемому коду.
     
А у вас не осталось, случаем, этого теста с Java сокетами? Я почти уверен, что его можно улучшить.

Каких я только баек не слышал про тормозную Java! В большинстве своём они возникают как раз от неумения бенчмаркать и анализировать. У меня на StackOverflow целая подборка ответов про курьёзы Java Performance. Вот, например:


На самом же деле, с производительностью Java есть ровно две проблемы:

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

При ресолвинге одного из invoke* байткодов либо при вызове из натива метод будет помещаться в очередь на компиляцию. При этом, если background компиляция не отключена, метод спокойненько продолжит исполняться в интерпретаторе.
Немного не так. -XX:-UseInterpreter не отключает интерпретатор. Для форсированного выполнения скомпилированного кода служит флаг -Xcomp, который эквивалентен -XX:-UseInterpreter -XX:-BackgroundCompilation -XX:-ClipInlining -XX:Tier3InvokeNotifyFreqLog=0 -XX:Tier4InvocationThreshold=0

Это будет хорошим приближением работы в полностью компилируемом режиме. Но на самом деле, HotSpot не умеет работать вообще без интерпретатора.
Точно. Делается примерно так:
    ucontext->uc_mcontext.gregs[REG_RIP] = new_return_address;

На x86 — REG_EIP, на amd64 — REG_RIP.
Обработчик сигнала не обязан вернуться ровно на ту же инструкцию. В хендлер передаётся ucontext с указателем на сохранённые значения всех регистров. Достаточно заменить значение, соответствующее регистру IP, чтобы по выходу из обработчика сигнала управление передалось на следующую инструкцию или в любое другое место. Например, при разыменовании нулевого указателя хендлер передаёт управление коду, бросающему NullPointerException.
Спасибо! Давно хотелось увидеть анализ OpenJDK.
Признаться, ожидал, что всё будет хуже.

Две, действительно, серьёзные баги (про приоритет "? :" и про sizeof(buf) в printf) к моменту публикации уже были пофикшены, а остальное — просто хорошие замечания, хотя и не влияющие на работоспособность. Каст double* к float* и вовсе не бага, а типичный сценарий переиспользования буфера.

Много ли было ложных срабатываний? Подозреваю, что с таким стилем кодирования, как в HotSpot, и с кучей макросов их должно быть просто море.
А зачем их «решать»? Allocation Failure — это самая обычная, самая нормальная причина запуска GC.
Собственно, сборка мусора для того и придумана, чтобы освобождать память, когда её не получается выделить.
А, судя по графику, это сборка в ParNew. И там ещё несколько точек около 1 секунды. Для сборки в молодом поколении (размер которого 2GB) это тем более много.

Такое бывает по разным причинам: либо чрезмерное количество Weak/Soft-ссылок, либо множество ссылок из старого поколения в новое, либо сильная фрагментация старого поколения, либо ресайз хипа, если его размеры не фиксированы, либо дисковая активность, попавшая на период сборки… В общем, я к тому, что делать выводы об эффективности GC без должного анализа — преждевременно. Точно так же, как и по результатам бенчмарка судить о производительности приложения.
Я правильно понял из таблицы, что при хипе в 12GB максимальный размер паузы для CMS-5 составил 3 секунды? При том, что promotion failure и concurrent mode failure не было? Тогда это очень странно. У нас на сервере, обрабатывающем 15000 rps, с 54GB хипом максимальная пауза за 500 мс не выходит. Что логи говорят, на какую фазу больше всего времени уходит?
Кстати, из той же серии. Типичная ошибка при реализации всякого рода lookup-таблиц:

    int hash = Math.abs(obj.hashCode());
    return table[hash % table.length];

В чём тут подвох? :)
Никогда не говорите "никогда" :)
Даже среди стандартных классов немало примеров компараторов с вычитанием:

java.lang.Enum

        return self.ordinal - other.ordinal;

java.lang.String

        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;

java.time.Year

        return year - other.year;

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

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Works in
Registered
Activity