2016-10-20 4 views
15

В приведенном ниже коде указаны две простые функции по 10 миллиардов раз каждый.Почему два аргумента строки более эффективны, чем один аргумент списка

public class PerfTest { 
    private static long l = 0; 

    public static void main(String[] args) { 
     List<String> list = Arrays.asList("a", "b"); 
     long time1 = System.currentTimeMillis(); 
     for (long i = 0; i < 1E10; i++) { 
      func1("a", "b"); 
     } 
     long time2 = System.currentTimeMillis(); 
     for (long i = 0; i < 1E10; i++) { 
      func2(list); 
     } 
     System.out.println((time2 - time1) + "/" + (System.currentTimeMillis() - time2)); 
    } 

    private static void func1(String s1, String s2) { l++; } 
    private static void func2(List<String> sl) { l++; } 
} 

Мое предположение заключалось в том, что выполнение этих двух вызовов будет близким к идентичному. Если что-нибудь, я бы догадался, что прохождение двух аргументов будет немного медленнее, чем передача одного. Учитывая, что все аргументы являются объектными ссылками, я не ожидал, что один из них представляет собой список, который имеет значение.

Я провел тест много раз, и типичным результатом является «12781/30536». Другими словами, вызов с использованием двух строк занимает 13 секунд, а вызов с использованием списка занимает 30 секунд.

Каково объяснение этой разницы в производительности? Или это несправедливый тест? Я попытался переключить два вызова (в случае, если это было вызвано эффектами запуска), но результаты одинаковы.

Update

Это не справедливо тест по многим причинам. Однако он демонстрирует реальное поведение компилятора Java. Обратите внимание на следующие два дополнения, чтобы продемонстрировать это:

  • Добавление выражений s1.getClass() и sl.getClass() к функциям делает два вызова функции perfom тот же
  • Запуск теста с -XX:-TieredCompilation также делает две функции вызовы выполняют ту же

Объяснение этого поведения приведено в принятом ниже ответе. Очень краткое изложение ответа @ apangin заключается в том, что func2 не встроен компилятором hotspot, потому что класс его аргумента (т. Е. List) не разрешен. Принудительное разрешение класса (например, с использованием getClass) заставляет его быть встроенным, что значительно улучшает его производительность. Как было указано в ответе, нерешенные классы вряд ли произойдут в реальном коде, что делает этот код нереалистичным краевым случаем.

+0

Можете ли вы добавить, что вы ожидали и почему? – ChiefTwoPencils

+1

@ChiefTwoPencils добавили этот параграф. – sprinter

+0

Я не проголосовал за закрытие, но если кто-то не захочет взломать среду выполнения, чтобы посмотреть на конкретные оптимизации компиляции, большинство вопросов производительности на самом деле не очень полезны (хотя они могут быть забавными/интересными), и ответы могут перейти от выпуска к выпуску. В этом случае я просто предполагаю, что JVM усложнил компиляцию или запоминание двух вызовов параметров, чем вызов массива, но серьезно - просто напишите, что является самым читаемым! Также обратите внимание, что наиболее читаемая версия часто такова, что оптимизация JVM оптимальна. –

ответ

19

Показатель unfair, однако он выявил интересный эффект.

Как отметил Sotirios Delimanolis, разница в производительности вызвана тем, что func1 встроен компилятором HotSpot, а func2 - нет. Причиной является аргумент func2 типа List, класс, который никогда не был разрешен во время выполнения теста.

Обратите внимание, что List класса фактически не используется: нет методов Списка называется, нет полого списка типа не объявляли, ни один класс не бросает, и никаких других действий не выполняются, которые обычно вызывает класс resolution. Если вы добавите использование класса List в любом месте кода, то func2 будет встроен.

Другой вопрос, который повлиял на стратегию компиляции, - это простота метода. Это так просто, что JVM решил скомпилировать его в Tier 1 (C1 без дальнейшей оптимизации). Если бы он был скомпилирован с C2, то был бы разрешен класс List. Попробуйте запустить с помощью -XX:-TieredCompilation, и вы увидите, что func2 удачно вложил и выполняет так же быстро, как func1.

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

+0

Я попытался добавить код к обеим функциям, чтобы гарантировать, что 'List' разрешен, а также попытался с параметром' TieredCompliation'. Как вы предсказали, оба make 'func2' выполняют так же быстро, как' func1'. Спасибо за четкое объяснение. Я обновлю вопрос, чтобы сделать эту информацию понятной для будущих читателей. – sprinter

+0

Спрашивая вопрос, я провел довольно много исследований по микрообъектам и попробовал JMH, а также по ручному кодированию следующих предложений по адресу http://stackoverflow.com/questions/504103/how-do-i-write-a-correct -micro-тест-в-Java. Мой вывод после нескольких попыток состоит в том, что до тех пор, пока вы не хотите точности и тест проходит достаточно долго, тесты на основе секундомера достигают точно такого же вывода. На ваш взгляд, все тоски о честных тестах действительно оправданы или это кулон? – sprinter

+0

@sprinter У меня есть ** много ** примеров, когда исходные ориентиры на основе секундомера приводят к ошибочным результатам. Вот несколько примеров из SO: [1] (http://stackoverflow.com/a/24889503/3448419), [2] (http: // stackoverflow.(http://stackoverflow.com/a/38578777/3448419), [4] (http://stackoverflow.com/a/33858960/3448419), [3] [5] (http://stackoverflow.com/a/24344114/3448419), [6] (http://stackoverflow.com/a/33193452/3448419), [7] (http: // stackoverflow. com/a/25242358/3448419), [8] (http://stackoverflow.com/a/39907607/3448419), [9] (http://stackoverflow.com/a/35671374/3448419) – apangin