Доступ к локальному полю против поля объекта. Док не прав?

Документация кажется неправильной. Может кто-нибудь сказать мне, что это правда?

В разделе " Мифы о производительности":

На устройствах без JIT кеширование доступа к полю происходит примерно на 20% быстрее, чем повторный доступ к полю. С JIT доступ к полю стоит примерно так же, как и локальный доступ.

В разделе Избегайте внутренних методов получения / установки:

Без JIT прямой доступ к полю примерно в 3 раза быстрее, чем вызов тривиального геттера. С JIT (где прямой доступ к полю обходится дешевле, чем доступ к локальному), прямой доступ к полю примерно в 7 раз быстрее, чем вызов тривиального геттера.

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

Но почему в первом случае производительность на 20% выше, а во втором - на 133% по той же причине, то есть JIT-оптимизации для поля вызывающего объекта?

3 ответа

Решение

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

Насколько я понимаю, аналогия прямого доступа к полю и локального доступа (а не локального доступа к полю, как вы написали в своем посте - здесь нет такого понятия, как локальное поле):

class foo {
    int bar = 42;

    int doStuff(){
        int x = 1;
        x += bar;
        x += bar;
        x += bar;
        return x;
    }
}

Каждая ссылка на bar имеет связанную стоимость производительности. Хороший компилятор распознает возможность оптимизации и "переписывает" код как таковой:

int doStuff(){
    int x = 1f;
    int local_bar = bar;
    x += local_bar;
    x += local_bar;
    x += local_bar;
    return x;
}

Без JIT это удобная оптимизация, которая дает вам 20% прирост производительности.

При использовании JIT оптимизация не требуется, поскольку JIT удаляет снижение производительности при доступе к bar на первом месте.

Вторая ссылка описывает следующий сценарий:

class foo {
    int bar = 42;

    int getBar() { return bar; }

    int doStuff(){
        int x = 1;
        x += getBar();
        x += getBar();
        x += getBar();
        return x;
    }
}

С каждым вызовом функции связан штраф за производительность. Компилятор НЕ может кешировать несколько getBar() вызовы метода (так как он кэшировал множественный прямой доступ к полям bar в предыдущем примере), потому что getBar() может возвращать совершенно разные числа каждый раз, когда он вызывается (т. е. если он имеет случайный или основанный на времени компонент к своему возвращаемому значению). Следовательно, он должен выполнить три вызова метода.

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

Если вы должны были вручную заменить getBar() в приведенной выше функции просто bar, вы бы достигли повышения производительности. На машине без JIT это повышение производительности примерно в 3 раза, потому что доступ к полю все еще несколько медленный, поэтому замена очень медленных методов на несколько медленный доступ к полю дает только умеренное повышение. Однако при использовании JIT доступ к полям происходит быстро, поэтому замена очень медленных методов на быстрый доступ к полям дает гораздо больший (7x) прирост.

Надеюсь это имеет смысл!

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

 caching field accesses is about 20% faster than repeatedly accesssing the field

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

int a = this.field;
if (a == 1)
...
if (a == 7) // etc.

дает лучшую производительность, чем

if (this.field == 1)
....
if (this.field == 7) //etc.

Цитата предполагает, что вы получите штрафной удар, если будете многократно ссылаться на поле, а не хранить его локально.

Вторая цитата предполагает, что без JIT использование тривиального метода получения / установки медленнее, чем прямой доступ к полю, например:

if (this.getField()) // etc.

медленнее чем:

if (this.field) // etc.

Я не думаю, что документация неверна или что одно утверждение подрывает другое.

Это просто обоснованное предположение, я понятия не имею о внутренностях Dalvik. Но обратите внимание, что в первом случае производительность локального доступа сравнивается с доступом к полю, тогда как во втором случае доступ к полю сравнивается с тривиальным вызовом метода. Также обратите внимание, что ускорение на x% на самом деле не на x% меньше времени, затрачиваемого на тот же код, добавляя JIT, мы говорим об относительной производительности: (a) Интерпретированный локальный доступ на 20% быстрее, чем интерпретированный доступ к полям, и (b) Локальный доступ JIT осуществляется так же быстро, как и доступ к полям JIT. (c) Интерпретированный локальный доступ осуществляется так же быстро, как локальный / полевой доступ JIT. На самом деле, скорее всего, медленнее.

Чтение локального в интерпретаторе - это, в большинстве архитектур ВМ, доступ к памяти, а не доступ к регистру (мы говорим о машинных регистрах, а не о регистрах Dalvik). Чтение поля еще медленнее - я не могу точно сказать, почему (я думаю, это будет второй поиск, считывающий регистр и поле объекта), но в любом случае это более сложно. JIT, с другой стороны, может помещать как поля, так и локальные данные в регистры (это то, что я должен предположить, чтобы объяснить равенство производительности, и на самом деле есть JIT, которые делают это - я просто не знаю, применимо ли это здесь) и удаляет большую часть накладных расходов.

Для вызовов методов, предполагая, что JIT Dalvik не являются встроенными методами (что подразумевается), у вас есть некоторые накладные расходы поверх фактического вызова, что делает вызовы дорогими, даже когда JIT'd: должен сохранять регистры в стеке, должен восстанавливать их впоследствии, не может оптимизировать так много, потому что не весь код виден. Вызов является относительно более дорогим, чем код без вызова, потому что альтернатива без вызова работает очень быстро, а не потому, что интерпретатор лучше выполняет вызовы (это не так, он просто делает все медленнее). Например, никакие оптимизации не предотвращаются вызовом, потому что нет никаких оптимизаций.

Другие вопросы по тегам