2020년 5월 28일 목요일

C++ 최적화 책, 4장 문자열 최적화


C++ 최적화 : 최고 성능을 구현하는 10가지 검증된 기법 
커트 건서로스 저/옥찬호 역/박수현 감수 | 한빛미디어

책을 보고 간략히 정리하려고 함.
오랜만에 보는 괜찮은 c++ 언어 서적이다.
"Effective Modern C++" 을 보다가 내용과 번역에 질려버리고 
"모던 C++ 입문" 을 봤었는데 그 책의 저자가 이 책을 번역하였음.

책의 전반적인 내용은 최적화라는 관점에서 가능한 모든 방법을 소개하는 것 같다.
최적화의 방법이 정도가 없어 일반적인 내용, 저자의 경험과 테스트를 통해서 알게 된 내용들을 소개하고 있다. 
C++ 문법, skill을 가르치는 것은 아니고 효율적인 코드를 작성하기 위해 알아야 하는 내용과 방법을 제시한다.



1장 최적화란

다음 권고 사항들로 정리할 수 있어 보인다.
- 더 좋은 컴파일러를 사용하고 최적화 설정을 사용하세요.
- 최적의 알고리즘을 사용하세요.
- 더 좋은 라이브러리를 더 잘 사용하세요.
- 메모리 할당을 줄이세요.
- 복사를 줄이세요.
- 계산을 제거하세요.
- 최적의 자료구조를 사용하세요.
- 동시성을 증가시키세요.
- 메모리 관리를 최적화하세요.

책의 목차를 보면 알겠지만 위 내용을 책에서 나눠 설명하고 있고
각 내용을 간략히 정리하는 챕터이다. c++ 언어 특성과 관련이 있는 팁도 있지만, 프로그래밍 시 일반적으로 알아야 하고 적용할 수 있는 조언이 대부분이라 유익하다.



4장 문자열 최적화

(오라일리에서 제공하는 online book 페이지입니다.
아래 내용, 코드는 모드 아래 출처에서 발췌하였다.)

std::string은 저장소를 동적으로 할당합니다.
문자열이 추가되면서 문자열보다 저장소가 훨씬 크게 할당할 수 있어 메모리가 낭비될 수 있다고 합니다.
문자열은 값처럼 동작하여 문자열 사용 시 복사가 많이 일어날 수밖에 없다고 합니다. 

책에서는 아래 함수를 예시로 최적화를 진행합니다.

std::string remove_ctrl(std::string s) {
    std::string result;
    for (int i=0; i<s.length(); ++i) {
        if (s[i] >= 0x20)
            result = result + s[i];
    }
    return result; 
} 

그냥 봐도 수정할 것이 많아 보임...

(임시 문자열 객체 생성 제거) 
result = result + s[i]  에서 
s[i]를 더한 새로운 result를 저장하기 위해 
생성된 임시 문자열을 
  result += s[i];
로 변경하여 임시 문자열이 생성되지 않게 하여 성능을 개선합니다.

(문자열 저장 공간 예약)
result에 저장하는 문자열은 최대 s의 크기이므로
result.reserve(s.length())를 호출하여
미리 저장 공간을 예약하여 
result 값이 증가함에 따라 저장 공간 확보 시도를 줄여 성능을 개선합니다. 

(함수 인자 복사를 제거)
std::string s로 넘겨지는 인자의 복사(pass by value)를 제거하기 위해
std::string const& s
로 변경하여 인자를 레퍼런스(pass by reference)로 받아
불필요한 임시객체 생성&복사를 방지하여 성능을 개선하려고 하였지만
s의 포인터 역참조(포인터의 값을 사용하기 위해 포인터가 가리키는 주소를 접근)로 인해 성능이 저하 되었습니다. 

std::string remove_ctrl_ref_args(std::string const& s) {
    std::string result;
    result.reserve(s.length());
    for (int i=0; i<s.length(); ++i) {
        if (s[i] >= 0x20)
            result += s[i];
    }
    return result; 
} 


(반복자로 포인터 역참조 제거)
역참조를 제거 하기 위해 s[i] 를 
iterator로 사용하여 성능을 개선할 수 있었습니다.
함께 loop을 반복할 때 마다 종료 조건에서의 간접 참조 비용을 줄이기 위해 
it != s.end() 대신 s.end()를 캐싱하여 it != end로 변경하여 성능을 개선합니다. 
  
std::string remove_ctrl_ref_args_it(std::string const& s) {
    std::string result;
    result.reserve(s.length());
    for (auto it=s.begin(),end=s.end(); it != end; ++it) {
        if (*it >= 0x20)
            result += *it;
    }
    return result; 
} 


(반환된 문자열 값의 복사 제거하기)
반환되는 std::string이 복사되게 되어
이를 생략하기 위해 함수의 참조 인자(std::string& result)로 변경하여 성능을 개선합니다.

마지막으로 속도가 중요한 경우에 사용할 수 있는
C 스타일 문자열을 사용하여 성능을 개선합니다. 

책에서는 성능 요약을 표로 정리했는데 릴리즈 빌드로 24.8 마이크로 걸리던 코드를 1.02 마이크로초로 줄이고 c 스타일 문자열을 사용하여 0.15 마이크로초까지 성능을 개선했다.

그 외 성능 개선을 위한 다른 방법들을 설명하고 있고  
다음의 방법으로 개선을 시도할 수 있다고 말하고 있음. 
- 더 좋은 알고리즘 사용
- 저 좋은 컴파일러 사용
- 더 좋은 문자열 라이브러리 사용
- 더 좋은 할당자 사용


2020년 5월 5일 화요일

Android Room을 사용하여 DB에 데이터 저장

일반적으로 android에서 app을 만들 때 DB 사용은 필요함.

기존에는 SQLite를 사용하여 data class를 읽고 쓰고 할 수 있는 Generic DB access hander 같은 것을 만들었는데 이제는 Android에서 이를 지원한다.

Android Jetpack에서 Room이 여기에 해당되며
SQL의 Abstraction layer로서 쉽게 DB를 사용 가능하도록 해준다.
단 코드 기반 Room class 생성으로 로직 수정 시 빌드가 필요하다.

Room 개요

https://developer.android.com/training/data-storage/room


프로젝트 설정

https://developer.android.com/jetpack/androidx/releases/room#declaring_dependencies

주의해야 할 것이 있는데 kotlin을 사용한다면 kapt를 사용해야 하고 kotlin-kapt 플러그인을 추가해야 함. 종속성 예시 아래에 별표로 알려주고 있는데 이를 미처 보지 못해서 실행 중 죽는 현상이 발생하여 원인 찾느라 고생함.

build.gradle의 상단에
apply plugin: 'kotlin-kapt'

를 명시하고 dependencies예제를 추가하되 annotationProcessor 대신 kapt를 사용함.

 def room_version = "2.2.3"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor

kotlin에서 수정없이 사용하면 아래 에러 발생됨.

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: X.X.X, PID: 13160
    java.lang.RuntimeException: cannot find implementation for X.X.X.MyDatabase. MyDatabase_Impl does not exist
        at androidx.room.Room.getGeneratedImplementation(Room.java:94)
        at androidx.room.RoomDatabase$Builder.build(RoomDatabase.java:952)


구현

Room을 사용하여 로컬 데이터베이스에 데이터 저장
https://developer.android.com/training/data-storage/room
를 따라하면 쉽게 구현할 수 있음.

주요 annotation은
@Entity - DB에 저장할 data class/object
@Dao - DB를 사용 시 필요한 interface 정의
@Database - DB class, DB class 인스턴스화 시 DB가 생성되므로 아래 가이드 처럼 Singleton pattern class로 만들어야 함.

참고: 앱이 단일 프로세스에서 실행되는 경우 AppDatabase 개체를 인스턴스화할 때 싱글톤 디자인 패턴을 따라야 합니다. 각 RoomDatabase 인스턴스는 리소스를 상당히 많이 소비하며 단일 프로세스 내에서 여러 인스턴스에 액세스할 필요가 거의 없습니다.

비동기 호출

Room 사용 시 DB transaction이 꽤 시간이 소요 되므로
UI thread에서 직접 호출할 수 없어 비동기적 방법을 사용해야 함.
기존 Android에서는 AsyncTask 같은 것을 사용했었지만 Coroutine을 사용할 수 있음.

https://kotlinlang.org/docs/reference/coroutines-overview.html
https://developer.android.com/topic/libraries/architecture/coroutines


아래 article도 참고할 만함.
https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5



2020년 4월 20일 월요일

생체 인증을 위한 CheckboxPreference 만들기

개인적으로 만드는 app에서 생체 인증을 사용하고자 하여 
생체인증 enable/disable을 위한 CheckboxPreference로 옵션을 만들고 
옵션을 변경할 때마다 생체 인증을 하도록 간략히 만들어 봄

사실 너무 간단한 내용이라 안드로이드 가이드만 봐도 충분하다. 


내가 구현하고자 했던 것은
- 생체 인증을 enable/disable 하는 옵션
- 생체 인증을 통해서만 옵션을 enable<->disable 할 수 있음
- 생체 인증 실패 시에는 옵션 변경 없고 에러 발생 시 토스트 표시


안드로이드 생체인증 가이드를 기초하여 구현을 간략히 설명하면

1. 먼저 생체 인증을 위한 jetpack 라이브러리를 사용하고자 종속성을 추가한다.
 : 현재는 1.0.1 이다. 항상 Jetpack 라이브러리의 최신 버전을 확인해야 할 필요 있음.
dependencies {
        implementation 'androidx.biometric:biometric:1.0.1'
    }

2. app 설정에서 CheckboxPreference 추가

<CheckBoxPreference app:iconSpaceReserved="false"
 android:defaultValue="false"  
 android:key="pref_enableBiometricSignIn"   android:summary="@string/settings_summary_entry_enable_biometric_sign_in" 
 android:title="@string/settings_entry_enable_biometric_sign_in" />


3. CheckboxPreference의 onPreferenceChangeListener에서 BiometricPrompt 표시

val biometricCheckBox = findPreference("pref_enableBiometricSignIn") as CheckBoxPreference biometricCheckBox.onPreferenceChangeListener = OnPreferenceChangeListener { preference, newValue ->

 val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Biometric login for app")
    .setSubtitle("Log in using your biometric credential")
    .setNegativeButtonText("Cancel")
    .build()
 val toBe = newValue as Boolean
 val biometricPrompt = BiometricPrompt(this, Executors.newSingleThreadExecutor(),
  object: BiometricPrompt.AuthenticationCallback() {
 
  override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
    super.onAuthenticationError(errorCode, errString) activity ? .runOnUiThread {
     Toast.makeText(activity ? .applicationContext, errString, Toast.LENGTH_SHORT).show()
    }
   }

   override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
    super.onAuthenticationSucceeded(result) activity ? .runOnUiThread {
     (preference as CheckBoxPreference).isChecked = toBe
    }
   }
  }) 

 biometricPrompt.authenticate(promptInfo) 
 false
}


안드로이드 가이드 코드를 거의 그대로 사용하였고 원하는 기능을 위해 아래 사항을 살짝 수정함

- 생체 인증에 성공 시에만 옵션의 check status를 변경하고자 OnPreferenceChangeListener 구현
 : 생체 인증 시에만 check status를 변경하고자 OnPreferenceChangeListener의 리턴값을 false로 전달
 : 생체인증 성공시에만 isChecked 값을 OnPreferenceChangeListener의 argument인 newValue로 설정

- 사용자가 옵션을 변경할 때마다 생체 인증 창을 띄우기 위해 Executors.newSingleThreadExecutor() 로 Executor 매번 생성
 : 솔직히 이게 맞는 것인지는 모르겠음 ㅜㅜ

- 생체 인증 중 지문이 틀릴 때마다 호출되는 onAuthenticationFailed() 를 override하지 않고 onAuthenticationError() 만 override하여 사용자가 Cancel 하였을 때만 처리함 
 : 사용자가 생체 인증을 여러 번 틀려 block될 경우 BiometricPrompt에 안내가 표시되고 OnAuthenticationError()가 호출되기만 하고 사용자가 직접 cancel을 눌러야함.
 : block된 상황에서는 BiometricPrompt를 닫고 다시 옵션을 누르더라도 생체 인증창이 뜨지 않고 OnAuthenticationFailed()가 호출됨.