이 장에서 다룰 내용
•
섣부른 최적화 받아들이기
•
성능 문제에 하향식 접근 방식 취하기
•
CPU와 I/O 병목 현상 최적화하기
•
안전한 코드를 더 빠르게, 안전하지 않은 코드를 더 안전하게 만들기
•
최적화에 관해 대부분의 참고 문헌은 저명한 컴퓨터 과학자 도널드 커누스의 말을 인용하는 것으로 시작한다.
◦
“섣부른 최적화는 모든 악의 근원이다.”
•
이 말은 잘못 알려졌을 뿐만 아니라 항상 잘못 인용되고 있다.
◦
첫째, 모든 악의 근원은 객체지향 프로그래밍이다. 올바르지 못한 객체지향 프로그래밍은 여러 문제를 불러온다.
◦
둘째, 실제 인용문은 더 미묘했다. 커누스가 실제로 한 말은 다음과 같다.
▪
“우리는 작은 효율성을 잊어야 한다. 97%의 경우에 말이다. 섣부른 최적화는 모든 악의 근원이다. 그러나 중요한 3%의 기회를 놓쳐서는 안 된다.”
•
저자는 섣부른 최적화를 오히려 학습의 기회라고 얘기한다.
1. 올바른 문제를 해결하라
•
성능 문제의 진정한 본질을 이해하기 위한 첫 번째 단계는 먼저 성능 문제가 있는지를 판단하는 것이다.
1. 단순한 벤치마킹
•
벤치마킹은 성능 측정을 위한 여러 지표를 서로 비교하는 활동을 말한다.
•
성능 문제의 근본적인 원인을 파악하는 데는 도움이 안 될 수 있지만, 성능 문제가 존재한다는 것을 식별하는 데는 도움이 될 수 있다.
2. 성능 대 응답성
•
성능이 곧 응답성을 의미하는 것은 아니다. 둘이 정비례 관계에 있지 않음을 말하는 것이다.
•
얘기하고 싶은 바는, 벤치마크의 결과인 성능 지표를 가지고 섣불리 문제가 있다고 단언해서는 안 된다는 것이다.
2. 완만함의 분석
•
일반적으로 속도와 관련된 모든 성능 문제는 명령어가 몇 개 실행되느냐에 따라 달라진다.
•
코드를 최적화하고 싶다면 실행되는 명령어의 수를 줄이거나 더 빠른 명령어를 사용하고자 시도하라.
3. 최고부터 시작하라
•
실행하는 명령어 수를 줄이는 두 번째로 좋은 방법은 더 빠른 알고리즘을 채택하는 것이다.
◦
첫 번째로 좋은 방법은 코드를 완전히 삭제하는 것이다. 무턱대고 지우라는 뜻은 아니다. 필요없는 코드는 삭제하라는 뜻이다.
•
최고부터 시작하라는 의미는, 가장 추상화된 단계부터 구체적으로 나아가며 문제를 파악하라는 의미다.
1. 중첩 루프
•
코드 속도를 늦추는 가장 쉬운 방법 중 하나는 코드를 루프 안에 위치시키는 것이다.
◦
우리는 단순히 몇 글자로 표현되는 반복문의 무서움을 과소평가하고는 한다.
2. 문자열 지향 프로그래밍
•
불필요하게 문자열을 사용하는 일반적인 방법 중 하나는 모든 컬렉션을 문자열 컬렉션으로 가정하는 것이다.
•
이 경우, 불필요한 타입캐스팅이 발생하며 오버헤드나 오타를 피할 수 없게 될 수 있다.
3. 2b || !2b 평가하기
•
if 문의 불리언 표현식은 보통 작성된 순서대로 평가된다.
◦
단축회로 평가 역시, 이런 사례를 지원한다.
•
불리언 평가를 최적화하는 동안은 기존의 연산을 유지하며 더 적은 입력값을 테스트하는 평가문을 앞에 위치시키자.
4. 병목 현상 깨뜨리기
•
소프트웨어의 지연에는 CPU, I/O, 사람이라는 세 가지 유형이 있다. 보통은 이 연결부에서의 병목이 문제를 발생시킨다.
•
그렇지 않다면 코드 자체를 최적화할 수 있는 방법으로 넘어갈 수 있다. 그러기 위해 최적화 옵션을 평가하려면 CPU가 제공하는 유용한 기능을 알고 있어야 한다.
1. 데이터를 패킹하지 마라
•
CPU가 정렬되지 않은 메모리 주소에서 데이터를 읽는 경우, 즉, 랜덤 액세스를 하는 경우 속도 이슈가 발생할 수 있다.
CPU 워드 크기
•
워드의 단위는 보통 CPU가 한 번에 처리할 수 있는 데이터의 양으로 정의된다. 즉, 워드 크기는 대부분 CPU의 가산기 레지스터(EAX, RAX)의 크기를 반영한다.
•
여기서 말하고자 하는 바는 메모리 공간을 최적화하려는 시도가 불필요할 경우, 피하라는 것이다.
•
메모리 공간을 최적화하려는 시도는, 데이터 정렬을 방해할 수 있고, 이는 스토리지 공간 확보를 위해 접근 시간을 희생시키는 결과가 될 수 있다.
10억 자리의 데이터를 저장하기 위해 이진 자료형과 숫자 자료형 중, 하나를 선택해야할 때, 메모리 공간 최적화를 위해 바이트를 사용한다면 숫자 자료형에 비해 접근 속도가 더 오래걸릴 수 있다.
2. 근접성을 활용하라
•
캐싱이란 보통 자주 사용하는 데이터를 일반적인 기억장치보다 더 빠르게 접근 가능한 위치에 위치시키는 것을 말한다.
•
배열이나 연결 리스트, 해쉬 맵을 구현으로 사용할 때, 데이터의 접근성을 생각해 CPU가 더 빠르게 데이터를 조회할 수 있는 방법을 채택하자.
3. 종속 작업을 세분화하라
•
단일 CPU 명령어는 프로세서의 개별 유닛에 의해 처리된다.
◦
예를 들어 한 유닛이 명령어의 디코딩을 담당한다면 다른 유닛은 메모리 액세스를 담당한다.
•
그러가 각 유닛은 서로의 작업을 동기적으로 수행하지 않고 병렬적으로 처리할 수 있는데, 이를 파이프라이닝이라고 한다.
◦
이는 다음 명령어가 이전 명령의 결과에 의존적이지 않는 한, 여러 명령어가 병렬로 실행될 수 있다는 것을 의미4. ㅇ한다.
4. 예측할 수 있도록 하라
•
스택 오버플로 역사상 가장 유명한 질문은 왜 정렬되지 않은 배열을 처리하는 것보다 정렬된 배열을 처리하는 것이 더 빠를까?이다.
◦
실행 시간을 최적화하기 위해 CPU는 실행 코드보다 선제적으로 움직여 필요하기 전에 미리 준비한다. 이럴 때 CPU가 사용하는 기술을 분기 예측이라고 한다.
•
CPU가 예측을 할 수 있도록 코드를 작성하는 것이 좋다. CPU가 순서와 분기를 예측 불가능하게 코드를 작성하면 할 수록 성능에 악영향이 간다.
5. SIMD
•
CPU는 단일 명령어로 여러 데이터에 대한 연산을 동시에 실행할 수 있는 특수한 명령어를 지원하는데, 이 기술을 단일 명령어 다중 데이터(Single Instruction Multiple Data, SIMD)라고 한다.
◦
이는 여러 변수에 동일한 연산을 수행하는 경우, 이를 지원하는 아키텍처에서 성능을 크게 향상시킬 수 있다.
5. 1초와 0초의 I/O(입출력)
•
I/O는 CPU가 디스크, 네트워크 어댑터, 심지어 GPU와 같은 주변 하드웨어와 통신하는 모든 것을 포함한다.
◦
I/O는 보통 성능 체인에게 가장 느린 링크이다.
1. I/O 속도 향상
•
많은 I/O 장치가 블록 단위로 읽고 쓰기 때문에 블록 장치라고 부르는데, 키보드는 한 번에 문제 하나를 보내기 때문에 문자 장치이다.
◦
블록 장치는 블록 크기보다 작게 읽을 수 없으므로 일반적인 블록 크기보다 작은 것을 읽는 것은 의미가 없다.
•
최적의 버퍼 크기를 찾고 필요 이상의 메모리를 할당하지 않는 것이 좋다.
2. I/O를 논 블로킹으로 만들어라
•
비동기 I/O를 멀티스레딩과 혼동하는 사례가 프로그래밍 세계에선 잦다.
•
비동기 I/O는 I/O 부하가 높은 작업만을 위한 병렬화 모델로 단일 코어에서 작동할 수 있다.
•
즉, 병렬화를 구현한 다양한 관점의 모델들이라고 생각하면 이해가 쉬울 것이다.
◦
멀티스레딩을 이용해 병렬화를 구현하느냐, 비동기 I/O를 이용해 병렬화를 구현하느냐
3. 오래된 방법
•
오래 전에는 콜백 함수를 이용해서 비동기 이후의 동작들을 결정하곤 했다.
4. 최신 비동기/대기
•
마이크로소프트의 뛰어난 설계자들은 비동기(async)/대기(await) 의미론을 사용하며 비동기 I/O 코드를 작성하는 훌륭한 방법을 발견했다.
•
async/await의 사용으로 엄청난 수고를 덜 수 있게 되었다.
5. 비동기 I/O의 잠재적 문제
•
프로그래밍 언어에서 입출력에만 비동기 메커니즘을 사용할 필요는 없다.
◦
즉, 입출력과 관련된 호출 없이 async 함수를 선언하고 CPU 작업만 수행할 수 있다. 이 경우, 아무런 이점도 없이 불필요하고 복잡하게만 만들 뿐이다.
•
async/await을 사용하게 될대 기억해야 하는 원칙 중 하나는 await은 기다리지 않는다는 것이다.
•
await은 물론 실행이 분명히 완료된 다음에야 다음 줄을 실행하나. 하지만 내부의 비동기식 콜백 함수로 기다리거나 차단없이 바로 실행한다.
•
만약 비동기 코드에서 무언가 완료될 때까지 계속 기다린다면 그건 잘못된 것이다.
6. 다른 모든 것이 실패할 경우, 캐시를 이용하라
•
캐싱은 성능을 즉시 향상시킬 수 있는 가장 확실한 방법 중 하나다.
◦
캐싱을 위해 설계되지 않은 데이터 구조는 사용하지 마라. 보통 오래된 데이터를 제거하거나 만료하는 메커니즘이 없으므로 메모리 누수가 발생하고 문제가 발생한다.
◦
데이터베이스나 Redis(Remote Dictionary Server)는 훌륭한 캐시 저장소 역할을 수행해준다.
7. 요약
•
이른 최적화를 연습하고 그것을 통해 학습하라.
•
불필요한 최적화로 스스로를 어려움에 빠뜨리지 마라.
•
항상 벤치마킹으로 최적화를 검증하라.
•
최적화와 대응성 사이의 균형을 유지하라.
•
중첩 루프, 문자열이 많은 코드, 비효율적인 불리언 표현식과 같이 문제가 있는 코드를 식별하는 습관을 가져라.
•
데이터 구조를 구축할 때는 더 나은 성능을 얻기 위해 메모리 정렬의 장점을 생각하라.
•
마이크로 최적화가 필요한 경우 CPU의 동작을 파악하고 캐시 지역성, 파이프라이닝, SIMD 같은 것을 다룰 수 있도록 하라.
•
올바른 버퍼링 메커니즘을 사용하여 입출력 성능을 향상시켜라.
•
스레드를 낭비하지 않고 코드와 입출력 작업을 병렬로 실행하기 위해 비동기식 프로그래밍을 사용하라.
•
비상시에는 캐시를 사용하라.