확장팩 FFF 모음 정리 링크



팩토리오 개발자는 신이다



https://www.factorio.com/blog/post/fff-421



FFF #421 - 최적화 2.0

게시자 Rseding, boskid, kovarex

2024-07-26



안녕하세요,

우리 모두는 점점 더 큰 공장을 짓는 것을 좋아하지만, UPS 천장에 부딪히면 기분이 나빠지죠.

그렇기 때문에 게임 최적화를 위한 끝없는 탐구를 계속해야 합니다.




로보포트 최적화 - Rseding


팩토리오에서 수년 동안 많은 세이브 파일을 프로파일링하면서 물류 로봇이나 건설 로봇의 업데이트에 많은 시간이 걸리는 세이브를 자주 보았습니다. 이는 새로운 일은 아니지만 해당 세이브에는 로봇과 함께 로보포트도 대량으로 등장합니다.



로보포트가 많은 전형적인 공장입니다.



로보포트는 결코 "느린" 적이 없지만 항상 존재하며, 다가오는 우주 시대 확장팩에는 원격으로 많은 일을 하게끔 더 많이 만들도록 권장됩니다. 가장 최근의 플레이 테스트 세션 후, 결과 세이브파일에서 작지만 결코 0은 아닌 시간이 걸리는 것을 보고 다시 한 번 생각하게 되었습니다.


로보포트가 항상 활성화되고 업데이트될 필요가 없다면 정말 좋을 것 같습니다. 대부분의 로보포트는 전력을 소비하는 것 외에는 할 일이 없습니다. 가끔씩 로봇의 에너지와 관련해서 로봇을 충전/보관해야 할 때가 있습니다. 하지만 대부분은 물류 네트워크의 연장선상에 존재하는 것 외에는 별다른 역할을 하지 않습니다. 그래서 실험을 해봤습니다. 특별한 작업이 필요하지 않을 때는 그냥 꺼두면 어떨까요? 로봇이 와서 충전이 필요하거나 기타 드물게 로봇이 일을 해야 하는 상황이 발생하면 그 일이 끝날 때까지 로보포트를 다시 켜는 것이죠.


물론 이보다 더 복잡한 내부적인 문제가 있었지만 최종적으로는 효과가 있었습니다.

최근 플레이 테스트 저장 파일에서 로보포트에 소요되는 시간이 평균 1ms에서 0.025ms로 줄었습니다.








레이더 로직 최적화 - Rseding


이번 달 초, 할 일 목록에 "작은 레이더 범위" 영역을 로보포트에 추가하는 사소한 기능 카드가 하나 올라왔습니다. 일반적으로는 5분 정도면 구현할 수 있는 작업이지만, 이 카드에는 작은 단서가 붙어 있었습니다: "겹치는 영역이 최적화 성능 비용을 증가시키지 않는 방식으로 수행".


이 시점까지만 해도 플레이어와 같은 다른 사물에 내장된 레이더 기능은 "자주 반복해서 그 주변을 둘러보고 맵 시스템에 계속 노출되도록 요청하는" 매우 단순한 방식이었습니다. 일반적으로 플레이어나 레이더가 겹치는 경우가 많지 않기 때문에 이 방식은 대부분 잘 작동했습니다. 하지만 이제는 겹치는 부분이 많고 매우 촘촘하게 구축하는 로보포트에도 동일한 로직이 적용되기를 원했습니다.


그래서 맵의 특정 영역을 공개하려는 모든 개체가 맵 시스템에서 해당 청크의 카운터를 늘려서 청크를 '계속 공개 옵션'으로 등록하는 시스템을 사용하기로 결정했습니다. 카운터가 0보다 크면 청크는 계속 표시됩니다. 원하는 만큼 겹칠 수 있으며 단순히 카운터 숫자가 늘어날 뿐입니다.




'계속 공개' 카운터를 표시하는 디버그 옵션



청크의 카운터가 1보다 크면 업데이트 버킷에 넣고 매 틱마다 1개의 버킷을 반복합니다.

청크의 카운터가 0으로 돌아가면 버킷에서 제거되고 업데이트가 중지됩니다.




버킷을 반복하여 청크 활성화 맵을 유지합니다.

(50% 속도로 재생됩니다)



이 새로운 시스템을 통해 레이더 엔티티의 로직과 다른 엔티티의 레이더 로직을 모두 동일한 시스템을 사용하도록 교체할 수 있었습니다. 그 결과 레이더 엔티티의 업데이트 시간이 줄어들고 레이더 범위를 제공하는 로보포트와 결합하여 레이더가 더 이상 필요하지 않게 되는 놀라운 효과를 얻었습니다.


또 다른 플레이 테스트 세이브 파일에서는 전반적인 게임 성능이 3.6% 향상되었습니다. 측정 가능한 영향을 미치려는 의도가 아니었다는 점을 감안하면(오히려 로보포트에 레이더 범위를 추가하면 성능이 악화될 것으로 예상했습니다), 3.6%의 전반적인 성능 향상은 대단한 수치입니다.


따라서 이는 성공적이었으며, 2.0 로보포트에는 2청크 만큼의 작은 '내장' 레이더 범위가 추가됩니다.








램프 항상 켜짐 - boskid


최근 사무실 랜 파티에서 레스토랑에서 식사를 하던 중 kovarex는 램프의 RGB 색상을 선택할 수 있다는 점을 감안하여 램프를 사용하여 이미지를 배치하고 싶다는 아이디어를 공유했습니다. 문제는 램프에 전원을 공급하기 위해 변전소를 자주 배치해야 해서 이미지가 약간 보기 흉해진다는 것이었습니다. 이 시점에서 저는 우주 플랫폼에는 전봇대가 필요 없다는 것을 알고 "우주 플랫폼에 짓지 않는 한"이라고 말했습니다. 다음 날 무슨 일이 일어날지 몰라 침착하게 기다리는 동안 그는 행복하고 신이 난 것 같았습니다.


다음 날 플레이 테스트가 시작되자 그는 가로 100, 세로 150 크기의 청사진에 램프를 넣고 그 램프의 RGB 색상을 이미지로 변환하여 우주 플랫폼에 배치했습니다. 하지만 우주 플랫폼은 항상 낮이기 때문에 램프가 켜지지 않는다는 한 가지 중요한 문제가 있었습니다. 낮 동안 램프를 강제로 켜려면 회로망에 램프를 연결해야 했습니다. 이는 세이브 파일의 업데이트 시간이 너무 빨라지면서 금방 알아차릴 수 있었습니다.




제어 동작에 의해 강제로 켜진 램프 중 하나.



15,000개의 램프를 매 틱마다 업데이트해야 하는 제어 동작을 갖는 것은 전혀 최적화스럽지 않기에 낮에도 램프를 강제로 켤 수 있도록 만들었습니다.




항상 켜짐을 사용하는 램프



이렇게 해서 램프가 회로망에 연결되는 것을 건너뛰는 것만으로도 저장 파일의 업데이트 속도가 약 1.2ms 빨라졌습니다. 하지만 변경 후 제어 동작의 업데이트 시간을 보면 왜 여전히 제어 동작 업데이트가 이렇게 느린지 조금 걱정이 되어 이를 확인해야 했습니다.









벨트 읽기 및 제어 동작 멀티스레딩 - boskid


제어 동작 업데이트가 여전히 느린 이유는 쉽게 설명할 수 있습니다. 바로 제가 만든 기능인 모든 벨트의 내용물을 순차적으로 읽을 수 있는 기능(FFF-405)이 문제였습니다.


벨트 전체 읽기 기능 자체는 램프 항상 켜짐 옵션과 비슷한 이유로 추가되었는데, 단순히 내용물을 읽기 위해 모든 벨트를 개별적으로 연결해야 하고 그로 인한 활성화된 제어 동작의 양을 줄이기 위해서였습니다. 모든 벨트의 내용물을 순서대로 읽을 수 있다는 것은 그 중 하나의 벨트만 와이어로 연결하면 되고, 하나의 제어 동작만 아이템을 계산하면 되며, 이송 라인 자체를 1타일 길이로 분할할 필요가 없다는 것을 의미합니다. 벨트 전체 읽기 기능을 추가함으로써 효과적으로 제어 동작의 업데이트 비용과 운송 라인의 업데이트 비용을 크게 줄이는 동시에 이전에는 불가능했던 새로운 기능(예: 지하 벨트에 있는 아이템 갯수)을 얻을 수 있었습니다.


벨트 전체 읽기 기능의 장점은 사용이 매우 간편하다는 점으로, 플레이 테스트 기간 동안 의도한 사용 사례인 우주 플랫폼뿐만 아니라 다른 곳에서도 많이 사용되었습니다.




벨트 전체 읽기 기능이 예상치 못한 곳에서 사용되는 모습.



플레이 테스트의 가장 큰 방해꾼은 바로 Hrusa라고 할 수 있겠네요. 그는 벨트 전체 읽기 기능 없이도 해결할 수 있는 일부 공장에 해당 기능을 남발했습니다. 또한 풀고라에 능동 공급 상자를 배치하여 1만 대 이상의 물류 로봇이 항상 공중에 떠 있는 물류 로봇 지옥으로 만들었습니다. 게임을 자유롭게 플레이하는 것을 금지할 수는 없으니 최적화가 필요했습니다. 물류 로봇의 코드 최적화는 다른 사람이 담당했지만, 벨트 전체 읽기 기능과 제어 동작은 제가 처리해야 했습니다.


벨트 전체 읽기 기능은 벨트 위에 어떤 아이템이 있는지, 벨트 위의 각 스택에 몇 개가 있는지 확인하여 총계를 계산하는 매우 단순한 구조입니다.


해당 기능을 최적화하는 작업은 몇 번의 반복을 거쳤습니다. 주요 문제중 하나는 운송 라인에서 총량을 계속 계산하려고 시도했지만 실패했던 일입니다.


벨트 전체 읽기 기능은 대부분 메모리(벨트 스택의 내용)에서 많은 데이터를 읽어 아이템을 계산하고 마지막에 회로망으로 전송할 단일 프레임의 신호를 생성하는 '읽기' 작업이라는 매우 간단한 사실을 깨닫게 된 것이 전환점이 되었습니다. 이 구조는 멀티스레딩이 가능해야 한다는 것을 의미합니다. 동시에 계산되는 여러 벨트 리더기는 벨트 내용물만 읽고 그 출력은 다른 벨트 리더기에서 사용하지 않기 때문에 서로 간섭하지 않습니다. 물류 네트워크 콘텐츠를 읽는 로보포트, 산술 조합기, 셀렉터 조합기, 일정신호 조합기 등 다른 곳에서도 비슷한 구조를 쉽게 볼 수 있습니다. 글로벌 랜덤 제너레이터를 더 이상 사용할 수 없는 셀렉터 조합기를 제외하고는 모두 멀티스레드로 만들기가 비교적 쉬웠습니다.


이러한 변경을 통해 플레이 테스트 세이브 파일을 약 9.5% 더 빠르게 실행할 수 있었습니다.


개발 중에 사용하던 6천 개의 회로 네트워크와 상호 연결된 7천 개의 조합기로 채워진 테스트용 세이브 파일은 CPU 사용률을 100%로 일관되게 유지하면서 14.9배 빠르게 실행되었습니다(벤치마크 완료 시간이 131초에서 약 8.2초로 단축됨).








실패한 시도: 멀티스레딩 전기 네트워크 업데이트 - boskid


"전기 네트워크 업데이트를 멀티스레드로 만들어주세요."

이것은 제가 모든 사람들로부터 계속 듣는 것 중 하나입니다.


전기 네트워크 업데이트는 이미 개선되었지만(FFF-209), 여전히 하나의 스레드로만 수행되어 업데이트 속도가 가장 느린 것으로 관찰되는 경우가 많았습니다. 현재 대부분의 경우 게임에는 하나의 거대한 전기 네트워크만 있기 때문에 멀티스레딩이 큰 이점을 주지 못합니다. 하지만 우주 시대에는 여러 행성에 나뉜 대규모 네트워크들이 많기 때문에 잠재된 문제점이 더 큽니다.


멀티스레드를 사용하는 모든 것의 경우 가장 먼저 해야 할 일은 움직이는 부분과 이들이 서로 어떻게 상호 작용하는지 파악하는 것입니다. 게임이 완전히 결정론적인 상태를 유지해야 하고 그렇지 않으면 동기화가 해제되기 때문에 이 모든 것이 매우 중요합니다.


언뜻 보기에는 각 스레드가 서로 다른 전기 네트워크에서 작동하면 끝나는 매우 단순한 구조라고 생각할 수 있습니다. 하지만 게임 메커니즘을 약간 더 복잡하게 만드는 한 가지 요소가 있는데, 바로 여러 전기 네트워크에서 엔티티를 구동할 수 있는 기능입니다.



전기 네트워크가 독립적이지 않은 경우 중 하나.



이 예제에서는 정유소가 레시피를 제작 중일 때 내부 에너지가 방전되어 전기 네트워크로 하여금 다시 충전해야 합니다. 여기서 발생할 수 있는 두 가지 경우가 있습니다:


- 왼쪽 전기 네트워크가 먼저 업데이트됨 - 증기 엔진에서 정유소를 충전하면 보일러가 연료를 연소하고 투입기가 활성화됩니다.

- 오른쪽 전기 네트워크가 먼저 업데이트됨 - 태양광 패널에서 정유소를 충전합니다(이 경우 보일러는 쉬는 상태로 유지됨).


이 때문에 네트워크는 서로 의존적이며 동일한 스레드에서 업데이트해야 합니다.


전원 스위치가 닫혀 여러 네트워크가 병합되는 경우와 같은 모든 경우를 찾아낸 후 업데이트 그룹에 포함해야 할 항목을 정의할 수 있었습니다. 두 개의 전기 네트워크에 두 네트워크에 의해 전원이 공급되는 엔티티가 하나 이상 있는 경우, 해당 네트워크는 동일한 업데이트 그룹에 속해야 하고 동일한 스레드로 업데이트되어야 하며 동기화 해제가 발생하지 않아야 합니다.


측정을 시작할 수 있었습니다...




결과


이 아이디어는 여기서 완전히 실패했습니다. 플레이 테스트 세이브 파일에는 적어도 4개의 대형 전기 네트워크가 서로 다른 행성에 있기 때문에 완전히 독립되어 있지만, 전기 네트워크 업데이트 시간은 동일하게 유지되고 오히려 CPU 사용량은 크게 증가했습니다.


결과적으로 전기 네트워크 업데이트는 변수를 두 개 읽고 한두 개를 더한 다음 다음 엔티티로 이동하는 등 실제로 많은 작업을 수행하지 않습니다. 메모리 처리량이 제한되어 있었고 메모리에서 데이터를 읽는 스레드가 많아지면 프로세서가 단순히 데이터를 더 빨리 읽을 수 없었습니다. 여기에 멀티스레딩을 사용하면 하나의 스레드가 메모리를 대기하는 대신 전기 네트워크 업데이트를 수행하는 모든 스레드가 메모리를 대기하면서 서로 속도가 느려집니다.


이 아이디어를 완전히 폐기하기 위해 인텔의 VTune과 같은 추가 프로파일링 툴을 사용하여 전기 네트워크가 메모리 처리량에 제한이 있음을 보여주는 더 많은 수치 인수를 제공해야 했습니다. 플레이 테스트 세이브 파일의 전기 네트워크 업데이트 시간은 0.5ms에서 0.39ms로 개선되었지만 CPU 사용량은 0.5%에서 15%로 증가했습니다. 전반적으로 세이브 파일이 더 빠르게 실행되지는 않았습니다.










작업 로봇의 똑똑한 업데이트 - kovarex


사무실 세이브 파일에서 물류 로봇에 문제가 있음을 발견했습니다. 누군가 자동화 시스템에서 회로 전선을 제거하여 풀고라의 로봇 포트에 로봇을 더 추가했습니다. 몇 시간 만에 모든 로봇 포트가 가득 차서 10,000대의 로봇이 갈 곳이 없는 물류 네트워크가 과밀화되는 상황이 발생했습니다.


몇 시간 동안 아무도 이 문제를 알아차리지 못했기 때문에 첫 번째 대응은 저장 공간이 없다는 알림과 유사하게 작업 로봇이 휴식할 공간이 없는 경우에 대한 알림을 추가하는 것이었습니다.




로봇 포트 주변에 모여 있는 집 없는 로봇들.



하지만 근본적인 문제는 작업 로봇 업데이트가 너무 단순하다는 것입니다. 모든 로봇은 매 틱마다 반복되고 업데이트 로직이 실행됩니다. 그러나 업데이트 로직은 대부분 "이 목표물을 향해 계속 이동", "충전을 시작하기 위해 대기열에서 대기" 등과 같이 매우 간단합니다.


실제로는 한 번씩만 업데이트하면서 무언가를 부드럽게 움직이는 것처럼 보이게 하는 것은 새로운 아이디어가 아닙니다. 우리는 9년 전에 청크 업데이트 플래너(FFF-161)와 연기 파티클 업데이트 트릭(FFF-84)을 사용했기 때문에 이러한 기술이 작업 로봇에 적용되는 것은 시간 문제였을 뿐입니다.


작업 로봇의 경우 연기만 피우는 것과 달리 다양한 작업(다양한 유형의 운송, 건설, 충전 등)을 수행한다는 점이 문제였지만, 게임을 최적화할 다른 방법을 찾고자 하는 의욕이 있었기 때문에 바로 뛰어들었습니다.


이 과제의 핵심은 로봇의 움직임이었습니다. 대부분의 논리는 다음과 같은 간단한 방식으로 작동했습니다.


- 도착했나요?

- 아니요! 거의 다 왔어요.

- 이제 다 왔나요?

- 네, 로직의 다음 부분을 수행하세요.


하지만 이제 로봇에 이동 의도를 저장하고 어딘가로 이동하고 싶을 때 이를 사용해야 합니다. 이동 의도를 알면 로봇이 루프에 들어갈 수 있고 렌더링 코드는 이 의도를 사용하여 로봇이 항상 원활하게 움직이는 척할 수 있습니다. 또한 목적지에 도착하는 시간을 계산하고 다음 업데이트 일정을 잡는 데 사용할 수 있습니다.



시각화 디버그: 빨간색 실제 로봇은 20틱에 한 번만 업데이트됩니다. 고스트 로봇은 렌더링에 사용되는 예상 위치입니다.



로봇이 장시간 이동할 수 있고 멀리 있는 로봇이 화면 중앙으로 이동할 수도 있지만 물리적인 위치가 너무 멀어서 알 수 없기 때문에 어느 정도 한계가 있어야 합니다. 비슷한 종류의 다른 이유(예를 들어 화면 밖에서 빛을 내는 램프)로 화면 가장자리를 확인하므로 어느 정도 여유가 있습니다. 그래서 저는 업데이트 없이 최대 20틱의 움직임과 업데이트 없이 60틱의 정지된 로봇을 하드코딩된 매직넘버로 정의하기로 결정했습니다. 이 수치는 더 늘릴 수도 있지만, 이 시점에서 실질적인 성능 향상은 극히 미미할 것입니다.





결과


사무실 LAN 파티 세이브 파일에서는 전체 세이브 파일의 성능이 15% 향상되었으며, 일반적으로 로봇의 양에 따라 다르지만 로봇을 많이 사용하는 세이브 파일이라고 하면 전체 성능 향상은 일반적으로 10-25% 정도입니다. 저는 이를 성공이라고 생각합니다.


모든 변경 사항이 결합되어 우주 시대 확장팩에서 성능 면에서 더 편안한 영역으로 들어가고 있습니다. 하지만 플레이어가 더 미칠듯이 자유롭게 플레이할 수 있도록 조금 더 밀어붙이면 좋을 것 같습니다. 많은 도움이 될 수 있는 몇 가지 아이디어가 있으니 계속 지켜봐 주시고, 실패한 사후 분석이 아닌 성공 사례가 되기를 바랍니다.




언제나 그렇듯이 평소와 마찬가지로 여러분의 의견을 알려주세요.