////
Search
7️⃣

7장. 자기 주장이 뚜렷한 최적화

이 장에서 다룰 내용

섣부른 최적화 받아들이기
성능 문제에 하향식 접근 방식 취하기
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 같은 것을 다룰 수 있도록 하라.
올바른 버퍼링 메커니즘을 사용하여 입출력 성능을 향상시켜라.
스레드를 낭비하지 않고 코드와 입출력 작업을 병렬로 실행하기 위해 비동기식 프로그래밍을 사용하라.
비상시에는 캐시를 사용하라.