시간을 재는 것도 공짜가 아니다
Esc

    시간을 재는 것도 공짜가 아니다

    –OS

    게임은 시간을 계속 묻습니다. 이번 프레임이 얼마나 걸렸는지, 쿨다운이 끝났는지, 다음 프레임까지 얼마 남았는지.

    그런데 시계가 하나가 아닙니다. 그리고 시간을 묻는 것도, 기다리는 것도 정확하지 않습니다.

    시계가 둘이다

    OS 는 성격이 다른 시계를 둘 줍니다.

    벽시계 단조 시계
    무엇을 재나 지금 몇 시인지 기기가 켜진 뒤로 흐른 시간
    뒤로 갈 수 있나 갑니다 안 갑니다
    왜 흔들리나 시각 동기화, 사용자가 시계를 바꿈, 시간대 흔들리지 않습니다
    쓰는 자리 날짜를 보여줄 때, 서버와 맞출 때 경과 시간을 잴 때 전부
    벽시계 단조 시계 시각 동기화 뒤로 물러남
    벽시계는 밖에서 고쳐지면 튀거나 물러난다. 단조 시계는 그 일이 없다.

    그래서 경과 시간을 벽시계로 재면 안 됩니다. 두 시각을 빼서 음수가 나오는 일이 실제로 생깁니다. 쿨다운을 벽시계로 재 두면 기기 시각을 돌려서 넘길 수도 있습니다.

    기기가 잠든 동안 단조 시계가 멈추는지 계속 가는지는 플랫폼마다 다릅니다. 백그라운드에서 돌아온 직후의 값은 어느 쪽이든 한 번 의심하고 잘라 내야 합니다.

    시간을 묻는 값

    시각을 아는 것은 원래 커널의 일입니다. 물어보려면 모드를 넘어갔다 와야 합니다. 그런데 이 질문은 너무 자주 나옵니다. 그래서 커널이 시간 값을 사용자 쪽에서 읽을 수 있는 자리에 올려 둡니다.

    덕분에 요즘 기계에서는 시간을 묻는 데 수십 나노초면 끝납니다. 문을 넘는 값이 얼마인지는 커널에 들어갔다 나오는 값에 적었습니다.

    싸다고 공짜는 아닙니다. 촘촘한 반복문 안에서 매번 시각을 묻거나, 구간마다 시간을 재서 로그로 남기면 재는 값이 재려던 것보다 커집니다. 프레임 시작에 한 번 받아 두고 그 프레임 내내 같은 값을 쓰는 이유가 이것입니다.

    자는 것은 정확하지 않다

    1밀리초를 자라고 하면 1밀리초 뒤에 깨어나는 것이 아닙니다. 1밀리초 뒤에 “깨워도 되는 상태” 가 될 뿐입니다. 실제로 언제 다시 CPU 에 올라가는지는 스케줄러가 정합니다.

    여기에 둘이 더 붙습니다. 타이머가 얼마나 촘촘하게 깨울 수 있는지, 그리고 그때 다른 스레드가 자리를 차지하고 있는지. 그래서 요청한 시간보다 늦게 깹니다. 얼마나 늦을지는 그때그때 다릅니다.

    남은 시간을 기다리는 방법은 둘입니다. 프레임 페이싱에서 실제로 고르는 갈림길입니다.

    장점
    • 코어를 놓아 줍니다. 그동안 다른 스레드가 일하고, 전력도 덜 씁니다.
    • 발열이 덜합니다. 모바일에서는 이게 곧 성능입니다.
    단점
    • 언제 깰지 모릅니다. 스케줄러가 다시 태워 줄 때까지 늦습니다.
    • 깬 다음이 느립니다. 캐시가 식은 채로 다시 시작합니다.
    장점
    • 깨어나는 지연이 없습니다. 목표 시각을 훨씬 정확하게 맞힐 수 있습니다.
    단점
    • 코어를 붙잡고 있습니다. 그 시간에 다른 스레드가 못 돕니다.
    • 전력과 발열을 그대로 씁니다. 모바일에서는 오래 못 버팁니다.
    • 쓰로틀링이 걸리면 정확해지려고 한 짓이 전체를 느리게 만듭니다.
    어디서 무엇을 고르나
    대부분의 구간 잠들어 기다립니다. 늦게 깨는 것보다 코어를 붙잡는 손해가 큽니다
    목표 시각 직전 짧은 구간 돌면서 기다리는 것이 값어치가 있을 때가 있습니다
    모바일 기본은 잠드는 쪽입니다. 발열이 곧 다음 프레임의 속도라서요

    프레임이 일정해 보이려면

    여기서 한 가지가 더 갈립니다. 일정한 간격으로 그리는 것과, 화면이 그걸 일정하게 받는 것은 다른 문제입니다.

    한 프레임이 걸린 시간을 재서 다음 프레임에 쓰는 것이 델타 시간입니다. 이 값이 흔들리면 화면도 흔들립니다. 프레임 하나가 오래 걸린 다음 프레임에서는 델타가 크게 들어오고, 그 값으로 위치를 계산하면 캐릭터가 순간이동합니다.

    그래서 대개 이렇게 합니다.

    • 델타에 상한을 둡니다. 한 프레임이 아무리 오래 걸려도 그 값 이상은 안 넘깁니다
    • 물리는 고정 간격으로 돕니다. 남은 시간을 모아 두었다가 정해진 크기로 나눠 돌립니다
    • 백그라운드에서 돌아온 직후 한 프레임은 버립니다. 그 델타는 실제 진행이 아닙니다

    화면에 언제 나가는지는 또 다른 이야기입니다. 그리기를 끝낸 시점과 화면이 새 그림을 받는 시점이 따로 있고, 그 사이를 맞춰 주는 장치가 수직 동기화입니다. 프레임레이트를 어디에 맞출지는 한 줄로 배터리를 절반 쓰는 방법에서 다뤘습니다.

    게임에서 자주 틀리는 자리

    틀리는 것 왜
    쿨다운을 벽시계로 잼 기기 시각을 바꾸면 넘어갑니다. 시각 동기화로 음수가 나오기도 합니다
    백그라운드 복귀 직후 델타를 그대로 씀 몇 분짜리 델타가 한 번에 들어옵니다
    반복문 안에서 매번 시각을 물음 재는 값이 쌓입니다. 프레임 시작에 한 번이면 됩니다
    정확하게 재겠다고 계속 돌면서 기다림 코어를 붙잡고 발열을 올려서 다음 프레임을 느리게 만듭니다
    서버 시간과 기기 시간을 섞어 씀 기준이 둘이 되면 어긋난 만큼이 그대로 버그입니다

    정리

    • 시계는 둘입니다. 날짜는 벽시계로, 경과 시간은 단조 시계로 잽니다. 벽시계는 뒤로 갑니다
    • 시간을 묻는 것은 원래 커널의 일이지만 너무 자주 불려서 사용자 쪽에서 읽을 수 있게 올려 둔 값입니다. 싸지만 공짜는 아닙니다
    • 자는 것은 정확하지 않습니다. 요청한 시간은 깨워도 되는 시점일 뿐이고, 실제로 언제 올라가는지는 스케줄러가 정합니다
    • 기다리는 방법은 둘이고 대부분은 잠드는 쪽이 맞습니다. 모바일에서는 발열이 곧 다음 프레임의 속도라서 더 그렇습니다
    • 델타 시간은 그대로 믿으면 안 됩니다. 상한을 두고, 물리는 고정 간격으로 돌리고, 복귀 직후 한 프레임은 버립니다