1. Friending the Machine:
How we did machine friendly software optimization
Sangwhan Moon <sangwhan@iki.fi> | Odd Concepts Inc.
2. 오드컨셉
영상 (사진, 동영상) 정보 검색 기술 전문 기업
현재 국내외 고객사들에게 엔진/SDK 라이센스 판매 모델
“딱 봐서 베낄 수 있는 기술은 만들어도 의미가 없다” 주의
서버 사이드 대량 검색 엔진 기술과 단말에서 전부 검색이 되
는 on-device 소량 검색 기술 보유
사내 개발인력 전원이 Cross Platform C/C++ 개발자
3. 고민
일반적으로 생각하는것보다 이미지는 훨씬 처리하는데 많은 자원이 들어감
“작은” 1024x768 이미지가 자그마치 786,432 픽셀이라는 사실
픽셀 하나가 일반적으로 3개 채널, 알파가 있으면 4개, 그리고 채널 하나가 1바이트임
해당 이미지를 처리하기 위해서 3백14만5천728픽셀에 대해서 뭔가를 해야하는 경우
가 많음
시장에서 경쟁력 있는 기술을 제공하기 위해서는 노드당 적어도 백만/천만장 단위로 처
리가 가능해야함
백만장 = 3,145,728,000,000 픽셀 = 9.435GBs (알파가 없고 오버헤드가 전혀 없다
고 가정할시, 단순 linear data)
…현실은 대부분 이미지는 1024x768보다 큼
4. 자주 범하는 실수
“개발자의 감”으로 접근
1% 개발자가 아니면 감을 믿지 말라.
이론상 Big O complexity가 낮은 알고리즘이 항상 현업 도입시 빠를것이라는
선입견
일치하지 않는 환경에서 측정 또는 최적화 작업 후의 측정을 진행한 다음 양산
빌드는 다른 환경으로 출시(작업시 -g, 출시할 때는 -O2)
컴파일러에서 나온 결과를 들여다보지 않고 최적화
문제가 되지 않는 코드의 최적화; 자원 소모가 일어나는 병목 코드가 아니거나
호출 빈도로 문제가 안되는 코드를 갖고 최적화
6. 무엇을 최적화 할까
함수 중에서…
명확하게 정의된 알고리즘을 구현한 함수 (유닛 테스트 또는 ground truth가 존재
하거나, 원론적으로 증명된)
자주 호출되는 함수
자주 안바뀌는 함수
SIMD나 스레드 없이 먼저 최적화를 진행하고, 그걸로 부족하
면 SIMD와 스레드를 사용하는게 수순
7. CPU 친화도 순위
Comparisons
(u)int add, subtract, bitwise operations, shift
FP add, sub
Indexed array access
(u)int32 mul, FP mul
FP division, remainder
(u)int division, remainder
Reproduced with permission. Copyright (c) Andrei Alexandrescu.
9. 측정툴
개인적으로 가장 좋아하는: Instruments (Shark의 후계자)
- 공교롭게도 Mac OS X 전용…
Intel Parallel Studio (VTune Amplifier)
Visual Studio’s Profiler
perf, OProfile, gprof, dtrace
등등: http://ocn.pt/WooK
10. 생성 코드 분석
컴파일러 출력 결과 분석
Interactive Disassembler (IDA) - Pro edition의 경우 흔하지 않은
아키텍쳐나 플랫폼 분석용으로는 현존 최강
Hopper
Ollydbg
binutils
(더 있음, 검색해볼 것)
11. Instruments + Coffee + Insomnia = Faster Code
부작용: 정신이 이상해지고, 친구가 줄며, 애인이 사라지는 수가 있다
12. 다른 언어는?
언어/VM이 신뢰도가 높은 측정 도구가 없다면 사용하는걸 다
시 한번 생각해볼 필요가 있음
위키피디아에 목록이 있다:
http://ocn.pt/WooK
최악의 경우“high precision timer [language_name]” 으로
검색해서 전통적인 (a.k.a. printf) 방법으로라도 측정은 가능
하다
단, 측정 코드 때문에 발생할 수 있는 측정 결과 오차에 대해서는 주의할 것!
13. 측정은 매우 중요. 기계가 코드를 실행시킬때 무엇
때문에 힘들어 하는지를 알기 위한 중요한 근거임.
14. 전통적인 기법
아래에 있는건 대부분 C/C++ 컴파일러에 플래그만 잘 쓰면 컴파일러가 해주는 것들
함수 호출시 발생하는 부가 비용을 생략하기 위한 manual inlining
코드를 결과적으로 동일하나 더 저렴한 instruction을 사용하게끔 유도하는 코드 -
예로 연산을 bitwise로 처리하는 방법 등이 있음
동적 타입 언어에서는 변수의 데이터형을 계속 같은것으로 유지하도록 하고, type
hinting 등을 도입
가능하면 최대한 float 대신 integer를 사용
Unrolling loops (짧은 loop의 경우)
16. 어떻게?
Manual inlining
연산을 가능한 건 전부 bitwise로 변환
typed arrays의 선택적 사용 (typed array가 무조건 빠르지만은 않다는 사실!)
변수의 데이터형을 코드 전반에 걸쳐서 일정하게 유지
Unrolling loops
branches 조건 간략화 및 branch 조건의 빈도에 따라 순서 재정렬
Typed array가 느린 자바스크립트 엔진은 과감하게 일반 array를 사용하도록 전환
그리고 가장 중요한 것: 최적화 하면서 지속적으로 측정
17. C/C++ 컴파일러
일반적으로 CPU 제조사의 컴파일러가 생성된 코드가 가장 효율적이고 빠름
단, 최신 표준 지원 등이 안좋거나 오픈소스 라이브러리 등이 제대로 테스트가 안된 경우가 많음
Intel: Intel C/C++ Compiler
ARM: RVCT
살 수 있다면 사용해볼만 함- 빌드가 되고 정상적으로 실행이 되면 일반적인 코드베
이스는 성능 향상이 있음
다만 일반적인 컴파일러들이라고 (msvc, gcc, clang) 심하게 느리지는 않음
업데이트가 한동안 없었거나 개발자/개발사에서 지원 중단한 컴파일러는 가급적 사
용하지 않는 편이 좋음
18. 컴파일러 플래그
가장 일반적인 방법은 우선 -O2 부터 시도
-O3 의 경우는 표준에 정의되지 않은 코드의 경우 엽기적인 버그들과
조우할 가능성이 매우 높아짐
…또는 그냥 컴파일러 optimizer의 버그
…아울러 -O2 보다 성능이 많이 나아지지 않고, 때로는 (꽤 자주) 느리기
까지 함
배포 대상이 확실하다면 CPU 기능을 (e.g. SSE4/AVX/AVX2) 활용할 수 있
도록 플래그를 설정해주는것도 좋은 방법
19. 템플릿
템플릿은 편하나, 간단한 generic을 직접 구현한것에 비해서 예
측이 굉장히 어려운 문제가 있음
“It took 69 single steps to get past a BOOST_FOREACH() statement.
Madness.” — John Carmack
단, STL의 경우는 컴파일러가 공격적으로 최적화하는 대상 중
하나이니 의외로 빠른 경우도…
컨테이너의 경우 사용하는 패턴이 확실하다면 직접 잘 짜면 어지
간한 generic보다 빠름 (물론, 잘 만든 경우.)
20. 알고리즘의 재설계
// Count the digits of input value v
uint32_t digits10(uint64_t v) {
uint32_t result = 0;
do {
++result;
v /= 10;
} while (v);
return result;
}
<- Expensive!
Reproduced with permission. Copyright (c) Andrei Alexandrescu.
22. 알고리즘의 재설계
// Count the digits of input value v
uint32_t digits10(uint64_t v) {
uint32_t result = 1;
for (;;) {
if (v < 10) return result;
if (v < 100) return result + 1;
if (v < 1000) return result + 2;
if (v < 10000) return result + 3;
v /= 10000U; // Jump four steps
result += 4;
}
return result;
}
Reproduced with permission. Copyright (c) Andrei Alexandrescu.
23. 테스트
Apple LLVM version 5.1 (clang-503.0.40) (based on
LLVM 3.4svn)
10,000,000 회 테스트
입력은 19 자리 정수
모든 테스트 1회 실행시 결과를 printf() 로 출력
(중요: 모든 테스트 환경이 일치해야 결과 비교가 용이함.)
25. 이건 아주 기본적인 사례지만, 같은 접근 방법을 여
러가지 알고리즘에 도입할 수 있으니 시도해볼것!
(다른 언어가 C/C++ 보다 효과가 좋을 수 있는데, 이건 optimizer가 엄청 공격적이기 때문임.)
26. 스레딩 - 팁
“느리니까 일단 스레드에 넣어야지”라는 생각이 제일 먼저 든다면 사고
방식의 전환이 필요함
여러 스레드에서 데이터를 공유하는 경우 발생할 수 있는 여러가지 문
제에 대해서 고려할 필요가 있음
locking 사용시 스레드 수가 증가함에 따라 lock 대기를 위해 쓰는
시간이 늘어나고, lock을 안할 경우에는 data race나 crash의 위험
성이 있음)
locking을 하지 않는다 하더라도, 여러 스레드에서 데이터를 접근하
게 될 경우 cache coherence 등으로 예상하지 못하는 성능 문제가
발생할 수 있음
27. 스레딩 - 팁
스레드로 처리할 후보로는 격리된 (순수) 함수가 state에 접근
이 필요한 함수 좋은 후보
처리 시간이 지나치게 짧지 않도록: threading overhead가
적지 않음
처리 시간이 지나치게 길지 않도록: caller가 결과를 기다려야
하는 상황이 발생한다면 처리 시간 때문에 다른 코드가 놀고
있어야 함
28. 그림으로 표현한 스레딩의 현실
wait ->
wait ->
<- wait
머리속에서 생각했을때 실제로 실행되었을때 모양
30. 스레딩 사례
자사 이미지 검색 엔진에서 사용한 접근
Incoming queue listener가 메인 스레드
격리된 사용자 쿼리를 신규 스레드로 배정
쿼리 스레드에서 처리 시간이 다소 긴 task 두개를 두개 스레드로 처리
Query -> Filter (2 threads) -> Analyze -> Quantize (2 threads) ->
Retrieval 1st pass -> Retrieval 2nd pass
Filter와 Quantize의 경우 시간이 예측이 가능했고 격리된 데이터로 처
리가 가능하여 병렬 처리를 하게 됨
31. 오늘 소개한 접근은 대부분 언어에서 적용이 가능하다.
정확히 말하면… “우리 코드 갖고 해보니 되더라.”
34. 들어주셔서 감사합니다.
질문은? sangwhan@iki.fi 또는 irc.freenode.net 에서 sangwhan
!
Special Thanks to Alessandro Gatti, Andrei Alexandrescu, Kim Nilsson, and Terje
Støback for inspiration, review, and material