2016년 10월 5일 수요일

[안드로이드] 성능향상 팁

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

불필요한 객체생성은 메모리의 낭비를 가져오게 되고, 결국에는 GC작업으로 이어지게 된다. GC는 매우 비싼 연산이므로, 성능에 매우 악영향을 미치게 된다. (2.3에서 동시 가비지 컬렉터가 소개되긴 했지만, 그럼에도 비싼 연산임을 유념하자!)
다음은 가장 흔한 몇 가지의 예를 보여준다.
  • 어떤 함수가 String을 반환하고, 그 반환값이 항상 StringBuffer에 추가되는 경우가 있다면, 메서드를 변경하여 내부에서 직접 StringBuffer에 추가하도록 한다.
  • 어떤 입력된 String 데이터로부터 문자열을 추출하는 경우, 새로 문자열을 생성하는 대신 기존 데이터의 substring() 메서드를 이용하도록 한다.
  • Integer 등 Wrapper 배열을 이용중이라면, primitive 타입 배열로 변경을 고려한다.
  • (Object1, Object2) 튜플 형태의 배열 1개를 이용중이라면, Object1[], Object2[]의 배열 2개로 변경을 고려한다.(단, API 제작 등 성능보다는 디자인패턴이 중요할 경우 예외)
즉, GC 작업을 피하기 위해, 불필요한 객체생성은 가능하면 피하는 것이 좋다.

2.Virtual 메서드보다는 Static 메서드를 이용하라

만약, 메서드 내에서 객체의 멤버필드를 이용하지 않는다면, 메서드를 static으로 변경하는 것을 고려한다. 메서드 실행이 대략 15~20% 정도 빨라질 것이다. 또한, 메서드 호출이, 객체의 상태를 바꾸지 못하는 것을 메서드 원형을 통해 바로 알 수 있어서 코드 분석하기 좋다.

3.상수에는 static final 키워드를 이용하라

static int intVal = 42;
static String strVal = "Hello";
상수를 선언하기 위해 다음과 같이 final 키워드 없이 static 필드를 이용하게 되면, 컴파일러가 이 값들을 해당 변수에 할당하고, 이 변수들을 검색테이블에 저장한다. 나중에 이 변수들을 이용하려면, 컴파일러는 이 검색테이블을 통해 변수를 검색하여 값을 얻어오게 된다.
static final int intVal = 42;
static final String strVal = "hello";
다음과 같이 final 필드를 함께 이용할 경우, 이 값들은 dex 파일의 static 영역에 함꼐 저장된다. 이 경우, 변수를 이용하려면 컴파일러는 검색테이블을 거치지 않고 바로 값에 접근한다. 그렇기에 일반적으로는 static final 필드가 변수 접근속도가 빠르다.
단, 이는 primitive 타입과 String 상수에 한해 유효하다.

4.클래스 내부에서의 Getter/Setter는 피하라

일반적인 OOP 네이티브 언어(C++, C#, JAVA 등)는 멤버필드의 Getter/Setter를 지향하며, 클래스 내부에서도 사용하기를 권장한다. 이는 컴파일러가 대부분 인라인으로 엑세스하기 때문에 성능이슈가 거의 없고, 필요에 따라 변수에 제약조건을 둘 수 있기 때문이다.
하지만, 안드로이드에서는 같은 자바 언어를 사용하지만 컴파일러의 동작이 다르기 때문에 클래스 내부에서의 Getter/Setter 사용은 변수에 바로 접근하는 것보다 매우 비싼 연산이다.(심지어 변수를 검색테이블에서 찾는 것 보다도 느리다.) 그러므로, 자신의 멤버필드를 접근할 때는 가급적 Getter/Setter를 피하고 직접 이용하도록 한다.
Note : 클래스 외부에서 Getter/Setter를 이용하지 말자는 의미가 아니다. OOP를 지향하기 위해 클래스 외부에서는 Getter/Setter를 이용하는 것이 좋다.

5.향상된 for 문을 이용하라

자바 1.5부터 지원하고 있는 향상된 for문 (일명 for-each문)은 일반 배열과 Collections 객체에 대해 이용할 수 있다. Collections를 향상된 for문으로 돌리면 컴파일러가 Iterator를 이용하는 방법으로 바꿔서 컴파일한다.(즉, 명시적으로 Iterator를 이용하는 것과 동일하다.) 일반 배열의 경우에는 성능차이가 많이 날 수 있다.
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() 메서드는 매 루프마다 mArray.length를 계속 접근하기 때문에 가장 느리다.
  • one() 메서드는 멤버변수의 검색을 피하기 위해서 로컬변수에 대입해 이용하고 있고, length도 매번 불리지 않도록 메모리에 캐싱해두었기 때문에 zero() 메서드보다 빠르다.
  • two() 메서드는 JIT가 없는 디바이스에서는 앞의 두 메서드보다 빠르고, JIT가 있는 디바이스에서는 one() 메서드와 거의 같은 성능을 보인다.

6.Private Inner 클래스에서의 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 inner 클래스인 Inner는 자신의 outer 클래스인 Foo의 private 필드와 메서드를 이용하고 있다. 이는 자바의 문법으로는 가능하며, 컴파일도 잘 되고, 실행값도 우리가 예상하는 대로 "Value is 27"이 출력될 것이다.
그러나, VM은 Foo와 Foo$Inner를 전혀 별개의 클래스로 판단하고, 자바문법이 허용했지만 Inner에서의 private 필드/메서드의 접근이 잘못되었다고 인식한다. 따라서, VM은
이를 해결하기 위해 다음과 같은 브릿지 메서드를 내부적으로 생성한다.
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
이 브릿지 메서드는 Inner에서 mValue를 접근하거나, doStuff() 메서드를 호출할 때마다 호출된다. 이는 당연히 필드를 직접 엑세스하는 것보다 느리다. 브릿지 메서드의 생성을 피하려면, Foo의 멤버/메서드를 private이 아닌 package 접근제어자로 변경해주면 된다. 하지만, 변경된 멤버/메서드는 같은 패키지 내의 다른 클래스에서 이용될 수 있기 때문에 OOP에 위배되므로 잘 고려해서 변경해야 한다.

7.부동소수점 사용을 피하라

안드로이드 디바이스에서는 부동소수점 연산은 정수 연산보다 약 2배정도 느리다. 속도 관점에서 보면, double과 float은 별 차이가 없다. 그러므로, 어차피 부동소수점 연산을 해야하고 메모리의 제약이 덜하다면, 2배 더 정밀한 표현이 가능한 double을 이용하는 것이 낫다.

8.시스템 Library를 잘 알고, 사용하자

먼저 코드를 구현하기 전에, 시스템에서 지원하는 라이브러리 중에 구현하고자 하는 동작을 지원하는지 확인하여 가급적 시스템이 제공하는 라이브러리를 이용하도록 한다. 이 라이브러리에서 제공하는 메서드들은 JIT 등, 컴파일러에 최적화되어 있어서, 대부분의 경우 유저가 직접 구현한 방법보다 빠르거나 같다.

9.주의해서 Native 메서드를 이용하라

대부분의 경우 NDK를 사용할 필요가 없을 정도로 JAVA에서 최적화를 진행할 수 있다. 그러나, 메모리, 파일 디스크립터 등의 네이티브 자원을 할당해야 하거나, 아키텍쳐별로 컴파일을 따로 해줘야 할 필요가 있는 등의 이유가 있다면, NDK를 이용한 네이티브 코드를 이용하는 것도 고려해 볼 수 있다.

성능에 대한 오해

JIT가 없는 디바이스에서는, 인터페이스 타입의 변수를 선언하고 그 인터페이스의 메서드를 이용하는 것이 실제 타입의 변수를 선언하고 메서드를 이용하는 것보다 조금 느린 것이 사실이다.(약 6%) 그러나 JIT가 있는 디바이스에서는 성능차이가 없다. 그러므로 6%의 얼마안되는 성능차이를 커버하고자 OOP의 안티패턴인 실제타입 변수를 선언하는 것은 재고해볼 필요가 있다.

참고

댓글 없음:

댓글 쓰기