Свойства кэша в локальных переменных?

Рассмотрим класс Foo.

public class Foo {
    private double size;

    public double getSize() {
        return this.size; // Always O(1)
    }
}

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

public void test(Foo foo) {
    double size = foo.getSize(); // Cache it or not?
    // size will be referenced in several places later on.
}

Стоит ли это, или излишний?

Если я не кэширую его, современные компиляторы достаточно умны, чтобы кэшировать их сами?

Ответ 1

Несколько факторов (в определенном порядке), которые я рассматриваю при принятии решения о том, хранить или не сохранять значение, возвращаемое вызовом метода get():

  • Производительность метода get(). Если API не указан или если вызывающий код не тесно связан с вызываемым методом, нет никаких гарантий эффективности метода get(). Теперь код может быть хорошим в тестировании, но может ухудшиться, если методы get() будут меняться в будущем или если тестирование не отражает реальных условий. (например, тестирование с тысячей объектов в контейнере, когда контейнер реального мира может иметь десять миллионов). Используется в цикле for, метод get() будет вызываться перед каждой итерацией

  • Считываемость. Переменной может быть задано конкретное и описательное имя, предоставляющее разъяснение его использования и/или значения таким образом, который может быть не ясен из встроенных вызовов метода get(). Не стоит недооценивать значение этого для тех, кто просматривает и поддерживает код.

  • Безопасность потоков. Может ли значение, возвращаемое методом get(), потенциально измениться, если другой поток изменяет объект, пока вызывающий метод выполняет свою работу? Должно ли такое изменение отразиться на поведении вызывающего метода?

Что касается вопроса о том, будут ли сами компиляторы кэшировать его, я собираюсь рассказать и сказать, что в большинстве случаев ответ должен быть "нет". Единственный способ, с помощью которого компилятор мог бы это сделать, - определить, сможет ли метод get() возвращать одно и то же значение при каждом вызове. И это может быть гарантировано только в том случае, если сам метод get() был помечен как final, и все, что он сделал, это вернуть константу (т.е. объект или примитив также отмечен как "final" ). Я не уверен, но я думаю, что это, вероятно, не сценарий, с которым компилятор беспокоится. Компилятор JIT имеет больше информации и, следовательно, может иметь большую гибкость, но у вас нет гарантий, что какой-то метод получит JIT'ed.

В заключение не беспокойтесь о том, что может сделать компилятор. Кэширование возвращаемого значения метода get() - это, вероятно, правильная вещь, которую нужно делать большую часть времени, и редко (я почти никогда) не ошибаюсь. Благодарите написанный код, который читается и исправляется по быстрому (est) и flashy.

Ответ 2

Я не знаю, есть ли "правильный" ответ, но я бы сохранил локальную копию.

В вашем примере, я вижу, что getSize() является тривиальным, но в реальном коде я не всегда знаю, является ли он тривиальным или нет; и даже если сегодня это тривиально, я не знаю, что кто-то не придет и не изменит метод getSize(), чтобы сделать его нетривиальным в будущем.

Ответ 3

Самым большим фактором будет производительность. Если это простая операция, которая не требует много циклов процессора, я бы сказал, не кэшировать ее. Но если вам постоянно нужно выполнять дорогостоящую операцию над данными, которые не меняются, то обязательно их кешируйте. Например, в моем приложении пользователь, который в настоящий момент вошел в систему, сериализуется на каждой странице в формате JSON, операция сериализации довольно дорога, поэтому для повышения производительности я теперь сериализую пользователя один раз, когда он подписывается, а затем использует сериализованную версию для помещая JSON на страницу. Здесь до и после, заметно улучшилась производительность:

//Перед

public User(Principal principal) {
    super(principal.getUsername(), principal.getPassword(), principal.getAuthorities());
    uuid            = principal.getUuid();
    id              = principal.getId();
    name            = principal.getName();
    isGymAdmin      = hasAnyRole(Role.ROLE_ADMIN);
    isCustomBranding= hasAnyRole(Role.ROLE_CUSTOM_BRANDING);
    locations.addAll(principal.getLocations());
}
public String toJson() {
    **return JSONAdapter.getGenericSerializer().serialize(this);**
}

//После

public User(Principal principal) {
    super(principal.getUsername(), principal.getPassword(), principal.getAuthorities());
    uuid            = principal.getUuid();
    id              = principal.getId();
    name            = principal.getName();
    isGymAdmin      = hasAnyRole(Role.ROLE_ADMIN);
    isCustomBranding= hasAnyRole(Role.ROLE_CUSTOM_BRANDING);
    locations.addAll(principal.getLocations());
    **json = JSONAdapter.getGenericSerializer().serialize(this);**
}
public String toJson() {
    return json;
}

Объект User не имеет методов setter, нет данных, которые данные могли бы изменить, если пользователь не выберет и не вернется, поэтому в этом случае я бы сказал, что безопасно кэшировать значение.

Ответ 4

Если значение размера вычислялось каждый раз, скажем, путем перебора массива и, следовательно, не O (1), кэширование значения имело бы очевидные преимущества по производительности. Однако, поскольку размер Foo не ожидается в какой-либо точке, и это O (1), кэширование значения в основном помогает читаемости. Я рекомендую продолжать кешировать значение просто потому, что читаемость часто вызывает большую озабоченность, чем производительность в современных вычислительных системах.

Ответ 5

IMO, если вы действительно обеспокоены производительностью, это немного избыточно или экстенсивно, но есть несколько способов гарантировать, что переменная "кэшируется" вашей виртуальной машиной,

Во-первых, вы можете создавать конечные статические переменные результатов (согласно вашему примеру 1 или 0), поэтому для всего класса сохраняется только одна копия, тогда ваша локальная переменная является только логической (с использованием только 1 бит), но при этом сохраняя значение результата double (также, возможно, вы можете использовать int, если он равен только 0 или 1)

private static final double D_ZERO = 0.0;
private static final double D_ONE = 1.0;

private boolean ZERO = false;

public double getSize(){
    return (ZERO ? D_ZERO : D_ONE);
}

Или, если вы можете установить размер при инициализации класса, с которым вы можете пойти, вы можете установить конечную переменную через конструктор и static, но поскольку это локальная переменная, вы можете пойти с конструктором:

private final int SIZE;
public foo(){
    SIZE = 0;
}

public double getSize(){
    return this.SIZE;
}

к этому можно получить доступ через foo.getSize()

Ответ 6

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

Например, если вычисление смещения от размера

int offset = fooSize * count1 + fooSize * count2;

легче читать (для меня), чем

int offset = foo.getSize() * count1 + foo.getSize() * count2;