안드로이드 기기의 성능이 좋아지면서 많이 묻히긴 했지만, 기본적으로 지키길 권장하는 구글의 퍼포먼스 팁을 옮겨보았다.

원문 : Performance Tips

불필요한 객체 생성을 피하라

객체 생성은 공짜가 아니다.  잠깐쓰는 자잘한 객체 좀 만드는게 큰일인가 싶겠지만, 메모리 할당이라는 작업 자체가 할당 안하는 것 보단 비싼 작업이다.  물론 안드로이드 버전이 2.3에 들어 concurrent GC가 도입되면서 앱이 사레들리는(hiccup) 현상은 좀 나아졌으나 그래도 쓸모없는 객체 생성은 피하는 것이 좋다.

  • 짧은 객체를 자주 생성하는 것을 피한다. String을 길게 이어붙인다면 StringBuffer를 쓴다.
  • (가능하다면) 복사 보다는 객체 있는거 그대로 써라.
  • (가능하다면) 박싱 된 데이터타입 보단 primitive 그대로..

static 메서드 사용을 권장한다

인스턴스에 뭔가 접근해서 작업할 것이 아니라면, static 메서드의 호출 속도가 15~20% 더 빠르다.

상수에는  static final을 붙여라

어떤 객체에 다음과 같은 상수를 선언했다 치자.

static int intVal = 42;
static String strVal = "Hello, world!";

컴파일러는 암묵적으로 <clinit>이라 불리는 초기화용 메서드를 생성 하며, 클래스가 처음 사용 될 때 해당 메서드가 호출된다. intVal은 이 때 42로 초기화 되고, strVal은 클래스 파일의 string table을 참조해 reference값이 할당 될 것이다. 이들 값이 이후에 참조될 때는, 룩업을 통해 참조된다.

여기에다가 final을 추가하면 어떻게 될까?

static final int intVal = 42;
static final String strVal = "Hello, world!";

final이 붙는 경우엔, dex파일 내에 있는 static field initializer에 의해 자동으로 초기화 된다. 즉 초기화 메서드가 필요 없어지므로 메서드 호출 단계가 하나 줄어든다.

Internal Getter/Setter 사용을 피하라

안드로이드에서 virtual method 호출은, 생각보다 비싼 작업이다. 따라서 가능하면 내부에서는 필드 접근을 다이렉트로 하는것이 추천된다. JIT를 사용한다고 가정했을 때, getter를 쓰는것과 안 쓰는것의 접근시간 차이는 약 7배정도라고 설명되어 있다.

ProGuard를 사용한다면, accessor들에 대해서 자동으로 인라인 시켜준다.

향상된 for loop 문법을 사용하라

향상된 for loop(흔히 foreach라 말하는)문법은 내부적으로 Iterable 인터페이스를 사용해 루프를 돌린다. 이 문법을 사용하면 ArrayList와 같은 콜렉션에서 직접 인덱스 카운팅하는 루프에 비해 3배정도 성능이 빨라진다.

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero()는 가장 느리다. 직접 카운팅 하는 루프는 JIT이 최적화를 하지 못한다.

one()은 zero에 비하면 더 빨라진 상황이긴하다. 배열에 있는 length 필드를 캐싱하여 사용함으로써 불필요한 룩업을 피하고 있다.

two()는 JIT을 사용할 수 없는 기기에서 가장 성능이 좋다. JIT을 사용하는 기기에서는 성능차이가 구분되지 않는다. Java 1.5부터 사용가능한 전형적인 foreach 문법이다.

그러니 가능하면 two()와 같은 문법을 추천하는 것이다. 하지만 경우에 따라선 직접 카운트 하면서 루프를 돌려야 하는 상황이 더 빠를수도 있다.

private 이너클래스에서 외부클래스의 private 멤버에 대한 접근은 신중하게 사용하라

아래 소스를 보자

public class Foo {
    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }

    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }
}

private 이너클래스(Foo$Inner)에서 Foo 클래스의 mValue에 직접 접근을 하고 있다. 이 문법 자체는 자바 문법 규칙상으론 합법적이다. ‘Value is 27’이 화면에 출력 될 것이다.

근데 이를 실행할 때 VM에서 무슨 일이 일어나는지 생각해볼 필요가 있다. VM에서는 Foo와 Inner는 전혀 다른 클래스로 취급한다. 따라서 Inner 클래스가 Foo의 private 멤버에 접근하는 것은 불법이다. VM은 안된다 하고, 자바 문법은 된다고 하고 모순이 아닌가?

따라서 이런 괴리를 해결하고자, 컴파일러는 암묵적으로 getter 메서드를 생성하여, 아래와 같이 메서드를 통해 해당 값을 읽어오도록 코드를 생성한다.

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

자바코드 짤때는 직접 접근한다고 짰더니, 내부적으론 getter를 통한 접근이 되어버렸다. 이렇게 보이지 않는 지점에서 성능상 손해를 보는 코드를 생성 해낼 수 있다.

부동소수점 사용은 피하라

경험에 의하면, 부동소수점은 정수에 비해 2배정도 느리다. 사실 속도라는 용어 관점에서 볼때 float와 double은 현대 FPU에서는 차이가 없다. 공간 관점에서는 double이 2배 더 크긴 하지만 공간이 문제되지 않는다면 걍 double 쓰는것이 낫다.

일부 성능이 떨어지는 CPU들은 심지어는 정수쪽까지도 하드웨어 곱셈/나눗셈이 지원되지 않을 수 있다. 이런 경우에는 소프트웨어 에뮬레이션을 통해 연산하게되니 느려터질것은 당연하다. 여러가지 최적화 방법을 통해 극복할 수 있다. 룩업 테이블이 그 중 한가지다.

내장 라이브러리를 잘 알고 사용하자

어떤 특별한 문제가 있지 않는 이상 내가 직접 만드는것보단 내장 라이브러리로 제공되는 것이 더 성능이 좋고 신뢰성이 있다. 예를들어 루프돌려서 직접 카피하는 메서드를 작성하느니, System.arraycopy를 쓰는게 낫다. JIT를 사용한 Nexus One 기종에서 실행했을 때, 약 9배정도 성능 차이가 난다.

네이티브 코드는 조심스럽게 사용하자

안드로이드의 NDK는 일반적인 앱 작성에는 그다지 효율적이지 않다. 왜냐하면 자바-네이티브간 전환에서 성능을 많이 깎아먹기 때문이다. 이 지점은 또한 JIT을 통해 최적화를 할 수 없는 부분이기도 하다. 이미 C로 포팅 된 예전 라이브러리들을 재활용하거나, 성능에 굉장히 크리티컬한 부분은 NDK가 적절하지만, 일반적인 앱까지 네이티브 코드를 쓰는건 뻘짓이다.

퍼포먼스 미신

JIT을 사용하지 않는 디바이스에서는, interface를 써서 concrete를 사용 하는것 보단 직접 concrete타입 레퍼런스를 통해 사용하는 것이 조금 더 성능이 좋다고 알려져 있다. 즉 HashMap을 사용할거면 Map<K, V> 인터페이스보다 HashMap<K, V>를 직접 쓰는게 나았단 것이다. 실제로 JIT를 사용할 수 없었던 시절엔 2배정도 성능차이가 발생했다.

하지만 JIT를 사용한다면 이야기가 달라진다. 겨우 6%의 성능차이가 발생한다. 이정도면 걍 Map을 쓰는게 프로그램 구조상 이득이다.

증거없는 맹목적인 최적화는 사쿠라들고 실실 웃는 아귀와 같다. 그 시간에 코드 구조를 개선하는게 더 이득이다.

항상 측정하라

최적화를 시작하기 전에, 문제를 확실하게 정리할 필요가 있다. 퍼포먼스를 향상 시키려면 어느지점에서 가장 시간을 잡아먹는지, 공간을 잡아먹는지를 찾아내야 한다.

Traceview와 같은 도구를 사용할 수 있다. 다만 JIT가 off되므로 코드 최적화시 주의해야 한다.