Chapter 3. 하드웨어와 운영체제
무어의 법칙 - 대량 생산한 칩상의 트랜지스터의 수는 약 18개월마다 2배씩 증가한다. 즉 시간이 흐르면서 컴퓨터 파워가 기하급수적으로 증가하는 현상을 나타낸 말
3.1 최신 하드웨어 소개
- 1990년대 이후 애플리케이션 개발자 세상은 대부분 인텔 x86/x64 아키텍처 위주로 돌아갔다.
- 프로세서 작동 원리를 단순화한 멘탈 모델은 전혀 맞지 않게 되었다.
3.2 메모리
- 급증한 트랜지스터는 처음에 CPU 클록 속도를 높이는 데 쓰였다.
- Clock Speed: 컴퓨터 프로세서의 동작 속도. “초당 주기”로 측정하며 헤르츠(Hz) 단위를 사용
- 클록 속도가 증가하면 초당 많은 명령어를 처리 가능
- 하지만 메인 메모리가 프로세서 코어의 데이터 수요를 따라가기 버거워졌다.
- 프로세서/메모리 간 성능 차이가 커짐
- 결국 데이터가 도착할 때까지 기다려야 하기에 CPU 사용률이 낮아진다.
3.2.1 메모리 캐시
캐시 일관성 프로토콜 (cache consistency protocol
- MESI 프로토콜: 메모리의 데이터와 캐시 데이터의 일관성을 해결하기 위한 프로토콜
- MESI 프로토콜은 캐시 라인 상태를 다음 네 가지로 정의한다.
- Modified (수정): 데이터가 수정된 상태
- Exclusive (베타): 이 캐시에만 존재하고 메인 메모리 내용과 동일한 상태
- Shared (공유): 둘 이상의 캐시에 존재하고 메모리 내용과 동일한 상태
- Invalid (무효): 다른 프로세스가 데이터를 수정하여 무효한 상태
- 멀티 프로세서가 동시에 공유 상태에 있을 수 있지만 한 프로세서가 베타나 수정 상태로 바뀌면 다른 모든 프로세서는 강제로 무효 상태가 된다.
- MESI 프로토콜에서 프로세서가 상태를 바꾸겠다는 의사를 브로드캐스팅 한다. (공유 메모리 버스를 통해)
- 캐시, 메모리 기록 방법
- write-through: 매번 캐시 연산 결과를 바로 메모리에 기록, 효율이 낮아 요즘 거의 안 씀
- write-back: 캐시 블록을 교체해도 프로세서가 변경된 캐시 블록만 메모리에 기록
3.3 최신 프로세서의 특징
메모리 캐시 말고도 최신 프로세서에 다양한 기술이 등장했다.
3.3.1 변환 색인 버퍼 (TLB)
- TLB(Translation Lookaside Buffer)는 가상 메모리 주소를 물리 메모리 주소로 매핑하는 페이지 테이블 캐시 역할을 수행
- 가상 주소를 참조해 물리 주소에 엑세스하는 작업 속도 향상
- TLB가 없다면 L1 캐시에 데이터가 있어도 가상 주소 룩업에 16 사이클이나 걸려 성능이 나오지 않는다.
3.3.2 분기 에측과 추측 실행
- 프로세서는 다단계 명령 파이프라인을 이용해 CPU 1사이클로 여러 단계로 나누어 실행
- 이 모델에서 조건문을 다 평가하기 전까지 분기 이후 다음 명령을 알 수 없는 문제가 있음
- 분기문 뒤의 다단계 파이프라인을 비우는 동안 프로세서는 멎게 된다.
- 분기 예측을 통해 프로세서가 조건 분기 하는 기준값을 평가하느라 대기하는 현상을 방지
- 프로세서는 발생 가능성이 가장 큰 브랜치를 미리 결정하는 휴리스틱을 형성
- 예측이 맞다면 CPU는 다음 작업을 진행
- 예측이 틀리면 실행한 명령을 모두 폐기하고 파이프라인을 비우는 대가를 치른다.
3.3.3 하드웨어 메모리 모델
- JIT 컴파일러인 javac와 CPU는 일반적으로 코드 실행 순서를 바꿀 수 있다.
- 순서를 바꿔도 결과에 영향이 없다는 전제가 필요
- 코드 실행 순서 변경에 의해 다른 스레드 입장에서 부정확한 값을 읽을 수도 있다.
- JMM은 명시적으로 약한 메모리 모델로 설계되었다.
- 프로세서 타입별 상이한 메모리 엑세스 일관성을 고려
- 멀티스레드 코드가 제대로 작동하려면 락과
volatile
을 정확히 알고 사용해야 한다.
3.4 운영체제
- OS의 주 임무는 프로세스가 공유하는 리소스 엑세스를 관장하는 일
- 메모리 관리 유닛(Memory Management Unit, MMU)을 통한 가상 주소 방식과 페이지 테이블은 메모리 엑세스 제어의 핵심
- TLB는 물리 메모리 주소 룩업 시간을 줄이는 하드웨어 기능
3.4.1 스케줄러
- 최신 시스템은 항상 가능한 수준보다 더 많은 스레드/프로세스로 가득하기에 CPU 경합을 해소해야만 한다.
- 프로세스 스케줄러는 CPU 엑세스를 통제
- 스케줄러는 인터럽트에 응답하고 CPU 코어 엑세스를 관리
- 실행 큐(run queue): CPU 차례를 기다리는 스레드 혹은 프로세스 대기 큐
- OS는 특성상 CPU에서 코드가 실행되지 않는 시간을 유발한다.
- I/O 또는 소프트웨어 락에 걸려 블로킹되는 경우
- 스케줄러 움직임을 확인하는 가장 쉬운 방법은 오버헤드를 관측하는 것이다.
- OS마다 오버헤드는 천차만별
- 1밀리초씩 총 1000회 스레드를 재우는 아래 코드에서 총 실행 시간을 보면 오버헤드를 짐잘할 수 있다.
- 스레드를 재우면 실행 큐 맨 뒤로 가고 새로 시간을 할당받을 때까지 기다린다.
long start = System.currentTimeMills();
for (int i = 0; i < 1000; i++) {
Thread.sleep(1);
}
long end = System.currentTimeMills();
System.out.println(end - start)
// 만약 위 코드 실행 시간이 2.8초라면 오버헤드는 180%
// 순수 코드 실행 시간은 1000밀리초, 즉 1초지만 1.8초에 오버헤드가 발생
3.4.2 시간 문제
- POSIX(portable operating system interface, 이식 가능 운영체제 인터페이스) 같은 업계 표준이 있어도 OS 동작 방식은 상이
os::javaTimeMills()
의 경우 OS가 제공하는 기능이기에 네이티브 메서드로 구현하는데 OS마다 다르게 구현된다.
3.4.3 컨텍스트 교환
- Context Switch: OS 스케줄러가 현재 실행 중인 스레드/태스크를 없애고 대기 중인 다른 스레도/태스크로 전환하는 프로세스
- 스레드 실행 명령과 스택 상태를 교체하는 모든 일에 연관
- 컨텍스트 교환은 비싼 작업
- 유저 스레드 사이에서의 교환보다 유저 모드에서 커널 모드로 바뀌면서 일어나는 교환이 더 비싸다
- 유저 공간에 있는 코드가 접근하는 메모리 영역은 커널 코드와 공유할 부분이 없기에 캐시를 강제로 비워야 하기 때문
- 커널 모드 컨텍스트 교환이 일어나면 TLB를 비롯한 다른 캐시까지 무효화된다.
- 커널 코드 교환 여파는 후에 유저 모드로 제어권이 넘어가도 당분간 이어진다.
- 가상 동적 공유 객체(virtual Dynamically Sahred Object)
- 리눅스가 커널 코드 컨텍스트 교환의 오버헤드를 만회하기 위해 제공
- 커널 프리빌리지(특권)이 필요 없는 시스템 콜 속도를 높이려고 사용하는 유저 공간의 메모리 영역
- ex) 부수 효과를 일으키지 않는 클록 시간을 얻는 시스템 콜의 경우 프리빌리지드 엑세스가 필요 없으므로 이 때 사용하는 자료 구조를 vDSO로 매핑한다면 커널 모드로 변경할 이유가 없다. (
gettimeofday(
)
- 타이밍 자료를 빈번히 반환하는 자바에선 이런 식으로 성능을 올릴 수 있다.
3.5 단순 시스템 모델
- 단순한 시스템 모델을 통해 성능 문제의 근원을 알아보자.
- 애플리케이션이 실행되는 하드웨어와 OS
- 애플리케이션이 실행되는 JVM/컨테이너
- 애플리케이션 코드 자체
- 애플리케이션이 호출되는 외부 시스템
- 애플리케이션으로 유입되는 트래픽
3.6 기본 감지 전략
- 애플리케이션이 잘 돌아간다는 것은 시스템 리소스를 효율적으로 잘 이용하고 있다는 뜻
- CPU 사용량, 메모리, 네트워크, I/O 대역폭 등
- 하나 이상의 리소스를 소진하면 바로 성능 문제가 발생한다.
- 성능 진단의 첫 단추는 어느 리소스가 한계인지를 밝히는 일
3.6.1 CPU 사용률
- CPU 사용률은 성능의 핵심 지표로써 사용률이 가능한 한 100%에 가까워야 한다.
- CPU-bound 프로그램은 CPU 사용률은 100%에 가깝에 유지하는 것이 목표
- CPU 사용률이 낮게 나타나는 경우
- 잦은 컨텍스트 교환
- I/O에서 블로킹이 발생
- 스레드 락 경합 발생
3.6.2 가비지 수집
- 핫스팟 JVM은 시작 시 메모리를 유저 공간에 할당/관리한다.
- 메모리 할당을 위해 시스템 콜, 즉 커널 교환을 할 필요가 없다.
- GC 자체가 커널 공간 사용률에 영향을 거의 주지 않기에 GC는 CPU 사용률의 주범은 아니다.
- 반면어떤 JVM 프로세스가 유저 공간에서 CPU를 100%에 가깝게 사용하고 있다면 GC를 의심해야 한다.
- GC 로그를 확인하고 CPU를 차지하는 게 JVM인지 유저 코드인지 확인해봐야 한다.
- GC 로깅 비용은 거의 공짜
3.6.3 입출력
- 파일 I/O는 전체 시스템 성능에 많은 악영향을 준다.
- I/O는 다른 OS 파트처럼 개발자가 적절히 추상화할 장치가 없기 때문
- 메모리 분야는 가상 메모리라는 격리 장치가 존재
- 다행히 자바 프로그램은 대부분 단순한 I/O만 처리한다.
- I/O 서브시스템을 심하게 가동하거나 CPU, 메모리 어느 한쪽과 I/O를 동시에 고갈시키는 애플리케이션은 거의 ㅇ벗다.
- 성능 분석 시 애플리케이션에서 I/O가 어떻게 일어나는지 인지하는 것으로 충분
3.6.4 기계 공감
기계 공감이란 성능을 쥐어짜기 위해 하드웨어를 폭넓게 이해하고 공감할 수 있는 능력이 무엇보다 중요하다는 생각
- 자바 개발자 또한 자바/JVM을 효과적으로 활용하려면 JVM과 하드웨어와의 상호작용을 이해할 수 있어야 한다.
- ex) 캐시 라인 동작
- 캐시 라인을 통해 메모리 블록을 미리 가져올 수 있다.
- 다만 멀티스레드 환경에서 두 스레드가 동일한 캐시 라인의 변수를 읽고 쓸 때 문제가 된다.
- 이러한 경합 상황은 성능 급락으로 귀결된다.
3.7 가상화
- 가상화란 이미 실행 중인 호스트 OS 위에 게스트 OS를 하나의 프로세스로 실행시키는 모양새가 보통이다.
- 가상화의 특징
- 가상화 OS에서 실행하는 프로그램은 베어 메탈(비가상화 OS)에서 실행할 때와 동일해게 작동해야 한다.
- 하이퍼바이저는 모든 하드웨어 리소스 엑세스를 조정해야 한다.
- 가상화 오버헤드는 가급적 적어야 하며 실행 시간 상당 부분을 차지해선 안 된다.
- 가상화 환경에선 프리빌리지드 명령어를 언프리빌리지드 명령어로 고쳐 쓴다.
- 일반 비가상화 시스템에선 OS 커널이 프리빌리지드 모드로 동작하기에 하드웨어를 건드릴 수 있지만 가상화 시스템에선 하드웨어에 직접 엑세스 불가능하다.
- 가상화 환경에서 컨텍스트 교환 발생 시 지나친 캐시 플러시(TLB)가 일어나지 않도록 일부 OS 커널 자료 구조는 shadow해야 한다.
- 가상 환경 내에서는 성능 분석 및 튜닝이 더 복잡해진다.
3.8 JVM과 운영체제
- JVM은 자바 코드에 공용 인터페이스를 제공하여 OS 독립적 실행 환경을 제공한다.
- native 키워드를 붙인 네이티브 메서드도 존재한다.
- 스레드 스케줄링 같은 서비스는 하부 OS에 엑세스가 필요하기 때문
- 네이티브 메서드는 C 언어로 작성
- 자바 네이티브 인터페이스(JNI)가 네이티브 메서드를 여느 자바 메서드처럼 엑세스할 수 있게 한다.
- 즉 네이티브 메서드란 비교적 저수준 플랫폼 관심사를 처리한다.