Задержка распределения кажется высокой, почему?

У меня есть (java) приложение, которое работает в среде с малой задержкой, обычно оно обрабатывает инструкции за ~ 600 микрон (+/- 100). Естественно, по мере того, как мы продвинулись дальше в микросекундное пространство, вы видите, что затраты на задержку меняются, и прямо сейчас мы заметили, что 2/3 этого времени тратится на выделение двух основных объектов домена.

Бенчмаркинг изолировал проблемные участки кода буквально для построения объектов из существующих ссылок, т.е. в основном множество ссылок (~15 в каждом классе) и пара обновленных списков, хотя см. примечание ниже о том, что именно измеряется. здесь.

Каждый из них постоянно занимает ~ 100 микрон, что для меня необъяснимо, и я пытаюсь выяснить, почему. Быстрый тест показывает, что объект аналогичного размера, полный строк, требует около 2-3 микросекунд для обновления, очевидно, что этот вид теста сопряжен с трудностями, но он может быть полезен в качестве базового уровня.

Здесь 2 вопроса

  • как расследовать такое поведение?
  • какие объяснения существуют для медленного распределения?

Обратите внимание, что в качестве аппаратного обеспечения используется Solaris 10 x86 на Sun X4600 с 8*двухъядерными оптеронами с частотой 3,2 ГГц.

Вещи, которые мы рассмотрели, включают

  • проверка статистики PrintTLAB показывает несколько медленных распределений, поэтому там не должно быть конфликтов.
  • PrintCompilation предполагает, что один из этих фрагментов кода не является JIT-дружественным, хотя Solaris, похоже, имеет здесь какое-то необычное поведение (а именно, по сравнению с современным Linux, у него нет Linux, аналогичного винтажу Solaris10, для работы на стенде прямо сейчас)
  • LogCompilation... немного сложнее разобрать, если не сказать больше, так что это постоянная работа, пока ничего очевидного
  • Версии JVM... согласуются между 6u6 и 6u14, еще не пробовали 6u18 или последнюю 7

Любые и все мысли приветствуются

Сводка комментариев к разным постам, чтобы попытаться внести ясность

  • стоимость, которую я измеряю, — это общая стоимость создания объекта, построенного с помощью Builder (например, одного из эти), частный конструктор которого несколько раз вызывает новый ArrayList, а также устанавливает ссылки на существующие объекты. Измеренная стоимость включает стоимость настройки построителя и преобразования построителя в объект домена.
  • компиляция (по горячей точке) оказывает заметное влияние, но все еще относительно медленная (компиляция в этом случае сокращает время со 100 до ~ 60)
  • компиляция (по горячей точке) в моем наивном тесте сокращает время выделения с ~ 2 микросекунд до ~ 300 нс.
  • задержка не зависит от алгоритма сбора молодого поколения (ParNew или Parallel Scavenge)

person Matt    schedule 17.11.2009    source источник
comment
Ваш вопрос очень многословен, но правильно ли я понимаю, что вам интересно, почему для создания списка требуется 150 мкс? Если да, то какая реализация списка? А что такое загрузка ссылок?   -  person jarnbjo    schedule 18.11.2009
comment
Не могли бы вы опубликовать (часть) оскорбительного кода?   -  person rsp    schedule 18.11.2009
comment
›Бенчмаркинг изолировал проблемные участки кода буквально до конструкции объектов из существующих ссылок, т. е., по сути, множества ссылок. Можете ли вы предоставить код?   -  person vickirk    schedule 18.11.2009
comment
Я имею в виду, что у меня есть класс, который имеет ряд атрибутов (несколько строк, пару перечислений, несколько длинных, некоторые другие объекты предметной области), которые уже были выделены/полностью построены, и весь конструктор (на самом деле построитель, который вызывает частный ctor) устанавливает члены класса так, чтобы они указывали на эти ссылки, а также создает пару пустых списков массивов. Работы по строительству данного объекта не ведутся.   -  person Matt    schedule 18.11.2009
comment
не уверен, как осмысленно опубликовать код, тбх   -  person Matt    schedule 18.11.2009
comment
Пробовали ли вы сравнивать построение вашего доменного объекта с существующими ссылками ArrayList вместо выделения новых?   -  person Tuure Laurinolli    schedule 18.11.2009
comment
Для сравнения, создание экземпляра ArrayList занимает около 30 нс в моей системе, что составляет порядка 4000 от результатов, на которые вы ссылаетесь. Если вы не предоставите более подробную информацию о своей настройке (возможно, вы используете какой-то объектный инструментарий, аспекты или подобное), я полагаю, что никто не сможет вам помочь.   -  person jarnbjo    schedule 18.11.2009
comment
Может ли сбор мусора играть роль в этом?   -  person Dave    schedule 18.11.2009
comment
никакие инструменты или аспекты не задействованы, наивный тест (выделение объекта, который потребляется другим потоком, следовательно, ускользает) примерно такого же размера показывает, что время для выделения примерно 2 микрос падает до ‹300 нс при компиляции вызова.   -  person Matt    schedule 06.01.2010


Ответы (5)


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

Очень полезный инструмент для получения лучшего представления о том, что происходит и когда проходит BTrace. Он похож на DTrace, но представляет собой чистый инструмент Java. На этой ноте я предполагаю, что вы знаете DTrace, если нет, то это также полезно, если не бестолково. Это даст вам некоторое представление о том, что и когда происходит в JVM и ОС.

О, еще одна вещь, которую нужно уточнить в вашем исходном сообщении. Какой у вас коллектор? Я предполагаю, что из-за проблемы с высокой задержкой вы используете сборщик с низкой паузой, такой как CMS. Если да, то пробовали ли вы какой-либо тюнинг?

person reccles    schedule 18.11.2009
comment
Да, мы используем CMS. До сих пор я не проверял/настраивал производительность распределения, а только общее время паузы. В списке находится еще один этап настройки сборщика мусора с учетом производительности распределения. Btrace выглядит интересно, попробую. К сожалению, DTrace не разрешен в нашей среде благодаря нашей всегда полезной инженерной группе. Грустно и странно, но факт. - person Matt; 19.11.2009

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

Если вы попробуете то же самое, когда JVM и/или процессор не прогреты. Вы получите совсем другие результаты.

Попробуйте сделать то же самое, скажем, 25 раз (меньше вашего порога компиляции) и спать (100) между тестами. Вы должны ожидать увидеть гораздо более высокие времена, ближе к тому, что вы видите в реальном приложении.

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

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

import java.io.*;
import java.util.Date;

/**
Cold JVM with a Hot CPU took 123 us average
Cold JVM with a Cold CPU took 403 us average
Cold JVM with a Hot CPU took 314 us average
Cold JVM with a Cold CPU took 510 us average
Cold JVM with a Hot CPU took 316 us average
Cold JVM with a Cold CPU took 514 us average
Cold JVM with a Hot CPU took 315 us average
Cold JVM with a Cold CPU took 545 us average
Cold JVM with a Hot CPU took 321 us average
Cold JVM with a Cold CPU took 542 us average
Hot JVM with a Hot CPU took 44 us average
Hot JVM with a Cold CPU took 111 us average
Hot JVM with a Hot CPU took 32 us average
Hot JVM with a Cold CPU took 96 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 80 us average
Hot JVM with a Hot CPU took 26 us average
Hot JVM with a Cold CPU took 90 us average
Hot JVM with a Hot CPU took 25 us average
Hot JVM with a Cold CPU took 98 us average
 */
public class HotColdBenchmark {
    public static void main(String... args) {
        // load all the classes.
        performTest(null, 25, false);
        for (int i = 0; i < 5; i++) {
            // still pretty cold
            performTest("Cold JVM with a Hot CPU", 25, false);
            // still pretty cold
            performTest("Cold JVM with a Cold CPU", 25, true);
        }

        // warmup the JVM
        performTest(null, 10000, false);
        for (int i = 0; i < 5; i++) {
            // warmed up.
            performTest("Hot JVM with a Hot CPU", 25, false);
            // bit cold
            performTest("Hot JVM with a Cold CPU", 25, true);
        }
    }

    public static long performTest(String report, int n, boolean sleep) {
        long time = 0;
        long ret = 0;
        for (int i = 0; i < n; i++) {
            long start = System.nanoTime();
            try {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(baos);
                oos.writeObject(new Date());
                oos.close();
                ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
                Date d = (Date) ois.readObject();
                ret += d.getTime();
                time += System.nanoTime() - start;
                if (sleep) Thread.sleep(100);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
        if (report != null) {
            System.out.printf("%s took %,d us average%n", report, time / n / 1000);
        }
        return ret;
    }
}
person Peter Lawrey    schedule 06.01.2010
comment
полностью согласен, но я уверен, что в данном случае это не проблема, поскольку я проводил длительные тесты (> 24 часа) и повторял тесты. Это означает, что n * 30 прогонов по 20 минут больше, чем время компиляции, и отчет об агрегированных результатах, т.е. отчет о производительности каждый ns, а затем суммирование результатов за период (что означает эквивалентный этап в прогоне) с min/max/avg и так далее. на. Результаты согласуются после начала компиляции. Кроме того, это тест приложения, а не синтетический, поэтому нагрузка является репрезентативной. - person Matt; 07.01.2010
comment
Ваш процессор горячий или холодный в течение временного периода? то есть он блокируется или спит до установленного времени? - person Peter Lawrey; 07.01.2010
comment
время безотказной работы сервера (ов) обычно исчисляется месяцами, и это тест приложения, поэтому он работает в нормальных условиях, поэтому я не контролирую состояние ядер напрямую до начала теста. В основном они будут бездействовать. Учитывая, что в коробке 16 ядер и нет наборов процессоров, а планировщик Solaris имеет тенденцию перемещать lwps, как ему кажется, практически невозможно контролировать состояние любого отдельного ядра. - person Matt; 07.01.2010
comment
Поэтому, если ваш ЦП обычно холодный, вам нужно сравнить, как ваше приложение работает с холодного запуска, чтобы получить сопоставимые результаты. Я обнаружил, что в этом случае задержка увеличивается в 2-5 раз. Кстати, не думайте, что вы ничего не можете с этим поделать. ;) - person Peter Lawrey; 08.01.2010
comment
результаты повторяются во многих запусках, когда приложение подвергается нормальной нагрузке. Он никогда не вызывает Thread.sleep (кроме запуска), обычно потоки обработки либо активны, либо ожидают работы (например, Unsafe.park через LinkedTransferQueue.take). Возможно, я упускаю из виду вашу точку зрения, но ваш пример говорит мне, что вы должны сделать все возможное, чтобы заставить себя подключиться к процессору и никогда не уступать, чтобы все продолжало вращаться. Я не понимаю, как это будет жизнеспособно в реальном приложении, если только у вас нет целой коробки для одного процесса, к сожалению, это не роскошь, которую я имею! - person Matt; 08.01.2010
comment
также стоит иметь в виду, что конкретный фрагмент кода, который меня беспокоит, находится в середине какой-либо другой обработки, поэтому поток активно работает на процессоре (учитывая, что мы не перегружаем блок, редко когда активный поток наткнулся) и выполняет разнообразную (связанную) работу, а не только конкретную часть, о которой я говорю здесь - person Matt; 08.01.2010
comment
Вы должны решить, стоит ли выделять поток/ЦП для задачи, ожидая занятости или нет. Если вы выделите ЦП, вы получите лучшее время задержки, если вы этого не сделаете, ЦП можно использовать для выполнения других задач (из того, что вы говорите, это предполагает, что это на самом деле не требуется). в любом случае не следует передавать работу между потоками. - person Peter Lawrey; 09.01.2010
comment
это не передача единиц работы между потоками, но мне нужно сбалансировать задержку с пропускной способностью, поэтому связывание такого потока на самом деле невозможно. - person Matt; 10.01.2010

Выделение памяти может вызвать побочные эффекты. Возможно ли, что выделение памяти приводит к уплотнению кучи? Вы смотрели, чтобы увидеть, не вызывает ли ваше распределение памяти запуск GC в то же время?

Засекали ли вы отдельно время, необходимое для создания новых списков массивов?

person RickNZ    schedule 18.11.2009

Вероятно, нет никакой надежды ожидать гарантии задержки в микросекундах от виртуальной машины общего назначения, работающей на ОС общего назначения, даже с таким отличным оборудованием. Огромная пропускная способность — это лучшее, на что вы можете надеяться. Как насчет переключения на виртуальную машину реального времени, если она вам нужна (я имею в виду RTSJ и все такое...)

...мои два цента

person Gyom    schedule 18.11.2009
comment
это правда, однако на самом деле у нас нет потребностей в жестком реальном времени (как правило, достаточно как можно быстрее), а RTSJ довольно инвазивен, если честно. В общем, пока я могу объяснить, где я провожу время, я счастлив. В этом случае это необъяснимо медленно, поэтому мне нужно понять это, иначе у нас будет неизвестное поведение. Неизвестное поведение IME обычно приводит к дальнейшим проблемам. - person Matt; 19.11.2009

Одни дикие догадки:

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

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

person Zan Lynx    schedule 23.12.2009
comment
каждый объект управляется GC, и выделение должно быть довольно дешевым, учитывая, что в момент выделения объекта он выделяется из предварительно выделенного локального буфера потока (TLAB), поэтому, по сути, это событие сдвига указателя. Существует стоимость межпоколенческой ссылки, но она, насколько мне известно, невелика в момент создания и влечет за собой затраты во время сбора из-за увеличения стоимости определения того, что живо, а что нет. по 2-му пункту, похоже, вы говорите о подсчете ссылок, которого JVM не делают. - person Matt; 26.12.2009
comment
Мэтт: относительно подсчета ссылок: есть подсчет ссылок, а есть подсчет ссылок... Сборщик мусора, который знает разницу между 1 и многими ссылками, может значительно упростить себе задачу. При очистке объектов, которые содержат единственную ссылку на другой объект, этот объект также может быть немедленно собран. - person Zan Lynx; 27.12.2009
comment
правильно, но стоимость (или экономия) в этом случае будет приходиться на время сбора, а не на время распределения - person Matt; 28.12.2009