您是否在本地变量中缓存属性?

时间:2014-01-27 21:56:16

标签: java caching coding-style

考虑班级Foo

public class Foo {
    private double size;

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

Foo有一个名为size的属性,它经常被访问,但从未被给定的方法修改过。每当在任何方法中多次访问变量时,我总是在变量中缓存一个属性,因为“有人告诉过我”而没有多想。即。

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

这值得吗,还是矫枉过正?

如果我不缓存它,那么现代编译器是否足够智能以自行缓存它?

6 个答案:

答案 0 :(得分:16)

在决定是否存储调用“get()方法”返回的值时,我考虑了几个因素(无特定顺序):

  1. get()方法的性能 - 除非API指定,或者除非调用代码与被调用方法紧密耦合,否则无法保证get()方法的性能。代码现在可以很好地进行测试,但如果get()方法性能在将来发生变化或者测试不能反映实际情况,则可能会变得更糟。 (例如,当真实世界的容器可能有一千万个时,在容器中只有一千个对象进行测试)在for循环中使用,每次迭代之前都会调用get()方法

  2. 可读性 - 变量可以被赋予特定的描述性名称,以对内联调用get()方法可能不明确的方式提供其使用和/或含义的说明。不要低估审查和维护代码的价值。

  3. 线程安全 - 如果另一个线程在调用方法正在执行其操作时修改对象,则get()方法返回的值是否可能发生更改?这种变化是否应该反映在调用方法的行为中?

  4. 关于编译器是否会自己缓存它的问题,我将推测并说在大多数情况下答案必须是'否'。编译器可以安全地执行此操作的唯一方法是,如果它可以确定get()方法将在每次调用时返回相同的值。只有当get()方法本身被标记为final并且它所做的只是返回一个常量(即也标记为'final'的对象或原语)时,才能保证这一点。我不确定,但我认为这可能不是编译器困扰的情况。 JIT编译器具有更多信息,因此可以具有更大的灵活性,但您无法保证某些方法将获得JIT。

    总之,不要担心编译器可能会做什么。缓存get()方法的返回值可能是大多数时候做的正确的事情,很少(,但几乎从不)是错误的事情。喜欢编写代码,这些代码在快速(可用)和华而不实的代码中是可读和正确的。

答案 1 :(得分:5)

我不知道是否有“正确”的答案,但我会保留一份本地副本。

在你的例子中,我可以看到getSize()是微不足道的,但在实际代码中,我并不总是知道它是否是微不足道的;即使今天它是微不足道的,我也不知道有人不会出现并改变getSize()方法,以便在将来某个时候使其变得非常微不足道。

答案 2 :(得分:3)

最大的因素是表现。如果这是一个简单的操作,不需要很多CPU周期,我会说不要缓存它。但是如果你经常需要对不会改变的数据执行昂贵的操作,那么一定要缓存它。例如,在我的应用程序中,当前登录的用户在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方法,除非用户注销然后重新进入,否则数据不会改变,所以在这种情况下我会说缓存值是安全的。

答案 3 :(得分:2)

如果计算 size 的值,每次通过循环遍历数组而不是O(1),则缓存该值将在性能方面具有明显的好处。但是,由于 Foo size 预计不会在任何时候发生变化且为O(1),因此缓存该值主要有助于提高可读性。我建议继续缓存该值,因为可读性通常比现代计算系统中的性能更令人担忧。

答案 4 :(得分:1)

IMO,如果你真的担心性能,这有点过分或广泛,但有几种方法可以确保变量被你的VM“缓存”,

首先,您可以创建结果的最终静态变量(根据您的示例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设置最终变量,但由于这是一个局部变量,你可以使用构造函数:< / p>

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

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

可以通过foo.getSize()

访问

答案 5 :(得分:1)

在我的代码中,如果getSize()方法耗时或者更常见的话,我会缓存它,结果用于或多或少复杂的表达式。

例如,如果计算尺寸

的偏移量
int offset = fooSize * count1 + fooSize * count2;

更容易阅读(对我而言)
int offset = foo.getSize() * count1 + foo.getSize() * count2;