프로세스와 스레드, 그리고 갈아 끼우는 값
Esc

    프로세스와 스레드, 그리고 갈아 끼우는 값

    –OS

    CPU 는 한 번에 하나만 실행합니다. 코어가 여덟 개면 여덟 개입니다. 그런데 지금 기기에서 도는 스레드는 수백 개입니다. 그래서 OS 가 번갈아 태웁니다.

    번갈아 태우려면 지금 돌던 것의 상태를 어딘가에 적어 두고, 다음 것의 상태를 꺼내 와야 합니다. 이 일에 값이 붙습니다. 그리고 프로세스를 바꾸느냐 스레드를 바꾸느냐에 따라 값이 다릅니다.

    프로세스는 주소 공간, 스레드는 그 안의 실행 흐름

    둘의 경계는 페이지 테이블을 공유하느냐에서 갈립니다.

    프로세스는 가상 주소를 물리 주소로 번역하는 표를 하나 가집니다. 그 표가 프로세스마다 따로라서 서로의 메모리에 닿을 수 없습니다.

    스레드는 그 표를 같이 씁니다. 따로 갖는 것은 셋뿐입니다.

    • 자기 스택
    • 레지스터 값들
    • 지금 어느 명령을 실행 중인지
    프로세스 하나 물리 메모리 스레드 1 스레드 2 페이지 테이블 둘이 같이 씁니다 힙 · 코드 · 열린 파일 둘이 같이 씁니다 스택 · 레지스터 자기 것 스택 · 레지스터 자기 것 프레임들
    스레드가 나눠 갖지 않는 것은 스택과 레지스터뿐이다.

    그래서 같은 프로세스의 스레드끼리는 서로의 데이터에 그냥 닿습니다. 편한 만큼 위험한 것도 여기서 나옵니다.

    갈아 끼울 때 실제로 바뀌는 것

    이 차이가 그대로 전환 비용이 됩니다.

    TLB 는 최근 번역 결과를 담아 두는 CPU 안의 작은 캐시입니다. 이게 왜 있고 없으면 얼마나 비싼지는 번역도 공짜가 아니다에 적었습니다. 표가 바뀌면 그 안에 담긴 번역은 다른 프로세스의 답이라 쓸 수 없습니다. 그래서 버립니다.

    버린 다음에는 다시 채워야 합니다. 새 프로세스는 한동안 번역을 매번 표에서 찾아 가며 돕니다.

    값은 갈아 끼운 다음에 옵니다

    레지스터를 저장하고 복원하는 것 자체는 몇백 나노초짜리 일입니다. 문제는 그다음입니다.

    • 캐시가 식습니다. 새로 올라온 스레드가 쓰는 데이터는 캐시에 없습니다
    • TLB 가 비어 있습니다. 프로세스가 바뀐 경우에 특히 그렇습니다
    • 분기 예측이 헛돕니다. 전에 배워 둔 것이 다른 코드의 습관입니다

    그래서 전환 직후의 코드는 같은 코드인데 몇 배 느립니다. 프로파일러에는 전환 비용으로 안 잡힙니다. 그 시간은 전부 “그 함수가 느림” 으로 기록됩니다.

    그래서 스레드를 많이 만들면 안 됩니다

    흔한 오해가 “스레드를 늘리면 일이 빨리 끝난다” 입니다. 코어 수를 넘어서면 그때부터는 번갈아 태우는 것이라 일이 더 늘어납니다.

    • 코어 여덟 개에 워커 여덟 개면 각자 자리를 잡고 끝까지 돕니다
    • 코어 여덟 개에 워커 백 개면 백 개가 번갈아 타면서 서로의 캐시를 밀어냅니다

    그래서 엔진의 잡 시스템은 코어 수에 맞춰 워커를 만들고 그 위에 일감을 얹습니다. 스레드를 일감마다 만드는 것이 아니라 스레드는 고정해 두고 일감만 갈아 끼우는 것입니다.

    모바일은 한 겹 더 있습니다. 코어가 성능이 다른 두 종류로 나뉩니다. 같은 스레드가 작은 코어에 올라가면 같은 코드가 몇 배 느립니다. 프레임이 들쭉날쭉한데 원인이 안 보이면 이걸 의심해 볼 값이 있습니다.

    게임에서 보이는 자리

    메인 스레드가 자리를 비우면 그 프레임은 끝입니다. 다른 스레드가 대신 돌아도 화면은 메인 스레드가 끝내야 나옵니다.

    메인 스레드가 자리를 비우는 흔한 경우들입니다.

    • 락을 기다릴 때. 커널이 그 스레드를 재우고 다른 것을 올립니다. 깨어날 때 전환 값이 또 붙습니다
    • 파일을 읽을 때. 읽기가 끝날 때까지 재워집니다
    • Sleep 을 부를 때. 정확히 그 시간만큼 자는 것이 아니라 다시 태워 줄 때까지 잡니다

    마지막 것이 프레임 페이싱에서 자주 물립니다. 1밀리초를 자라고 했는데 스케줄러가 다음에 태워 주는 시점이 그보다 늦으면, 그 차이가 그대로 프레임에 실립니다.

    정리

    • 프로세스와 스레드를 가르는 것은 페이지 테이블을 공유하느냐입니다. 스레드가 따로 갖는 것은 스택과 레지스터뿐입니다
    • 스레드끼리 전환은 레지스터를 갈아 끼우는 값입니다. 프로세스끼리 전환은 거기에 표를 바꾸고 TLB 를 버리는 값이 얹힙니다
    • 진짜 값은 갈아 끼운 다음에 옵니다. 캐시와 TLB 가 식어서 같은 코드가 한동안 느립니다
    • 그 시간은 프로파일러에 전환 비용으로 안 잡히고 함수 시간으로 기록됩니다
    • 코어 수를 넘는 스레드는 일을 늘립니다. 스레드는 고정해 두고 일감을 갈아 끼우는 쪽이 맞습니다